diff --git a/SAS/TMSS/docker-compose-ua.yml b/SAS/TMSS/docker-compose-ua.yml index 92097328364f16615a1a435ad1d3a6b61a046f43..7d2f96c0c010b2c8614ad920c1977b78eaa33019 100644 --- a/SAS/TMSS/docker-compose-ua.yml +++ b/SAS/TMSS/docker-compose-ua.yml @@ -1,6 +1,12 @@ # This docker-compose is used to run TMSS together with a test open ID connect server on the User Acceptance system (tmss-ua) version: "3" services: + rabbitmq: + image: rabbitmq:latest + hostname: rabbitmq + ports: + - 5672:5672 + - 15672:15672 web: image: nexus.cep4.control.lofar:18080/tmss_django:latest restart: on-failure diff --git a/SAS/TMSS/frontend/tmss_webapp/package.json b/SAS/TMSS/frontend/tmss_webapp/package.json index b8a53534056c2171aa2bddc4e885529473f5ef16..98a358b8eab1a02049d514cb456e457f2e997b1d 100644 --- a/SAS/TMSS/frontend/tmss_webapp/package.json +++ b/SAS/TMSS/frontend/tmss_webapp/package.json @@ -45,7 +45,7 @@ "test": "react-scripts test", "eject": "react-scripts eject" }, - "proxy": "http://127.0.0.1:8008/", + "proxy": "http://192.168.99.100:8008/", "eslintConfig": { "extends": "react-app" }, diff --git a/SAS/TMSS/frontend/tmss_webapp/src/App.js b/SAS/TMSS/frontend/tmss_webapp/src/App.js index 0d028b94c866d1375b2fff83dad07463d1bf8822..af0b8d760c3c17348d38f349362ee983a10dbc1b 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/App.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/App.js @@ -35,9 +35,10 @@ class App extends Component { this.menu = [ {label: 'Dashboard', icon: 'pi pi-fw pi-home', to:'/dashboard'}, {label: 'Cycle', icon:'pi pi-fw pi-spinner', to:'/cycle'}, + {label: 'Project', icon: 'fab fa-fw fa-wpexplorer', to:'/project'}, {label: 'Scheduling Units', icon: 'pi pi-fw pi-calendar', to:'/schedulingunit'}, - {label: 'Tasks', icon: 'pi pi-fw pi-check-square', to:'/task'}, - {label: 'Project', icon: 'fa fa-fw fa-binoculars', to:'/project'}, + // {label: 'Tasks', icon: 'pi pi-fw pi-check-square', to:'/task'}, + ]; diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/ViewTable.js b/SAS/TMSS/frontend/tmss_webapp/src/components/ViewTable.js index 9116f1ae11c5ab5bc18c7a447f2f708a6229e9ba..1265b8af9357f528a4232d0fa20bbce57f91008f 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/components/ViewTable.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/components/ViewTable.js @@ -1,11 +1,13 @@ -import React, {useRef } from "react"; -import { useSortBy, useTable, useFilters, useGlobalFilter, useAsyncDebounce } from 'react-table' +import React, {useRef, useState } from "react"; +import { useSortBy, useTable, useFilters, useGlobalFilter, useAsyncDebounce, usePagination } from 'react-table' import matchSorter from 'match-sorter' import _ from 'lodash'; import moment from 'moment'; import { useHistory } from "react-router-dom"; import {OverlayPanel} from 'primereact/overlaypanel'; - +import {InputSwitch} from 'primereact/inputswitch'; +import { Calendar } from 'primereact/calendar'; +import {Paginator} from 'primereact/paginator'; let tbldata =[]; let isunittest = false; @@ -16,7 +18,6 @@ function GlobalFilter({ globalFilter, setGlobalFilter, }) { - const [value, setValue] = React.useState(globalFilter) const onChange = useAsyncDebounce(value => {setGlobalFilter(value || undefined)}, 200) return ( @@ -46,10 +47,173 @@ function DefaultColumnFilter({ ) } + +// This is a custom filter UI for selecting +// a unique option from a list +function SelectColumnFilter({ + column: { filterValue, setFilter, preFilteredRows, id }, +}) { + // Calculate the options for filtering + // using the preFilteredRows + const options = React.useMemo(() => { + const options = new Set() + preFilteredRows.forEach(row => { + options.add(row.values[id]) + }) + return [...options.values()] + }, [id, preFilteredRows]) + + // Render a multi-select box + return ( + <select + value={filterValue} + onChange={e => { + setFilter(e.target.value || undefined) + }} + > + <option value="">All</option> + {options.map((option, i) => ( + <option key={i} value={option}> + {option} + </option> + ))} + </select> + ) +} + +// This is a custom filter UI that uses a +// slider to set the filter value between a column's +// min and max values +function SliderColumnFilter({ + column: { filterValue, setFilter, preFilteredRows, id }, +}) { + // Calculate the min and max + // using the preFilteredRows + + const [min, max] = React.useMemo(() => { + let min = preFilteredRows.length ? preFilteredRows[0].values[id] : 0 + let max = preFilteredRows.length ? preFilteredRows[0].values[id] : 0 + preFilteredRows.forEach(row => { + min = Math.min(row.values[id], min) + max = Math.max(row.values[id], max) + }) + return [min, max] + }, [id, preFilteredRows]) + + return ( + <> + <input + type="range" + min={min} + max={max} + value={filterValue || min} + onChange={e => { + setFilter(parseInt(e.target.value, 10)) + }} + /> + <button onClick={() => setFilter(undefined)}>Off</button> + </> + ) +} + +// This is a custom filter UI that uses a +// switch to set the value +function BooleanColumnFilter({ + column: { setFilter}, +}) { + const [value, setValue] = useState(true); + return ( + <> + <InputSwitch checked={value} onChange={() => { setValue(!value); setFilter(!value); }} /> + <button onClick={() => setFilter(undefined)}>Off</button> + </> + ) +} + +// This is a custom filter UI that uses a +// calendar to set the value +function CalendarColumnFilter({ + column: { setFilter}, +}) { + const [value, setValue] = useState(''); + return ( + <> + <Calendar value={value} onChange={(e) => { + const value = moment(e.value, moment.ISO_8601).format("YYYY-MMM-DD") + setValue(value); setFilter(value); + }} showIcon></Calendar> + <button onClick={() => setFilter(undefined)}>Off</button> + </> + ) +} + + +// This is a custom UI for our 'between' or number range +// filter. It uses two number boxes and filters rows to +// ones that have values between the two +function NumberRangeColumnFilter({ + column: { filterValue = [], preFilteredRows, setFilter, id }, +}) { + const [min, max] = React.useMemo(() => { + let min = preFilteredRows.length ? preFilteredRows[0].values[id] : 0 + let max = preFilteredRows.length ? preFilteredRows[0].values[id] : 0 + preFilteredRows.forEach(row => { + min = Math.min(row.values[id], min) + max = Math.max(row.values[id], max) + }) + return [min, max] + }, [id, preFilteredRows]) + + return ( + <div + style={{ + display: 'flex', + }} + > + <input + value={filterValue[0] || ''} + type="number" + onChange={e => { + const val = e.target.value + setFilter((old = []) => [val ? parseInt(val, 10) : undefined, old[1]]) + }} + placeholder={`Min (${min})`} + style={{ + width: '70px', + marginRight: '0.5rem', + }} + /> + to + <input + value={filterValue[1] || ''} + type="number" + onChange={e => { + const val = e.target.value + setFilter((old = []) => [old[0], val ? parseInt(val, 10) : undefined]) + }} + placeholder={`Max (${max})`} + style={{ + width: '70px', + marginLeft: '0.5rem', + }} + /> + </div> + ) +} + + function fuzzyTextFilterFn(rows, id, filterValue) { return matchSorter(rows, filterValue, { keys: [row => row.values[id]] }) } +const filterTypes = { + 'select': SelectColumnFilter, + 'switch': BooleanColumnFilter, + 'slider': SliderColumnFilter, + 'date': CalendarColumnFilter, + 'range': NumberRangeColumnFilter +}; + // Let the table remove the filter if the string is empty fuzzyTextFilterFn.autoRemove = val => !val @@ -103,19 +267,25 @@ function Table({ columns, data, defaultheader, optionalheader }) { allColumns, getToggleHideAllColumnsProps, state, + page, preGlobalFilteredRows, setGlobalFilter, setHiddenColumns, + gotoPage, + setPageSize, + } = useTable( { columns, data, defaultColumn, filterTypes, + initialState: { pageIndex: 0 } }, useFilters, useGlobalFilter, useSortBy, + usePagination ) React.useEffect(() => { @@ -124,12 +294,21 @@ function Table({ columns, data, defaultheader, optionalheader }) { ); }, [setHiddenColumns, columns]); - const firstPageRows = rows.slice(0, 10) let op = useRef(null); + const [currentpage, setcurrentPage] = React.useState(0); + const [currentrows, setcurrentRows] = React.useState(10); + + const onPagination = (e) => { + gotoPage(e.page); + setcurrentPage(e.first); + setcurrentRows(e.rows); + setPageSize(e.rows) + }; + return ( <> - <div id="block_container" style={{ display: 'flex', verticalAlign: 'middle', marginTop:'20px'}}> + <div id="block_container"> <div style={{textAlign:'left', marginRight:'30px'}}> <i className="fa fa-columns col-filter-btn" label="Toggle Columns" onClick={(e) => op.current.toggle(e)} /> <OverlayPanel ref={op} id="overlay_panel" showCloseIcon={false} > @@ -145,7 +324,8 @@ function Table({ columns, data, defaultheader, optionalheader }) { </div> {allColumns.map(column => ( <div key={column.id} style={{'display':column.id !== 'actionpath'?'block':'none'}}> - <input type="checkbox" {...column.getToggleHiddenProps()} /> {(defaultheader[column.id])?defaultheader[column.id]:(optionalheader[column.id]?optionalheader[column.id]:column.id)} + <input type="checkbox" {...column.getToggleHiddenProps()} /> { + (defaultheader[column.id]) ? defaultheader[column.id] : (optionalheader[column.id] ? optionalheader[column.id] : column.id)} </div> ))} <br /> @@ -167,40 +347,41 @@ function Table({ columns, data, defaultheader, optionalheader }) { </div> </div> - <div style={{overflow: 'auto', padding: '0.75em',}}> - - <table {...getTableProps()} style={{width:'100%'}} data-testid="viewtable" className="viewtable" > + <div className="table_container"> + <table {...getTableProps()} data-testid="viewtable" className="viewtable" > <thead> {headerGroups.map(headerGroup => ( <tr {...headerGroup.getHeaderGroupProps()}> {headerGroup.headers.map(column => ( - <th {...column.getHeaderProps(column.getSortByToggleProps())} > + <th> + <div {...column.getHeaderProps(column.getSortByToggleProps())}> {column.Header !== 'actionpath' && column.render('Header')} - {/* {column.Header !== 'Action'? - column.isSorted ? (column.isSortedDesc ? <i className="pi pi-sort-down" aria-hidden="true"></i> : <i className="pi pi-sort-up" aria-hidden="true"></i>) : <i className="pi pi-sort" aria-hidden="true"></i> + {column.Header !== 'Action'? + column.isSorted ? (column.isSortedDesc ? <i className="pi pi-sort-down" aria-hidden="true"></i> : <i className="pi pi-sort-up" aria-hidden="true"></i>) : "" : "" - } */} - {/* Render the columns filter UI */} + } + </div> + + {/* Render the columns filter UI */} {column.Header !== 'actionpath' && - <div className={columnclassname[0][column.Header]} > {column.canFilter && column.Header !== 'Action' ? column.render('Filter') : null}</div> + <div className={columnclassname[0][column.Header]} > + {column.canFilter && column.Header !== 'Action' ? column.render('Filter') : null} + + </div> } - </th> + </th> ))} </tr> ))} </thead> <tbody {...getTableBodyProps()}> - {firstPageRows.map((row, i) => { - + {page.map((row, i) => { prepareRow(row) return ( <tr {...row.getRowProps()}> - {row.cells.map(cell => { - if(cell.column.id !== 'actionpath') - return <td {...cell.getCellProps()} >{cell.render('Cell')}</td> - else - return ""; + {row.cells.map(cell => { + return <td {...cell.getCellProps()}>{cell.render('Cell')}</td> })} </tr> ) @@ -208,6 +389,9 @@ function Table({ columns, data, defaultheader, optionalheader }) { </tbody> </table> </div> + <div className="pagination"> + <Paginator rowsPerPageOptions={[10,25,50,100]} first={currentpage} rows={currentrows} totalRecords={rows.length} onPageChange={onPagination}></Paginator> + </div> </> ) } @@ -242,7 +426,7 @@ function ViewTable(props) { let defaultdataheader = Object.keys(defaultheader[0]); let optionaldataheader = Object.keys(optionalheader[0]); - if(props.showaction === 'true'){ + if(props.showaction === 'true') { columns.push({ Header: 'Action', id:'Action', @@ -264,17 +448,17 @@ function ViewTable(props) { }) } // Object.entries(props.paths[0]).map(([key,value]) =>{}) - - } //Default Columns - defaultdataheader.forEach(header =>{ + defaultdataheader.forEach(header => { + const isString = typeof defaultheader[0][header] === 'string'; columns.push({ - Header: defaultheader[0][header], - id: defaultheader[0][header], + Header: isString ? defaultheader[0][header] : defaultheader[0][header].name, + id: isString ? defaultheader[0][header] : defaultheader[0][header].name, accessor: header, - filter: 'fuzzyText', + filter: (!isString && defaultheader[0][header].filter=== 'date') ? 'includes' : 'fuzzyText', + Filter: isString ? DefaultColumnFilter : (filterTypes[defaultheader[0][header].filter] ? filterTypes[defaultheader[0][header].filter] : DefaultColumnFilter), isVisible: true, Cell: props => <div> {updatedCellvalue(header, props.value)} </div>, }) @@ -282,11 +466,13 @@ function ViewTable(props) { //Optional Columns optionaldataheader.forEach(header => { + const isString = typeof optionalheader[0][header] === 'string'; columns.push({ - Header: optionalheader[0][header], - id: header, + Header: isString ? optionalheader[0][header] : optionalheader[0][header].name, + id: isString ? optionalheader[0][header] : optionalheader[0][header].name, accessor: header, - filter: 'fuzzyText', + filter: (!isString && optionalheader[0][header].filter=== 'date') ? 'includes' : 'fuzzyText', + Filter: isString ? DefaultColumnFilter : (filterTypes[optionalheader[0][header].filter] ? filterTypes[optionalheader[0][header].filter] : DefaultColumnFilter), isVisible: false, Cell: props => <div> {updatedCellvalue(header, props.value)} </div>, }) @@ -320,8 +506,6 @@ function ViewTable(props) { return value; } - - return ( <div> <Table columns={columns} data={tbldata} defaultheader={defaultheader[0]} optionalheader={optionalheader[0]} /> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_viewtable.scss b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_viewtable.scss index 1ff00e3767bed05640a6646c99d5dc1e391e481e..e01190f81d64d52bc77229f233135fd198961f0c 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_viewtable.scss +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_viewtable.scss @@ -1,7 +1,19 @@ - .viewtable{ +#block_container { + display: flex; + vertical-align: middle; + margin-top: 20px +} + +.table_container { + overflow: auto; + padding: 0.75em 0; +} + +.viewtable{ overflow: auto !important; padding: 0.75em; - } + width: 100%; +} .viewtable th { color: #7e8286; @@ -21,7 +33,41 @@ padding: .65rem; border-bottom: 1px solid lightgray; overflow-wrap: anywhere; -} +} + +.pagination { + display: block !important; +} + +body .p-paginator { + background-color: #ebeaea; + border: none; + border-bottom: 1px solid lightgray; + border-top: 1px solid lightgray; +} + +.p-paginator .p-paginator-icon { + display: block; + position: absolute; + left: 50%; + top: 50%; + width: 1em; + height: 1em; + margin-top: -.5em; + margin-left: -.5em; + border-color: black; + color: black; +} + +.p-dropdown .p-dropdown-trigger .p-dropdown-trigger-icon { + top: 50%; + left: 50%; + margin-top: -.5em; + margin-left: -.5em; + position: absolute; + border-color: black; + border: black; +} .filter-input input{ max-width: 175px; @@ -51,3 +97,24 @@ width: 175px; } +.table_container .pi { + padding-left: 3px; +} + +/* }.pagination button { + margin-left: 3px; + background-color: #005b9f; + border: 1px solid #005b9f; + border-radius: 4px; + color: white; + font-weight: 900; +} + +.pagination button:disabled { + margin-left: 3px; + background-color: #c8c9c9; + border: 1px solid #c8c9c9; + border-radius: 4px; + color: white; +} */ + diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/ResourceInputList.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/ResourceInputList.js index 940dec9e217daa5f9a9f103a59a874c4e5e6f526..a188d348b297f4e601f82792923ce561fcd8ae6d 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/ResourceInputList.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/ResourceInputList.js @@ -41,7 +41,7 @@ export class ResourceInputList extends Component { <InputNumber key={'item1-'+ index} id={'item1-'+ index} name={'item1-'+ index} suffix={` ${this.props.unitMap[item.quantity_value]?this.props.unitMap[item.quantity_value].display:''}`} placeholder={` ${this.props.unitMap[item.quantity_value]?this.props.unitMap[item.quantity_value].display:item.name}`} min={0} useGrouping={false} - value={this.state.cycleQuota[item.name]} + value={this.props.cycleQuota[item.name]} onChange={(e) => this.onInputChange(item.name, e)} onBlur={(e) => this.onInputChange(item.name, e)} style={{width:"90%", marginRight: "5px"}} diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/create.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/create.js index 73268577c74802b207eaad26edb399fded4885ee..79ddd60b6b80b15e064ae8a60df296ff6b51674b 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/create.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/create.js @@ -50,13 +50,13 @@ export class CycleCreate extends Component { {name:'LOFAR Observing Time'}, {name:'LOFAR Observing Time prio A'}, {name:'LOFAR Observing Time prio B'}, - {name:'LOFAR Processing time '}, - {name:'LOFAR LTA resources'}, + {name:'CEP Processing Time'}, + {name:'LTA Storage'}, {name:'LOFAR LTA resources SARA'}, {name:'LOFAR LTA resources Jülich'}, {name:'LOFAR LTA resources Poznan'}, {name:'LOFAR Observing time DDT/Commissioning'}, - {name:'LOFAR Support'}]; + {name:'LOFAR Support Time'}]; this.cycleResourceDefaults = {}; // Default values for default resources this.resourceUnitMap = UnitConverter.resourceUnitMap; // Resource unit conversion factor and constraints this.tooltipOptions = UIConstants.tooltipOptions; @@ -98,7 +98,7 @@ export class CycleCreate extends Component { setCycleQuotaDefaults(resources) { let cycleQuota = this.state.cycleQuota; for (const resource of resources) { - const conversionFactor = this.resourceUnitMap[resource.quantity_value]?this.resourceUnitMap[resource.quantity_value].conversionFactor:1; + // const conversionFactor = this.resourceUnitMap[resource.quantity_value]?this.resourceUnitMap[resource.quantity_value].conversionFactor:1; // cycleQuota[resource['name']] = this.cycleResourceDefaults[resource.name]/conversionFactor; cycleQuota[resource['name']] = 0; } diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/edit.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/edit.js new file mode 100644 index 0000000000000000000000000000000000000000..4d599c33ef64cd790084b5f0012a3c2ad3fde1e4 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/edit.js @@ -0,0 +1,505 @@ +import React, {Component} from 'react'; +import { Link, Redirect } from 'react-router-dom'; +import _ from 'lodash'; +import moment from 'moment' + +import {InputText} from 'primereact/inputtext'; +import {Calendar} from 'primereact/calendar'; +import {InputTextarea} from 'primereact/inputtextarea'; +import {Dropdown} from 'primereact/dropdown'; +import { Button } from 'primereact/button'; +import {Dialog} from 'primereact/components/dialog/Dialog'; +import {Growl} from 'primereact/components/growl/Growl'; + +import {ResourceInputList} from './ResourceInputList'; + +import AppLoader from '../../layout/components/AppLoader'; +import CycleService from '../../services/cycle.service'; +import UnitConverter from '../../utils/unit.converter'; +import UIConstants from '../../utils/ui.constants'; + +export class CycleEdit extends Component { + constructor(props) { + super(props); + this.state = { + isLoading: true, + dialog: { header: '', detail: ''}, + cycle: { + projects: [], + quota: [], // Mandatory Field in the back end + }, + cycleQuota: {}, // Holds the value of resources selected with resource_type_id as key + validFields: {}, // Holds the list of valid fields based on the form rules + validForm: false, // To enable Save Button + errors: {}, + resources: [], // Selected resources for the cycle + resourceList: [], // Available resources to select for the cycle + redirect: this.props.match.params.id?"":'/cycle/list' //If no cycle name passed redirect to Cycle list page + } + this.cycleQuota = [] // Holds the old list of cycle_quota saved for the cycle + // Validation Rules + this.formRules = { + name: {required: true, message: "Name can not be empty"}, + description: {required: true, message: "Description can not be empty"}, + start: {required: true, message: "Start Date can not be empty"}, + stop: {required: true, message: "Stop Date can not be empty"}, + }; + this.defaultResources = [ + {name:'LOFAR Observing Time'}, + {name:'LOFAR Observing Time prio A'}, + {name:'LOFAR Observing Time prio B'}, + {name:'LOFAR Processing time '}, + {name:'LOFAR LTA resources'}, + {name:'LOFAR LTA resources SARA'}, + {name:'LOFAR LTA resources Jülich'}, + {name:'LOFAR LTA resources Poznan'}, + {name:'LOFAR Observing time DDT/Commissioning'}, + {name:'LOFAR Support'}]; + this.cycleResourceDefaults = {}; + this.resourceUnitMap = UnitConverter.resourceUnitMap; + this.tooltipOptions = UIConstants.tooltipOptions; + + this.getCycleDetails = this.getCycleDetails.bind(this); + this.cycleOptionTemplate = this.cycleOptionTemplate.bind(this); + this.setCycleQuotaDefaults = this.setCycleQuotaDefaults.bind(this); + this.setCycleParams = this.setCycleParams.bind(this); + this.addNewResource = this.addNewResource.bind(this); + this.removeResource = this.removeResource.bind(this); + this.setCycleQuotaParams = this.setCycleQuotaParams.bind(this); + this.saveCycle = this.saveCycle.bind(this); + this.saveCycleQuota = this.saveCycleQuota.bind(this); + this.cancelEdit = this.cancelEdit.bind(this); + } + + componentDidMount() { + CycleService.getCycle(this.props.match.params.id) + .then(result =>{ + this.setState({ + cycle: result.data, + isLoading : false, + }) + }) + .then(()=>{ + CycleService.getResources() + .then(resourceList => { + this.setState({resourceList: resourceList}); + }) + .then((resourceList, resources) => { + this.getCycleDetails(); + }); + }) + } + + /** + * Function retrieve cycle details and resource allocations(cycle_quota) and assign to appropriate varaibles + */ + async getCycleDetails() { + let cycle = this.state.cycle; + let resourceList = this.state.resourceList; + let cycleQuota = {}; + if (cycle) { + // Get cycle_quota for the cycle and asssign to the component variable + for (const id of cycle.quota_ids) { + let quota = await CycleService.getCycleQuota(id); + let resource = _.find(resourceList, ['name', quota.resource_type_id]); + quota.resource = resource; + this.cycleQuota.push(quota); + const conversionFactor = this.resourceUnitMap[resource.quantity_value]?this.resourceUnitMap[resource.quantity_value].conversionFactor:1; + cycleQuota[quota.resource_type_id] = quota.value / conversionFactor; + }; + // Remove the already assigned resources from the resoureList + const resources = _.remove(resourceList, (resource) => { return _.find(this.cycleQuota, {'resource_type_id': resource.name})!=null }); + this.setState({cycle: cycle, resourceList: resourceList, resources: resources, + cycleQuota: cycleQuota, isLoading: false}); + + // Validate form if all values are as per the form rules and enable Save button + this.validateForm(); + } else { + this.setState({redirect: '../../not-found'}); + } + } + + /** + * Cycle option sub-component with cycle object + */ + cycleOptionTemplate(option) { + return ( + <div className="p-clearfix"> + <span style={{fontSize:'1em',float:'right',margin:'1em .5em 0 0'}}>{option.name}</span> + </div> + ); + } + + /** + * Function to set cycle resource allocation + * @param {Array} resources + */ + setCycleQuotaDefaults(resources) { + let cycleQuota = this.state.cycleQuota; + for (const resource of resources) { + const conversionFactor = this.resourceUnitMap[resource.quantity_value]?this.resourceUnitMap[resource.quantity_value].conversionFactor:1; + cycleQuota[resource['name']] = this.cycleResourceDefaults[resource.name]/conversionFactor; + } + return cycleQuota; + } + + /** + * Function to add new resource to cycle + */ + addNewResource(){ + if (this.state.newResource) { + let resourceList = this.state.resourceList; + const newResource = _.remove(resourceList, {'name': this.state.newResource}); + let resources = this.state.resources?this.state.resources:[]; + resources.push(newResource[0]); + this.setState({resources: resources, resourceList: resourceList, newResource: null}); + } + } + + /** + * Callback function to be called from ResourceInpulList when a resource is removed from it + * @param {string} name - resource_type_id + */ + removeResource(name) { + let resources = this.state.resources; + let resourceList = this.state.resourceList; + let cycleQuota = this.state.cycleQuota; + const removedResource = _.remove(resources, (resource) => { return resource.name === name }); + resourceList.push(removedResource[0]); + delete cycleQuota[name]; + this.setState({resourceList: resourceList, resources: resources, cycleQuota: cycleQuota}); + } + + /** + * Function to call on change and blur events from input components + * @param {string} key + * @param {any} value + */ + setCycleParams(key, value, type) { + let cycle = this.state.cycle; + switch(type) { + case 'NUMBER': { + cycle[key] = value?parseInt(value):0; + break; + } + default: { + cycle[key] = value; + break; + } + } + this.setState({cycle: cycle, validForm: this.validateForm(key)}); + } + + /** + * Callback Function to call from ResourceInputList on change and blur events + * @param {string} key + * @param {InputEvent} event + */ + setCycleQuotaParams(key, event) { + let cycleQuota = this.state.cycleQuota; + if (event.target.value) { + let resource = _.find(this.state.resources, {'name': key}); + let newValue = 0; + if (this.resourceUnitMap[resource.quantity_value] && + event.target.value.toString().indexOf(this.resourceUnitMap[resource.quantity_value].display)>=0) { + newValue = event.target.value.replace(this.resourceUnitMap[resource.quantity_value].display,''); + } else { + newValue = event.target.value; + } + cycleQuota[key] = (newValue==="NaN" || isNaN(newValue))?0:newValue; + } else { + let cycleQuota = this.state.cycleQuota; + cycleQuota[key] = 0; + } + this.setState({cycleQuota: cycleQuota}); + } + + /** + * Validation function to validate the form or field based on the form rules. + * If no argument passed for fieldName, validates all fields in the form. + * @param {string} fieldName + */ + validateForm(fieldName) { + let validForm = false; + let errors = this.state.errors; + let validFields = this.state.validFields; + if (fieldName) { + delete errors[fieldName]; + delete validFields[fieldName]; + if (this.formRules[fieldName]) { + const rule = this.formRules[fieldName]; + const fieldValue = this.state.cycle[fieldName]; + if (rule.required) { + if (!fieldValue) { + errors[fieldName] = rule.message?rule.message:`${fieldName} is required`; + } else { + validFields[fieldName] = true; + } + } + } + } else { + errors = {}; + validFields = {}; + for (const fieldName in this.formRules) { + const rule = this.formRules[fieldName]; + const fieldValue = this.state.cycle[fieldName]; + if (rule.required) { + if (!fieldValue) { + errors[fieldName] = rule.message?rule.message:`${fieldName} is required`; + } else { + validFields[fieldName] = true; + } + } + } + } + + if (Object.keys(validFields).length === Object.keys(this.formRules).length) { + validForm = true; + } + + if(this.state.cycle['start'] && this.state.cycle['stop']){ + var isSameOrAfter = moment(this.state.cycle['stop']).isSameOrAfter(this.state.cycle['start']); + if(!isSameOrAfter){ + errors['stop'] = ` Stop date should be after Start date`; + validForm = false; + }else{ + validForm = true; + } + } + + this.setState({errors: errors, validFields: validFields, validForm: validForm}); + return validForm; + } + + /** + * Function to call when 'Save' button is clicked to update the cycle. + */ + saveCycle() { + if (this.validateForm) { + let cycle = this.state.cycle; + let stoptime = _.replace(this.state.cycle['stop'],'00:00:00', '23:59:59'); + cycle['start'] = moment(this.state.cycle['start']).format("YYYY-MM-DDTHH:mm:ss"); + cycle['stop'] = moment(stoptime).format("YYYY-MM-DDTHH:mm:ss"); + this.setState({cycle: cycle}); + CycleService.updateCycle(this.props.match.params.id, this.state.cycle) + .then(async (cycle) => { + if (cycle && this.state.cycle.updated_at !== cycle.updated_at) { + this.saveCycleQuota(cycle); + } else { + this.growl.show({severity: 'error', summary: 'Error Occured', detail: 'Unable to update Cycle'}); + this.setState({errors: cycle}); + } + }); + } + } + + /** + * Function to Create, Update & Delete cycle_quota for the cycle + */ + async saveCycleQuota(cycle) { + let dialog = {}; + let quotaError = {}; + let updatingCycleQuota = []; + let newCycleQuota = []; + let deletingCycleQuota = []; + for (const resource in this.state.cycleQuota) { + const resourceType = _.find(this.state.resources, {'name': resource}); + const conversionFactor = this.resourceUnitMap[resourceType.quantity_value]?this.resourceUnitMap[resourceType.quantity_value].conversionFactor:1 + let quotaValue = this.state.cycleQuota[resource] * conversionFactor; + let existingQuota = _.find(this.cycleQuota, {'resource_type_id': resource}); + if (!existingQuota) { + let quota = { cycle: cycle.url, + resource_type: resourceType['url'], + value: quotaValue }; + newCycleQuota.push(quota); + } else if (existingQuota && existingQuota.value !== quotaValue) { + existingQuota.cycle = cycle.url; + existingQuota.value = quotaValue; + updatingCycleQuota.push(existingQuota); + } + } + let cycleQuota = this.state.cycleQuota; + deletingCycleQuota = _.filter(this.cycleQuota, function(quota) { return !cycleQuota[quota.resource_type_id]}); + + for (const cycleQuota of deletingCycleQuota) { + const deletedCycleQuota = await CycleService.deleteCycleQuota(cycleQuota); + if (!deletedCycleQuota) { + quotaError[cycleQuota.resource_type_id] = true; + } + } + for (const cycleQuota of updatingCycleQuota) { + const updatedCycleQuota = await CycleService.updateCycleQuota(cycleQuota); + if (!updatedCycleQuota) { + quotaError[cycleQuota.resource_type_id] = true; + } + } + for (const cycleQuota of newCycleQuota) { + const createdCycleQuota = await CycleService.saveCycleQuota(cycleQuota); + if (!createdCycleQuota) { + quotaError[cycleQuota.resource_type_id] = true; + } + } + if (_.keys(quotaError).length === 0) { + dialog = {header: 'Success', detail: 'Cycle updated successfully.'}; + } else { + dialog = {header: 'Error', detail: 'Cycle updated successfully but resource allocation not updated properly. Try again!'}; + } + this.setState({dialogVisible: true, dialog: dialog}); + } + + /** + * Cancel edit and redirect to Cycle View page + */ + cancelEdit() { + this.setState({redirect: `/cycle/view/${this.state.cycle.name}`}); + } + + render() { + if (this.state.redirect) { + return <Redirect to={ {pathname: this.state.redirect} }></Redirect> + } + return ( + <React.Fragment> + <div className="p-grid"> + <Growl ref={(el) => this.growl = el} /> + + <div className="p-col-10 p-lg-10 p-md-10"> + <h2>Cycle - Edit</h2> + </div> + <div className="p-col-2 p-lg-2 p-md-2"> + <Link to={{ pathname: `/cycle/view/${this.state.cycle.name}`}} title="Close Edit" style={{float: "right"}}> + <i className="fa fa-window-close" style={{marginTop: "10px"}}></i> + </Link> + </div> + </div> + + { this.state.isLoading ? <AppLoader/> : + <> + <div> + <div className="p-fluid"> + <div className="p-field p-grid"> + <label htmlFor="cycleName" className="col-lg-2 col-md-2 col-sm-12">Name <span style={{color:'red'}}>*</span></label> + <div className="col-lg-3 col-md-3 col-sm-12"> + <InputText className={this.state.errors.name ?'input-error':''} id="cycleName" data-testid="name" + tooltip="Enter name of the cycle" tooltipOptions={this.tooltipOptions} maxLength="128" + value={this.state.cycle.name} + onChange={(e) => this.setCycleParams('name', e.target.value)} + onBlur={(e) => this.setCycleParams('name', e.target.value)}/> + <label className={this.state.errors.name?"error":"info"}> + {this.state.errors.name ? this.state.errors.name : "Max 128 characters"} + </label> + </div> + <div className="col-lg-1 col-md-1 col-sm-12"></div> + <label htmlFor="description" className="col-lg-2 col-md-2 col-sm-12">Description <span style={{color:'red'}}>*</span></label> + <div className="col-lg-3 col-md-3 col-sm-12"> + <InputTextarea className={this.state.errors.description ?'input-error':''} rows={3} cols={30} + tooltip="Short description of the cycle" tooltipOptions={this.tooltipOptions} maxLength="128" + data-testid="description" value={this.state.cycle.description} + onChange={(e) => this.setCycleParams('description', e.target.value)} + onBlur={(e) => this.setCycleParams('description', e.target.value)}/> + <label className={this.state.errors.description ?"error":"info"}> + {this.state.errors.description ? this.state.errors.description : "Max 255 characters"} + </label> + </div> + </div> + + <div className="p-field p-grid"> + <label htmlFor="cycleName" className="col-lg-2 col-md-2 col-sm-12">Start Date <span style={{color:'red'}}>*</span></label> + <div className="col-lg-3 col-md-3 col-sm-12"> + <Calendar + d dateFormat="dd-M-yy" + inputId="start" + value= {new Date(this.state.cycle.start)} + onChange= {e => this.setCycleParams('start',e.value)} + onBlur= {e => this.setCycleParams('start',e.value)} + data-testid="start" + tooltip="Moment at which the cycle starts, that is, when its projects can run." tooltipOptions={this.tooltipOptions} + showIcon={true} + /> + <label className={this.state.errors.start?"error":"info"}> + {this.state.errors.start ? this.state.errors.start : ""} + </label> + </div> + <div className="col-lg-1 col-md-1 col-sm-12"></div> + <label htmlFor="cycleName" className="col-lg-2 col-md-2 col-sm-12">Stop Date <span style={{color:'red'}}>*</span></label> + <div className="col-lg-3 col-md-3 col-sm-12"> + <Calendar + d dateFormat="dd-M-yy" + value= {new Date(this.state.cycle.stop)} + onChange= {e => this.setCycleParams('stop', e.value)} + onBlur= {e => this.setCycleParams('stop',e.value)} + inputId="stop" + data-testid="stop" + tooltip="Moment at which the cycle officially ends." tooltipOptions={this.tooltipOptions} + showIcon={true} + /> + <label className={this.state.errors.stop?"error":"info"}> + {this.state.errors.stop ? this.state.errors.stop : ""} + </label> + </div> + </div> + + {this.state.resourceList && + <div className="p-fluid"> + <div className="p-field p-grid"> + <div className="col-lg-2 col-md-2 col-sm-12"> + <h5>Resource Allocations:</h5> + </div> + <div className="col-lg-3 col-md-3 col-sm-10"> + <Dropdown optionLabel="name" optionValue="name" + tooltip="Resources to be allotted for the cycle" + tooltipOptions={this.tooltipOptions} + value={this.state.newResource} + options={_.sortBy(this.state.resourceList, ['name'])} + onChange={(e) => {this.setState({'newResource': e.value})}} + placeholder="Add Resources" /> + </div> + <div className="col-lg-2 col-md-2 col-sm-2"> + <Button label="" className="p-button-primary" icon="pi pi-plus" onClick={this.addNewResource} disabled={!this.state.newResource} data-testid="add_res_btn" /> + </div> + </div> + {/* {_.keys(this.state.cycleQuota).length>0 && */} + <div className="p-field p-grid resource-input-grid"> + <ResourceInputList list={this.state.resources} unitMap={this.resourceUnitMap} + cycleQuota={this.state.cycleQuota} callback={this.setCycleQuotaParams} + removeInputCallback={this.removeResource} /> + </div> + {/* } */} + </div> + } + </div> + </div> + <div className="p-grid p-justify-start act-btn-grp"> + <div className="p-col-1"> + <Button label="Save" className="p-button-primary" id="save-btn" data-testid="save-btn" icon="pi pi-check" onClick={this.saveCycle} disabled={!this.state.validForm} /> + </div> + <div className="p-col-1"> + <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.cancelEdit} /> + </div> + </div> + + </> + } + {/* Dialog component to show messages and get input */} + <div className="p-grid" data-testid="confirm_dialog"> + <Dialog header={this.state.dialog.header} visible={this.state.dialogVisible} style={{width: '30vw'}} inputId="confirm_dialog" + modal={true} onHide={() => {this.setState({dialogVisible: false})}} + footer={<div> + <Button key="back" onClick={() => {this.setState({dialogVisible: false}); this.cancelEdit();}} label="Ok" /> + {/* <Button key="submit" type="primary" onClick={this.reset} label="Yes" /> */} + </div> + } > + <div className="p-grid"> + <div className="col-lg-2 col-md-2 col-sm-2"> + <i className="pi pi-check-circle pi-large pi-success"></i> + </div> + <div className="col-lg-10 col-md-10 col-sm-10"> + <span style={{marginTop:"5px"}}>{this.state.dialog.detail}</span> + </div> + </div> + </Dialog> + </div> + </React.Fragment> + ); + } +} \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/index.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/index.js index 09c0ab83797cff44dcaea7d193e408a362e94f10..a52436177a762d8b91a368fa6e9394ca77ee6f2c 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/index.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/index.js @@ -1,5 +1,6 @@ import CycleList from './list'; import {CycleCreate} from './create'; -import CycleView from './view'; +import {CycleView} from './view'; +import {CycleEdit} from './edit'; -export {CycleList, CycleCreate, CycleView}; +export {CycleList, CycleCreate, CycleView, CycleEdit}; diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/list.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/list.js index 55dcecbd82c16b036fe00205e82e9ccb28802b41..8ecbf062eccfb70a68b53adfb27e3e8fbe81a115 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/list.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/list.js @@ -70,14 +70,22 @@ class CycleList extends Component{ cycle.id = cycle.name ; cycle.regularProjects = regularProjects.length; cycle.longterm = longterm.length; - cycle.observingTime = this.getUnitConvertedQuotaValue(cycle, cycleQuota, 'observing_time'); - cycle.processingTime = this.getUnitConvertedQuotaValue(cycle, cycleQuota, 'cep_processing_time'); - cycle.ltaResources = this.getUnitConvertedQuotaValue(cycle, cycleQuota, 'lta_storage'); - cycle.support = this.getUnitConvertedQuotaValue(cycle, cycleQuota, 'support_time'); - cycle.observingTimeDDT = this.getUnitConvertedQuotaValue(cycle, cycleQuota, 'observing_time_commissioning'); - cycle.observingTimePrioA = this.getUnitConvertedQuotaValue(cycle, cycleQuota, 'observing_time_prio_a'); - cycle.observingTimePrioB = this.getUnitConvertedQuotaValue(cycle, cycleQuota, 'observing_time_prio_b'); - cycle['actionpath'] = '/cycle/view'; + // cycle.observingTime = this.getUnitConvertedQuotaValue(cycle, cycleQuota, 'observing_time'); + // cycle.processingTime = this.getUnitConvertedQuotaValue(cycle, cycleQuota, 'cep_processing_time'); + // cycle.ltaResources = this.getUnitConvertedQuotaValue(cycle, cycleQuota, 'lta_storage'); + // cycle.support = this.getUnitConvertedQuotaValue(cycle, cycleQuota, 'support_time'); + // cycle.observingTimeDDT = this.getUnitConvertedQuotaValue(cycle, cycleQuota, 'observing_time_commissioning'); + // cycle.observingTimePrioA = this.getUnitConvertedQuotaValue(cycle, cycleQuota, 'observing_time_prio_a'); + // cycle.observingTimePrioB = this.getUnitConvertedQuotaValue(cycle, cycleQuota, 'observing_time_prio_b'); + cycle.observingTime = this.getUnitConvertedQuotaValue(cycle, cycleQuota, 'LOFAR Observing Time'); + cycle.processingTime = this.getUnitConvertedQuotaValue(cycle, cycleQuota, 'CEP Processing Time'); + cycle.ltaResources = this.getUnitConvertedQuotaValue(cycle, cycleQuota, 'LTA Storage'); + cycle.support = this.getUnitConvertedQuotaValue(cycle, cycleQuota, 'LOFAR Support Time'); + cycle.observingTimeDDT = this.getUnitConvertedQuotaValue(cycle, cycleQuota, 'LOFAR Observing Time Commissioning'); + cycle.observingTimePrioA = this.getUnitConvertedQuotaValue(cycle, cycleQuota, 'LOFAR Observing Time prio A'); + cycle.observingTimePrioB = this.getUnitConvertedQuotaValue(cycle, cycleQuota, 'LOFAR Observing Time prio B'); + + cycle['actionpath'] = `/cycle/view/${cycle.id}`; return cycle; }); this.setState({ @@ -88,7 +96,7 @@ class CycleList extends Component{ } componentDidMount(){ - const promises = [CycleService.getCycleQuota(), CycleService.getResources()] + const promises = [CycleService.getAllCycleQuotas(), CycleService.getResources()] Promise.all(promises).then(responses => { const cycleQuota = responses[0]; this.setState({ resources: responses[1] }); diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/view.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/view.js index a03ae5e6608bff3f31c50e1441bcc5fc8acca4c2..55430cc1fd440589988e30d2a1e13aa1e91c4d51 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/view.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/view.js @@ -1,17 +1,140 @@ import React, {Component} from 'react'; +import {Link, Redirect} from 'react-router-dom' +import moment from 'moment'; +import _ from 'lodash'; +import { Chips } from 'primereact/chips'; +import ResourceDisplayList from './ResourceDisplayList'; + +import AppLoader from '../../layout/components/AppLoader'; +import CycleService from '../../services/cycle.service'; +import UnitConverter from '../../utils/unit.converter'; + +/** + * Component to view the details of a cycle + */ export class CycleView extends Component { + DATE_FORMAT = 'YYYY-MMM-DD HH:mm:ss'; + constructor(props) { + super(props); + this.state = { + isLoading: true, + }; + if (this.props.match.params.id) { + this.state.cycleId = this.props.match.params.id; + } else if (this.props.location.state && this.props.location.state.id) { + this.state.cycleId = this.props.location.state.id; + } + this.state.redirect = this.state.cycleId?"":'/cycle' // If no cycle id is passed, redirect to cycle list page + this.resourceUnitMap = UnitConverter.resourceUnitMap; // Resource unit conversion factor and constraints + } - constructor(props){ - super(props) - console.log(this.props) + componentDidMount() { + const cycleId = this.state.cycleId; + if (cycleId) { + this.getCycleDetails(); + } else { + this.setState({redirect: "/not-found"}); + } } + + /** + * To get the cycle details from the backend using the service + * + */ + async getCycleDetails() { + let cycle = await CycleService.getCycleDetails(this.state.cycleId); + let cycleQuota = []; + let resources = []; + + if (cycle) { + // If resources are allocated for the cycle quota fetch the resources master from the API + if (cycle.quota) { + resources = await CycleService.getResources(); + } + + // For every cycle quota, get the resource type & assign to the resource variable of the quota object + for (const id of cycle.quota_ids) { + let quota = await CycleService.getCycleQuota(id); + let resource = _.find(resources, ['name', quota.resource_type_id]); + quota.resource = resource; + cycleQuota.push(quota); + }; + this.setState({cycle: cycle, cycleQuota: cycleQuota, isLoading: false}); + } else { + this.setState({redirect: "../../not-found"}) + } + + } + render() { + if (this.state.redirect) { + return <Redirect to={ {pathname: this.state.redirect} }></Redirect> + } + return ( - <h1>CycleView</h1> + <React.Fragment> + <div className="p-grid"> + <div className="p-col-10 p-lg-10 p-md-10"> + <h2>Cycle - Details </h2> + </div> + { this.state.cycle && + <div className="p-col-2 p-lg-2 p-md-2"> + <Link to={{ pathname: `/cycle`}} title="Close View" style={{float: "right"}}> + <i className="fa fa-times" style={{marginTop: "10px", marginLeft: "5px"}}></i> + </Link> + <Link to={{ pathname: `/cycle/edit/${this.state.cycle.name}`, state: {id: this.state.cycle?this.state.cycle.name:''}}} title="Edit Cycle" + style={{float: "right"}}> + <i className="fa fa-edit" style={{marginTop: "10px"}}></i> + </Link> + </div> + } + </div> + { this.state.isLoading && <AppLoader /> } + { this.state.cycle && + <React.Fragment> + <div className="main-content"> + <div className="p-grid"> + <label className="col-lg-2 col-md-2 col-sm-12">Name</label> + <span className="col-lg-4 col-md-4 col-sm-12">{this.state.cycle.name}</span> + <label className="col-lg-2 col-md-2 col-sm-12">Description</label> + <span className="col-lg-4 col-md-4 col-sm-12">{this.state.cycle.description}</span> + </div> + <div className="p-grid"> + <label className="col-lg-2 col-md-2 col-sm-12">Created At</label> + <span className="col-lg-4 col-md-4 col-sm-12">{moment.utc(this.state.cycle.created_at).format(this.DATE_FORMAT)}</span> + <label className="col-lg-2 col-md-2 col-sm-12">Updated At</label> + <span className="col-lg-4 col-md-4 col-sm-12">{moment.utc(this.state.cycle.updated_at).format(this.DATE_FORMAT)}</span> + </div> + + <div className="p-grid"> + <label className="col-lg-2 col-md-2 col-sm-12">Projects</label> + <Chips className="col-lg-4 col-md-4 col-sm-12 chips-readonly" disabled value={this.state.cycle.projects_ids}></Chips> + </div> + <div className="p-fluid"> + <div className="p-field p-grid"> + <div className="col-lg-3 col-md-3 col-sm-12"> + <h5 data-testid="resource_alloc">Resource Allocations</h5> + </div> + </div> + </div> + {this.state.cycleQuota.length===0 && + <div className="p-field p-grid"> + <div className="col-lg-12 col-md-12 col-sm-12"> + <span>Reosurces not yet allocated. + <Link to={{ pathname: `/cycle/edit/${this.state.cycle.name}`, state: {id: this.state.cycle?this.state.cycle.name:''}}} title="Edit Cycle" > Click</Link> to add. + </span> + </div> + </div> + } + <div className="p-field p-grid resource-input-grid"> + <ResourceDisplayList cycleQuota={this.state.cycleQuota} unitMap={this.resourceUnitMap} /> + </div> + </div> + </React.Fragment> + } + </React.Fragment> ); } -} - -export default CycleView; \ No newline at end of file +} \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.js index e8df560a75cc56560d2eb39adc94a404697e259d..64a96b3f0a9b6ba7ff8a99cf52d416e0e49a23a0 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.js @@ -255,10 +255,12 @@ export class ProjectCreate extends Component { let projectQuota = []; for (const resource in this.state.projectQuota) { let resourceType = _.find(this.state.resources, {'name': resource}); - let quota = { project: this.state.project.name, - resource_type: resourceType['url'], - value: this.state.projectQuota[resource] * (this.resourceUnitMap[resourceType.quantity_value]?this.resourceUnitMap[resourceType.quantity_value].conversionFactor:1)}; - projectQuota.push(quota); + if(resourceType){ + let quota = { project: this.state.project.name, + resource_type: resourceType['url'], + value: this.state.projectQuota[resource] * (this.resourceUnitMap[resourceType.quantity_value]?this.resourceUnitMap[resourceType.quantity_value].conversionFactor:1)}; + projectQuota.push(quota); + } } ProjectService.saveProject(this.state.project, this.defaultResourcesEnabled?projectQuota:[]) .then(project => { @@ -306,11 +308,12 @@ export class ProjectCreate extends Component { this.setState({ dialog: { header: '', detail: ''}, project: { + url: '', name: '', description: '', trigger_priority: 1000, priority_rank: null, - project_quota: [] + quota: [] }, projectQuota: projectQuota, validFields: {}, diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/view.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/view.js index 1d92160e99852a538690878a470ef9633117efbc..2ede26034deb218bf3ba36f0049ada765236d2a9 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/view.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/view.js @@ -21,22 +21,18 @@ export class ProjectView extends Component { this.state = { isLoading: true, }; - console.log(this.props); if (this.props.match.params.id) { this.state.projectId = this.props.match.params.id; } else if (this.props.location.state && this.props.location.state.id) { this.state.projectId = this.props.location.state.id; } - console.log(this.state.projectId); this.state.redirect = this.state.projectId?"":'/project' // If no project id is passed, redirect to Project list page this.resourceUnitMap = UnitConverter.resourceUnitMap; // Resource unit conversion factor and constraints } componentDidMount() { const projectId = this.state.projectId; - console.log(projectId); if (projectId) { - console.log(projectId); this.getProjectDetails(projectId); } else { this.setState({redirect: "/not-found"}); @@ -115,7 +111,7 @@ export class ProjectView extends Component { <label className="col-lg-2 col-md-2 col-sm-12">Trigger Priority</label> <span className="col-lg-4 col-md-4 col-sm-12">{this.state.project.trigger_priority}</span> <label className="col-lg-2 col-md-2 col-sm-12">Allows Trigger Submission</label> - <span className="col-lg-4 col-md-4 col-sm-12"><i className={this.state.project.can_trigger?'fa fa-check-square':'fa fa-times'}></i></span> + <span className="col-lg-4 col-md-4 col-sm-12"><i className={this.state.project.can_trigger?'fa fa-check-circle':'fa fa-times-circle'}></i></span> </div> <div className="p-grid"> <label className="col-lg-2 col-md-2 col-sm-12">Project Category</label> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/SchedulingUnitList.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/SchedulingUnitList.js index 9e64bdb13a17ccfad2dbcc20b55990a511a512c6..a3ec244ea830589b7cc5b830d3415aee43c046af 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/SchedulingUnitList.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/SchedulingUnitList.js @@ -1,6 +1,6 @@ import React, { Component } from 'react' import 'primeflex/primeflex.css'; -import moment, { duration } from 'moment'; +import moment from 'moment'; import AppLoader from "./../../layout/components/AppLoader"; import ViewTable from './../../components/ViewTable'; @@ -66,11 +66,6 @@ class SchedulingUnitList extends Component{ }) } - componentDidMount(){ - this.getSchedulingUnitList(); - - } - componentDidMount(){ this.getSchedulingUnitList(); } diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/ViewSchedulingUnit.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/ViewSchedulingUnit.js index d1774c92f2a617f471744ac18cd7e22a25fa59d8..2a33a72b8c5c54a7f283b1363120ab270779226b 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/ViewSchedulingUnit.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/ViewSchedulingUnit.js @@ -65,6 +65,7 @@ class ViewSchedulingUnit extends Component{ task.duration = moment.utc(task.duration*1000).format('HH:mm:ss'); task.relative_start_time = moment.utc(task.relative_start_time*1000).format('HH:mm:ss'); task.relative_stop_time = moment.utc(task.relative_stop_time*1000).format('HH:mm:ss'); + return task; }); this.setState({ scheduleunit : scheduleunit.data, @@ -88,10 +89,10 @@ class ViewSchedulingUnit extends Component{ style={{float:'right'}}> <i className="fa fa-times" style={{marginTop: "10px", marginLeft: '5px'}}></i> </Link> - <Link to={{ pathname: '/schedulingunit/edit', state: {id: this.state.scheduleunit?this.state.scheduleunit.id:''}}} title="Edit" + {/* <Link to={{ pathname: '/schedulingunit/edit', state: {id: this.state.scheduleunit?this.state.scheduleunit.id:''}}} title="Edit" style={{float:'right'}}> <i className="fa fa-edit" style={{marginTop: "10px"}}></i> - </Link> + </Link> */} </div> </div> { this.state.isLoading ? <AppLoader/> :this.state.scheduleunit && diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js index f6fc99eb3ad6877bbff4216e8787d7e7a10882c8..0739e7824a404d7e1a21de18298cd809ae0dd79d 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js @@ -11,7 +11,7 @@ import {Dashboard} from './Dashboard'; import {Scheduling} from './Scheduling'; import {TaskEdit, TaskView} from './Task'; import ViewSchedulingUnit from './Scheduling/ViewSchedulingUnit' -import { CycleCreate, CycleList, CycleView } from './Cycle'; +import { CycleList, CycleCreate, CycleView, CycleEdit } from './Cycle'; export const routes = [ { @@ -66,6 +66,18 @@ export const routes = [ component: ProjectEdit, name: 'Project Edit' },{ + path: "/cycle/edit/:id", + component: CycleEdit, + name: 'Cycle Edit' + },{ + path: "/cycle/view", + component: CycleView, + name: 'Cycle View' + },{ + path: "/cycle/view/:id", + component: CycleView, + name: 'Cycle View' + }, { path: "/cycle/create", component: CycleCreate, name: 'Cycle Add' @@ -75,11 +87,6 @@ export const routes = [ component: CycleList, name: 'Cycle List' }, - { - path: "/cycle/view", - component: CycleView, - name: 'Cycle View' - }, ]; export const RoutedContent = () => { diff --git a/SAS/TMSS/frontend/tmss_webapp/src/services/ScheduleService.js b/SAS/TMSS/frontend/tmss_webapp/src/services/ScheduleService.js deleted file mode 100644 index d2d5285744fd955b4ea115424a322b3be51976cf..0000000000000000000000000000000000000000 --- a/SAS/TMSS/frontend/tmss_webapp/src/services/ScheduleService.js +++ /dev/null @@ -1,52 +0,0 @@ -import axios from 'axios' - -export async function getScheduling_Unit_Draft(){ - let res = []; - await axios.get('/api/scheduling_unit_draft/?ordering=id', { - headers: { - "Content-Type": "application/json", - "Authorization": "Basic dGVzdDp0ZXN0" - } - } - ).then(function(response) { - res= response; - - }).catch(function(error) { - console.log('Error on Authentication',error); - }); - return res; -} - -export async function getScheduling_Unit_Draft_By_Id(id){ - let res = []; - await axios.get('/api/scheduling_unit_draft/'+id, { - headers: { - "Content-Type": "application/json", - "Authorization": "Basic dGVzdDp0ZXN0" - } - } - ).then(function(response) { - res= response; - }).catch(function(error) { - console.log('Error on Authentication',error); - }); - return res; -} - -export async function getTasks_Draft_By_scheduling_Unit_Id(id){ - let res=[]; - await axios.get('/api/scheduling_unit_draft/'+id+'/task_draft/?ordering=id', { - headers: { - "Content-Type": "application/json", - "Authorization": "Basic dGVzdDp0ZXN0" - } - } - ).then(function(response) { - res= response; - }).catch(function(error) { - console.log('Error on Authentication',error); - }); - return res; -} - - \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/services/cycle.service.js b/SAS/TMSS/frontend/tmss_webapp/src/services/cycle.service.js index 1d01b7b6aa8825eb18da72c52592f5778cb68913..f758c543f5a72a8096175dee6484822dc4a2fb48 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/services/cycle.service.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/services/cycle.service.js @@ -13,6 +13,7 @@ const CycleService = { console.error(error); } }, + // Duplicate getCycleById: async function (id) { try { const url = `/api/cycle/${id}/project`; @@ -22,10 +23,10 @@ const CycleService = { console.error(error); } }, - - getCycleQuota: async function () { + getAllCycleQuotas: async function () { let res = []; - await axios.get('/api/cycle_quota/') + // To be changed once the cycle_quota for cycle is available. + await axios.get('/api/cycle_quota/?limit=1000&offset=0') .then(response => { res = response.data.results; }).catch(function (error) { @@ -33,27 +34,16 @@ const CycleService = { }); return res; }, - getResources: async function () { - let res = []; - await axios.get('/api/resource_type') - .then(response => { - res = response; - }).catch(function (error) { - console.error('[cycle.services.resource_type]', error); - }); - return res; - - }, + // Duplicate getCycle: async function(id) { try { - const url = `/api/cycle/${id}`; - const response = await axios.get(url); - return response.data.results; + const response = await axios.get((`/api/cycle/${id}`)); + return response; } catch (error) { console.error(error); } }, - // To be rmoved + // To be removed getAllCycle: async function (){ let res = []; await axios.get('/api/cycle/') @@ -65,6 +55,7 @@ const CycleService = { return res; }, + //Duplicate getResources: async function() { try { const url = `/api/resource_type`; @@ -74,6 +65,14 @@ const CycleService = { console.error('[cycle.services.getResources]',error); } }, + getCycleQuota: async function(id) { + try { + const response = await axios.get((`/api/cycle_quota/${id}`)); + return response.data; + } catch (error) { + console.error(error); + } + }, saveCycle: async function(cycle, cycleQuota) { try { const response = await axios.post(('/api/cycle/'), cycle); @@ -96,7 +95,45 @@ const CycleService = { console.error(error); return null; } - }, + }, + updateCycle: async function(id, cycle) { + try { + const response = await axios.put((`/api/cycle/${id}/`), cycle); + return response.data; + } catch (error) { + console.log(error.response.data); + return error.response.data; + } + }, + deleteCycleQuota: async function(cycleQuota) { + try { + const response = await axios.delete(`/api/cycle_quota/${cycleQuota.id}/`); + return response.status===204?{message: 'deleted'}:null; + } catch (error) { + console.error(error); + return null; + } + }, + updateCycleQuota: async function(cycleQuota) { + try { + const response = await axios.put(`/api/cycle_quota/${cycleQuota.id}/`, cycleQuota); + return response.data; + } catch (error) { + console.error(error); + return null; + } + }, + //Duplicate + getCycleDetails: async function(id) { + try { + const response = await axios.get((`/api/cycle/${id}`)); + let cycle = response.data; + return cycle; + } catch(error) { + console.error(error); + return null; + } + }, } export default CycleService; diff --git a/SAS/TMSS/src/tmss/settings.py b/SAS/TMSS/src/tmss/settings.py index 6ad450c3e6b4f378fe1c3ea88e6a494f58471c8d..1a569e2b6d05d93320c14ba6b79b89f4c6a11ebd 100644 --- a/SAS/TMSS/src/tmss/settings.py +++ b/SAS/TMSS/src/tmss/settings.py @@ -260,16 +260,15 @@ if "OIDC_RP_CLIENT_ID" in os.environ.keys(): # OPEN-ID CONNECT OIDC_DRF_AUTH_BACKEND = 'mozilla_django_oidc.auth.OIDCAuthenticationBackend' - # For talking to Mozilla Identity Provider: OIDC_RP_SCOPES = "openid email profile" # todo: groups are not a standard scope, how to handle those? OIDC_RP_CLIENT_ID = os.environ.get('OIDC_RP_CLIENT_ID', '2') # Secret, do not put real credentials on Git OIDC_RP_CLIENT_SECRET = os.environ.get('OIDC_RP_CLIENT_SECRET', 'secret') # Secret, do not put real credentials on Git - OIDC_ENDPOINT_HOST = os.environ.get('OIDC_ENDPOINT_HOST', 'tmss_test_oidc') - OIDC_OP_AUTHORIZATION_ENDPOINT = "http://%s:8088/openid/authorize" % OIDC_ENDPOINT_HOST - OIDC_OP_TOKEN_ENDPOINT = "http://%s:8088/openid/token" % OIDC_ENDPOINT_HOST - OIDC_OP_USER_ENDPOINT = "http://%s:8088/openid/userinfo" % OIDC_ENDPOINT_HOST + OIDC_ENDPOINT_HOST = os.environ.get('OIDC_ENDPOINT_HOST', 'localhost') + OIDC_OP_AUTHORIZATION_ENDPOINT = os.environ.get('OIDC_OP_AUTHORIZATION_ENDPOINT', "http://localhost:8088/openid/authorize/") + OIDC_OP_TOKEN_ENDPOINT = os.environ.get('OIDC_OP_TOKEN_ENDPOINT', "http://localhost:8088/openid/token/") + OIDC_OP_USER_ENDPOINT = os.environ.get('OIDC_OP_USER_ENDPOINT', "http://localhost:8088/openid/userinfo/") AUTHENTICATION_BACKENDS += ('mozilla_django_oidc.auth.OIDCAuthenticationBackend',) MIDDLEWARE.append('mozilla_django_oidc.middleware.SessionRefresh') diff --git a/SAS/TMSS/src/tmss/tmssapp/migrations/0001_initial.py b/SAS/TMSS/src/tmss/tmssapp/migrations/0001_initial.py index 81e15db2fa25ddd0d68245ed2d78273a47ccf76e..607273e7c9f438c01d81c5a90d077e7e79b3bd95 100644 --- a/SAS/TMSS/src/tmss/tmssapp/migrations/0001_initial.py +++ b/SAS/TMSS/src/tmss/tmssapp/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2.12 on 2020-08-04 12:35 +# Generated by Django 3.0.9 on 2020-08-19 13:24 from django.conf import settings import django.contrib.postgres.fields @@ -145,6 +145,9 @@ class Migration(migrations.Migration): ('version', models.CharField(help_text='Version of this template (with respect to other templates of the same name).', max_length=128)), ('schema', django.contrib.postgres.fields.jsonb.JSONField(help_text='Schema for the configurable parameters needed to use this template.')), ], + options={ + 'abstract': False, + }, ), migrations.CreateModel( name='DataproductHash', @@ -171,6 +174,9 @@ class Migration(migrations.Migration): ('version', models.CharField(help_text='Version of this template (with respect to other templates of the same name).', max_length=128)), ('schema', django.contrib.postgres.fields.jsonb.JSONField(help_text='Schema for the configurable parameters needed to use this template.')), ], + options={ + 'abstract': False, + }, ), migrations.CreateModel( name='DataproductTransform', @@ -309,6 +315,9 @@ class Migration(migrations.Migration): ('schema', django.contrib.postgres.fields.jsonb.JSONField(help_text='Schema for the configurable parameters needed to use this template.')), ('create_function', models.CharField(help_text='Python function to call to execute the generator.', max_length=128)), ], + options={ + 'abstract': False, + }, ), migrations.CreateModel( name='PeriodCategory', @@ -450,6 +459,22 @@ class Migration(migrations.Migration): 'abstract': False, }, ), + migrations.CreateModel( + name='SchedulingUnitObservingStrategyTemplate', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('tags', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=128), blank=True, default=list, help_text='User-defined search keywords for object.', size=8)), + ('created_at', models.DateTimeField(auto_now_add=True, help_text='Moment of object creation.')), + ('updated_at', models.DateTimeField(auto_now=True, help_text='Moment of last object update.')), + ('name', models.CharField(help_text='Human-readable name of this object.', max_length=128)), + ('description', models.CharField(help_text='A longer description of this object.', max_length=255)), + ('version', models.CharField(help_text='Version of this template (with respect to other templates of the same name).', max_length=128)), + ('template', django.contrib.postgres.fields.jsonb.JSONField(help_text='JSON-data compliant with the JSON-schema in the scheduling_unit_template. This observation strategy template like a predefined recipe with all the correct settings, and defines which parameters the user can alter.')), + ], + options={ + 'abstract': False, + }, + ), migrations.CreateModel( name='SchedulingUnitTemplate', fields=[ @@ -462,6 +487,9 @@ class Migration(migrations.Migration): ('version', models.CharField(help_text='Version of this template (with respect to other templates of the same name).', max_length=128)), ('schema', django.contrib.postgres.fields.jsonb.JSONField(help_text='Schema for the configurable parameters needed to use this template.')), ], + options={ + 'abstract': False, + }, ), migrations.CreateModel( name='StationType', @@ -550,6 +578,9 @@ class Migration(migrations.Migration): ('queue', models.BooleanField(default=False)), ('realtime', models.BooleanField(default=False)), ], + options={ + 'abstract': False, + }, ), migrations.CreateModel( name='SubtaskType', @@ -649,28 +680,27 @@ class Migration(migrations.Migration): ('version', models.CharField(help_text='Version of this template (with respect to other templates of the same name).', max_length=128)), ('schema', django.contrib.postgres.fields.jsonb.JSONField(help_text='Schema for the configurable parameters needed to use this template.')), ], + options={ + 'abstract': False, + }, ), migrations.CreateModel( - name='TaskSchedulingRelationBlueprint', + name='TaskType', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('tags', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=128), blank=True, default=list, help_text='User-defined search keywords for object.', size=8)), - ('created_at', models.DateTimeField(auto_now_add=True, help_text='Moment of object creation.')), - ('updated_at', models.DateTimeField(auto_now=True, help_text='Moment of last object update.')), - ('time_offset', models.IntegerField(default=60, help_text='Time offset of start of second task with respect to start of first task.')), + ('value', models.CharField(max_length=128, primary_key=True, serialize=False, unique=True)), ], options={ 'abstract': False, }, ), migrations.CreateModel( - name='TaskSchedulingRelationDraft', + name='Setting', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('tags', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=128), blank=True, default=list, help_text='User-defined search keywords for object.', size=8)), ('created_at', models.DateTimeField(auto_now_add=True, help_text='Moment of object creation.')), ('updated_at', models.DateTimeField(auto_now=True, help_text='Moment of last object update.')), - ('time_offset', models.IntegerField(default=60, help_text='Time offset of start of second task with respect to start of first task.')), + ('name', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, primary_key=True, serialize=False, to='tmssapp.Flag', unique=True)), + ('value', models.BooleanField()), ], options={ 'abstract': False, @@ -688,73 +718,47 @@ class Migration(migrations.Migration): ('version', models.CharField(help_text='Version of this template (with respect to other templates of the same name).', max_length=128)), ('schema', django.contrib.postgres.fields.jsonb.JSONField(help_text='Schema for the configurable parameters needed to use this template.')), ('validation_code_js', models.CharField(help_text='JavaScript code for additional (complex) validation.', max_length=128)), + ('type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='tmssapp.TaskType')), ], + options={ + 'abstract': False, + }, ), migrations.CreateModel( - name='TaskType', + name='TaskSchedulingRelationDraft', fields=[ - ('value', models.CharField(max_length=128, primary_key=True, serialize=False, unique=True)), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('tags', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=128), blank=True, default=list, help_text='User-defined search keywords for object.', size=8)), + ('created_at', models.DateTimeField(auto_now_add=True, help_text='Moment of object creation.')), + ('updated_at', models.DateTimeField(auto_now=True, help_text='Moment of last object update.')), + ('time_offset', models.IntegerField(default=60, help_text='Time offset of start of second task with respect to start of first task.')), + ('first', models.ForeignKey(help_text='First Task Draft to connect.', on_delete=django.db.models.deletion.CASCADE, related_name='first_to_connect', to='tmssapp.TaskDraft')), + ('placement', models.ForeignKey(help_text='Task scheduling relation placement.', on_delete=django.db.models.deletion.PROTECT, to='tmssapp.SchedulingRelationPlacement')), + ('second', models.ForeignKey(help_text='Second Task Draft to connect.', on_delete=django.db.models.deletion.CASCADE, related_name='second_to_connect', to='tmssapp.TaskDraft')), ], options={ 'abstract': False, }, ), - migrations.CreateModel( - name='Setting', + name='TaskSchedulingRelationBlueprint', fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('tags', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=128), blank=True, default=list, help_text='User-defined search keywords for object.', size=8)), ('created_at', models.DateTimeField(auto_now_add=True, help_text='Moment of object creation.')), ('updated_at', models.DateTimeField(auto_now=True, help_text='Moment of last object update.')), - ('name', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, primary_key=True, serialize=False, to='tmssapp.Flag', unique=True)), - ('value', models.BooleanField()), + ('time_offset', models.IntegerField(default=60, help_text='Time offset of start of second task with respect to start of first task.')), + ('first', models.ForeignKey(help_text='First Task Blueprint to connect.', on_delete=django.db.models.deletion.CASCADE, related_name='first_to_connect', to='tmssapp.TaskBlueprint')), + ('placement', models.ForeignKey(default='after', help_text='Task scheduling relation placement.', on_delete=django.db.models.deletion.PROTECT, to='tmssapp.SchedulingRelationPlacement')), + ('second', models.ForeignKey(help_text='Second Task Blueprint to connect.', on_delete=django.db.models.deletion.CASCADE, related_name='second_to_connect', to='tmssapp.TaskBlueprint')), ], options={ 'abstract': False, }, ), - migrations.AddConstraint( - model_name='tasktemplate', - constraint=models.UniqueConstraint(fields=('name', 'version'), name='TaskTemplate_unique_name_version'), - ), - migrations.AddField( - model_name='tasktemplate', - name='type', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='tmssapp.TaskType'), - ), - migrations.AddField( - model_name='taskschedulingrelationdraft', - name='first', - field=models.ForeignKey(help_text='First Task Draft to connect.', on_delete=django.db.models.deletion.CASCADE, related_name='first_to_connect', to='tmssapp.TaskDraft'), - ), - migrations.AddField( - model_name='taskschedulingrelationdraft', - name='placement', - field=models.ForeignKey(help_text='Task scheduling relation placement.', on_delete=django.db.models.deletion.PROTECT, to='tmssapp.SchedulingRelationPlacement'), - ), - migrations.AddField( - model_name='taskschedulingrelationdraft', - name='second', - field=models.ForeignKey(help_text='Second Task Draft to connect.', on_delete=django.db.models.deletion.CASCADE, related_name='second_to_connect', to='tmssapp.TaskDraft'), - ), - migrations.AddField( - model_name='taskschedulingrelationblueprint', - name='first', - field=models.ForeignKey(help_text='First Task Blueprint to connect.', on_delete=django.db.models.deletion.CASCADE, related_name='first_to_connect', to='tmssapp.TaskBlueprint'), - ), - migrations.AddField( - model_name='taskschedulingrelationblueprint', - name='placement', - field=models.ForeignKey(default='after', help_text='Task scheduling relation placement.', on_delete=django.db.models.deletion.PROTECT, to='tmssapp.SchedulingRelationPlacement'), - ), - migrations.AddField( - model_name='taskschedulingrelationblueprint', - name='second', - field=models.ForeignKey(help_text='Second Task Blueprint to connect.', on_delete=django.db.models.deletion.CASCADE, related_name='second_to_connect', to='tmssapp.TaskBlueprint'), - ), migrations.AddConstraint( model_name='taskrelationselectiontemplate', - constraint=models.UniqueConstraint(fields=('name', 'version'), name='TaskRelationSelectionTemplate_unique_name_version'), + constraint=models.UniqueConstraint(fields=('name', 'version'), name='taskrelationselectiontemplate_unique_name_version'), ), migrations.AddField( model_name='taskrelationdraft', @@ -854,7 +858,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='taskconnectortype', name='input_of', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='inpput_connector_types', to='tmssapp.TaskTemplate'), + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='input_connector_types', to='tmssapp.TaskTemplate'), ), migrations.AddField( model_name='taskconnectortype', @@ -968,7 +972,12 @@ class Migration(migrations.Migration): ), migrations.AddConstraint( model_name='schedulingunittemplate', - constraint=models.UniqueConstraint(fields=('name', 'version'), name='SchedulingUnitTemplate_unique_name_version'), + constraint=models.UniqueConstraint(fields=('name', 'version'), name='schedulingunittemplate_unique_name_version'), + ), + migrations.AddField( + model_name='schedulingunitobservingstrategytemplate', + name='scheduling_unit_template', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='tmssapp.SchedulingUnitTemplate'), ), migrations.AddField( model_name='schedulingunitdraft', @@ -980,6 +989,11 @@ class Migration(migrations.Migration): name='copy_reason', field=models.ForeignKey(help_text='Reason why source was copied (NULLable).', null=True, on_delete=django.db.models.deletion.PROTECT, to='tmssapp.CopyReason'), ), + migrations.AddField( + model_name='schedulingunitdraft', + name='observation_strategy_template', + field=models.ForeignKey(help_text='Observation Strategy Template used to create the requirements_doc.', null=True, on_delete=django.db.models.deletion.PROTECT, to='tmssapp.SchedulingUnitObservingStrategyTemplate'), + ), migrations.AddField( model_name='schedulingunitdraft', name='requirements_template', @@ -1047,7 +1061,7 @@ class Migration(migrations.Migration): ), migrations.AddConstraint( model_name='generatortemplate', - constraint=models.UniqueConstraint(fields=('name', 'version'), name='GeneratorTemplate_unique_name_version'), + constraint=models.UniqueConstraint(fields=('name', 'version'), name='generatortemplate_unique_name_version'), ), migrations.AddField( model_name='filesystem', @@ -1096,7 +1110,7 @@ class Migration(migrations.Migration): ), migrations.AddConstraint( model_name='dataproductspecificationstemplate', - constraint=models.UniqueConstraint(fields=('name', 'version'), name='DataproductSpecificationsTemplate_unique_name_version'), + constraint=models.UniqueConstraint(fields=('name', 'version'), name='dataproductspecificationstemplate_unique_name_version'), ), migrations.AddField( model_name='dataproducthash', @@ -1110,7 +1124,7 @@ class Migration(migrations.Migration): ), migrations.AddConstraint( model_name='dataproductfeedbacktemplate', - constraint=models.UniqueConstraint(fields=('name', 'version'), name='DataproductFeedbackTemplate_unique_name_version'), + constraint=models.UniqueConstraint(fields=('name', 'version'), name='dataproductfeedbacktemplate_unique_name_version'), ), migrations.AddField( model_name='dataproductarchiveinfo', @@ -1152,6 +1166,10 @@ class Migration(migrations.Migration): name='station_type', field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='tmssapp.StationType'), ), + migrations.AddConstraint( + model_name='tasktemplate', + constraint=models.UniqueConstraint(fields=('name', 'version'), name='tasktemplate_unique_name_version'), + ), migrations.AddIndex( model_name='taskschedulingrelationdraft', index=django.contrib.postgres.indexes.GinIndex(fields=['tags'], name='tmssapp_tas_tags_d1e21f_gin'), @@ -1174,7 +1192,7 @@ class Migration(migrations.Migration): ), migrations.AddConstraint( model_name='subtasktemplate', - constraint=models.UniqueConstraint(fields=('name', 'version'), name='SubtaskTemplate_unique_name_version'), + constraint=models.UniqueConstraint(fields=('name', 'version'), name='subtasktemplate_unique_name_version'), ), migrations.AddIndex( model_name='subtaskstatelog', diff --git a/SAS/TMSS/src/tmss/tmssapp/models/scheduling.py b/SAS/TMSS/src/tmss/tmssapp/models/scheduling.py index 0868f0e846e9baed309f476e779da82336ddf0b6..4ac8634bab3ccf3a644f423d5fca7330ef387a2f 100644 --- a/SAS/TMSS/src/tmss/tmssapp/models/scheduling.py +++ b/SAS/TMSS/src/tmss/tmssapp/models/scheduling.py @@ -106,8 +106,6 @@ class SubtaskTemplate(Template): queue = BooleanField(default=False) realtime = BooleanField(default=False) - class Meta: - pass class DefaultSubtaskTemplate(BasicCommon): name = CharField(max_length=128, unique=True) @@ -115,8 +113,7 @@ class DefaultSubtaskTemplate(BasicCommon): class DataproductSpecificationsTemplate(Template): - class Meta: - pass + pass class DefaultDataproductSpecificationsTemplate(BasicCommon): @@ -125,8 +122,7 @@ class DefaultDataproductSpecificationsTemplate(BasicCommon): class DataproductFeedbackTemplate(Template): - class Meta: - pass + pass # todo: do we need to specify a default? diff --git a/SAS/TMSS/src/tmss/tmssapp/models/specification.py b/SAS/TMSS/src/tmss/tmssapp/models/specification.py index c73951f9f2275285fca57e0b297b03de9916e0c1..f292c06a9e03d7a0e9a3d9e44626715c30daa714 100644 --- a/SAS/TMSS/src/tmss/tmssapp/models/specification.py +++ b/SAS/TMSS/src/tmss/tmssapp/models/specification.py @@ -232,18 +232,31 @@ class Template(NamedCommon): class GeneratorTemplate(Template): create_function = CharField(max_length=128, help_text='Python function to call to execute the generator.') - class Meta: - pass - class DefaultGeneratorTemplate(BasicCommon): name = CharField(max_length=128, unique=True) template = ForeignKey("GeneratorTemplate", on_delete=PROTECT) +class SchedulingUnitObservingStrategyTemplate(NamedCommon): + ''' + A SchedulingUnitObservingStrategyTemplate is a template in the sense that it serves as a template to fill in json data objects conform its referred scheduling_unit_template. + It is however not derived from the (abstract) Template super-class, because the Template super class is for JSON schemas, not JSON data objects. + ''' + version = CharField(max_length=128, help_text='Version of this template (with respect to other templates of the same name).') + template = JSONField(null=False, help_text='JSON-data compliant with the JSON-schema in the scheduling_unit_template. ' + 'This observation strategy template like a predefined recipe with all the correct settings, and defines which parameters the user can alter.') + scheduling_unit_template = ForeignKey("SchedulingUnitTemplate", on_delete=PROTECT, null=False, help_text="") + + def save(self, force_insert=False, force_update=False, using=None, update_fields=None): + if self.template and self.scheduling_unit_template_id and self.scheduling_unit_template.schema: + validate_json_against_schema(self.template, self.scheduling_unit_template.schema) + + super().save(force_insert, force_update, using, update_fields) + + class SchedulingUnitTemplate(Template): - class Meta: - pass + pass class DefaultSchedulingUnitTemplate(BasicCommon): @@ -255,8 +268,6 @@ class TaskTemplate(Template): validation_code_js = CharField(max_length=128, help_text='JavaScript code for additional (complex) validation.') type = ForeignKey('TaskType', null=False, on_delete=PROTECT) - class Meta: - pass class DefaultTaskTemplate(BasicCommon): name = CharField(max_length=128, unique=True) @@ -264,8 +275,7 @@ class DefaultTaskTemplate(BasicCommon): class TaskRelationSelectionTemplate(Template): - class Meta: - pass + pass class DefaultTaskRelationSelectionTemplate(BasicCommon): name = CharField(max_length=128, unique=True) @@ -369,10 +379,17 @@ class SchedulingUnitDraft(NamedCommon): generator_instance_doc = JSONField(null=True, help_text='Parameter value that generated this run draft (NULLable).') scheduling_set = ForeignKey('SchedulingSet', related_name='scheduling_unit_drafts', on_delete=CASCADE, help_text='Set to which this scheduling unit draft belongs.') requirements_template = ForeignKey('SchedulingUnitTemplate', on_delete=CASCADE, help_text='Schema used for requirements_doc.') + observation_strategy_template = ForeignKey('SchedulingUnitObservingStrategyTemplate', on_delete=PROTECT, null=True, help_text='Observation Strategy Template used to create the requirements_doc.') def save(self, force_insert=False, force_update=False, using=None, update_fields=None): - if self.requirements_doc and self.requirements_template_id and self.requirements_template.schema: - validate_json_against_schema(self.requirements_doc, self.requirements_template.schema) + if self.requirements_doc: + if self.requirements_template_id and self.requirements_template.schema: + # If this scheduling unit was created from an observation_strategy_template, + # then make sure that the observation_strategy_template validates against this unit's requirements_template.schema + if self.observation_strategy_template_id and self.observation_strategy_template.template: + validate_json_against_schema(self.observation_strategy_template.template, self.requirements_template.schema) + + validate_json_against_schema(self.requirements_doc, self.requirements_template.schema) super().save(force_insert, force_update, using, update_fields) diff --git a/SAS/TMSS/src/tmss/tmssapp/populate.py b/SAS/TMSS/src/tmss/tmssapp/populate.py index dbb041b5e1f659fc9129bb7571858a3076b559ea..91152d8f2c3e4526eb439e17448cb2ce60ee4034 100644 --- a/SAS/TMSS/src/tmss/tmssapp/populate.py +++ b/SAS/TMSS/src/tmss/tmssapp/populate.py @@ -47,6 +47,8 @@ def populate_settings(apps, schema_editor): def populate_lofar_json_schemas(apps, schema_editor): _populate_scheduling_unit_schema() + _populate_scheduling_unit_observation_strategry_schema() + # populate task schema's _populate_preprocessing_schema() _populate_observation_with_stations_schema() @@ -81,124 +83,34 @@ def populate_test_data(): for set_nr in range(3): scheduling_set_data = SchedulingSet_test_data(name="Test Scheduling Set UC1 example %s" % (set_nr,), project=tmss_project) scheduling_set = models.SchedulingSet.objects.create(**scheduling_set_data) - scheduling_set.tags = ["TEST"] + scheduling_set.tags = ["TEST", "UC1"] scheduling_set.save() - for unit_nr in range(3): - # construct a scheduling_unit_doc, i.e.: a specification of interrelated tasks which conforms the scheduling unit schema - # by default, this scheduling_unit_doc holds no tasks, so lets setup the UC1 sequence of tasks here, and add it to the scheduling_unit_doc - scheduling_unit_template = models.SchedulingUnitTemplate.objects.get(name="scheduling unit schema") - scheduling_unit_doc = get_default_json_object_for_schema(scheduling_unit_template.schema) - - # create and add a calibrator task spec - # Change autoselect to False (or provide tile_beam pointings for Target Observation) to avoid Exception - json_schema_calibrator = get_default_json_object_for_schema(models.TaskTemplate.objects.get(name="calibrator schema").schema) - json_schema_calibrator['autoselect'] = False - scheduling_unit_doc['tasks'].append({"name": "Calibrator Observation 1", - "description": "Calibrator Observation for UC1 HBA scheduling unit", - "specifications_doc": json_schema_calibrator, - "specifications_template": "calibrator schema"}) - - # create and add a calibrator preprocessing spec - scheduling_unit_doc['tasks'].append({"name": "Pipeline Calibrator1", - "description": "Preprocessing Pipeline for Calibrator Observation 1", - "specifications_doc": get_default_json_object_for_schema(models.TaskTemplate.objects.get(name="preprocessing schema").schema), - "specifications_template": "preprocessing schema"}) - - # create and add a target obs spec - scheduling_unit_doc['tasks'].append({"name": "Target Observation", - "description": "Target Observation for UC1 HBA scheduling unit", - "specifications_doc": get_default_json_object_for_schema(models.TaskTemplate.objects.get(name="observation schema").schema), - "specifications_template": "observation schema"}) - - # create and add a target pipeline spec for sap0 - scheduling_unit_doc['tasks'].append({"name": "Preprocessing Pipeline SAP0", - "description": "Preprocessing Pipeline for Target Observation SAP0", - "specifications_doc": get_default_json_object_for_schema(models.TaskTemplate.objects.get(name="preprocessing schema").schema), - "specifications_template": "preprocessing schema"}) - - # create and add a target pipeline spec for sap1 - scheduling_unit_doc['tasks'].append({"name": "Preprocessing Pipeline SAP1", - "description": "Preprocessing Pipeline for Target Observation SAP1", - "specifications_doc": get_default_json_object_for_schema(models.TaskTemplate.objects.get(name="preprocessing schema").schema), - "specifications_template": "preprocessing schema"}) - - # create and add a calibrator task spec - scheduling_unit_doc['tasks'].append({"name": "Calibrator Observation 2", - "description": "Calibrator Observation for UC1 HBA scheduling unit", - "specifications_doc": json_schema_calibrator, - "specifications_template": "calibrator schema"}) - - # create and add a calibrator preprocessing spec - scheduling_unit_doc['tasks'].append({"name": "Pipeline Calibrator2", - "description": "Preprocessing Pipeline for Calibrator Observation 2", - "specifications_doc": get_default_json_object_for_schema(models.TaskTemplate.objects.get(name="preprocessing schema").schema), - "specifications_template": "preprocessing schema"}) - - # ----- end of tasks - - # setup task_scheduling_relations between Target and Calibrator observations - scheduling_unit_doc['task_scheduling_relations'].append({"first": "Calibrator Observation 1", - "second": "Target Observation", - "placement": "before", - "time_offset": 60 }) - scheduling_unit_doc['task_scheduling_relations'].append({"first": "Calibrator Observation 2", - "second": "Target Observation", - "placement": "after", - "time_offset": 60 }) - - # ----- end of task_scheduling_relations - - #TODO: check various input/output datatypes and roles for each task_relation - scheduling_unit_doc['task_relations'].append({"producer": "Calibrator Observation 1", - "consumer": "Pipeline Calibrator1", - "tags": [], - "input": { "role": "input", "datatype": "visibilities" }, - "output": { "role": "correlator", "datatype": "visibilities" }, - "dataformat": "MeasurementSet", - "selection_doc": {}, - "selection_template": "All" }) - - scheduling_unit_doc['task_relations'].append({"producer": "Calibrator Observation 2", - "consumer": "Pipeline Calibrator2", - "tags": [], - "input": { "role": "input", "datatype": "visibilities" }, - "output": { "role": "correlator", "datatype": "visibilities" }, - "dataformat": "MeasurementSet", - "selection_doc": {}, - "selection_template": "All" }) - - scheduling_unit_doc['task_relations'].append({"producer": "Target Observation", - "consumer": "Preprocessing Pipeline SAP0", - "tags": [], - "input": { "role": "input", "datatype": "visibilities" }, - "output": { "role": "correlator", "datatype": "visibilities" }, - "dataformat": "MeasurementSet", - "selection_doc": {"sap": [0]}, - "selection_template": "SAP" }) - - scheduling_unit_doc['task_relations'].append({"producer": "Target Observation", - "consumer": "Preprocessing Pipeline SAP1", - "tags": [], - "input": { "role": "input", "datatype": "visibilities" }, - "output": { "role": "correlator", "datatype": "visibilities" }, - "dataformat": "MeasurementSet", - "selection_doc": {"sap": [1]}, - "selection_template": "SAP" }) - - # finally... add the scheduling_unit_doc to a new SchedulingUnitDraft instance, and were ready to use it! - scheduling_unit_data = SchedulingUnitDraft_test_data(name="Test Scheduling Unit UC1 example %s.%s" % (set_nr, unit_nr), scheduling_set=scheduling_set, - template=scheduling_unit_template, requirements_doc=scheduling_unit_doc) - scheduling_unit_draft = models.SchedulingUnitDraft.objects.create(**scheduling_unit_data) + logger.info('created test scheduling_set: %s', scheduling_set.name) + + for unit_nr in range(2): + strategy_template = models.SchedulingUnitObservingStrategyTemplate.objects.get(name="UC1 observation strategy template") + + + # the 'template' in the strategy_template is a predefined json-data blob which validates against the given scheduling_unit_template + # a user might 'upload' a partial json-data blob, so add all the known defaults + scheduling_unit_spec = add_defaults_to_json_object_for_schema(strategy_template.template, strategy_template.scheduling_unit_template.schema) + + # add the scheduling_unit_doc to a new SchedulingUnitDraft instance, and were ready to use it! + scheduling_unit_draft = models.SchedulingUnitDraft.objects.create(name="UC1 test scheduling unit %s.%s" % (set_nr+1, unit_nr+1), + scheduling_set=scheduling_set, + requirements_template=strategy_template.scheduling_unit_template, + requirements_doc=scheduling_unit_spec, + observation_strategy_template=strategy_template) + scheduling_unit_draft.tags = ["TEST", "UC1"] + scheduling_unit_draft.save() + + logger.info('created test scheduling_unit_draft: %s', scheduling_unit_draft.name) try: - if set_nr==0 and unit_nr==0: - create_task_blueprints_and_subtasks_and_schedule_subtasks_from_scheduling_unit_draft(scheduling_unit_draft) - else: - create_task_blueprints_and_subtasks_from_scheduling_unit_draft(scheduling_unit_draft) + create_task_blueprints_and_subtasks_from_scheduling_unit_draft(scheduling_unit_draft) except TMSSException as e: - logger.error(e) - + logger.exception(e) except ImportError: pass @@ -266,11 +178,25 @@ def _populate_scheduling_unit_schema(): scheduling_unit_template_data = {"name": "scheduling unit schema", "description": 'Schema for scheduling unit', "version": '0.1', - "tags": ["UC1"], + "tags": [], "schema": json_data} SchedulingUnitTemplate.objects.create(**scheduling_unit_template_data) +def _populate_scheduling_unit_observation_strategry_schema(): + with open(os.path.join(working_dir, "schemas/UC1-scheduling-unit-observation-strategy.json")) as json_file: + json_data = json.loads(json_file.read()) + scheduling_unit_template = models.SchedulingUnitTemplate.objects.get(name="scheduling unit schema") + + template_data = {"name": "UC1 observation strategy template", + "description": 'UC1 observation strategy template', + "scheduling_unit_template": scheduling_unit_template, + "version": '0.1', + "tags": ["UC1"], + "template": json_data} + SchedulingUnitObservingStrategyTemplate.objects.create(**template_data) + + def _populate_observation_with_stations_schema(): with open(os.path.join(working_dir, "schemas/task-observation-with-stations.json")) as json_file: json_data = json.loads(json_file.read()) diff --git a/SAS/TMSS/src/tmss/tmssapp/schemas/CMakeLists.txt b/SAS/TMSS/src/tmss/tmssapp/schemas/CMakeLists.txt index 4fb2a448999fb6ad5988477c7ac5de4c037fd5f9..f192559794af5108cca56446981e32d39eb070da 100644 --- a/SAS/TMSS/src/tmss/tmssapp/schemas/CMakeLists.txt +++ b/SAS/TMSS/src/tmss/tmssapp/schemas/CMakeLists.txt @@ -3,6 +3,7 @@ include(PythonInstall) set(_json_schema_files scheduling-unit.json + UC1-scheduling-unit-observation-strategy.json task-calibrator-addon.json task-observation-with-stations.json task-stations.json diff --git a/SAS/TMSS/src/tmss/tmssapp/schemas/UC1-scheduling-unit-observation-strategy.json b/SAS/TMSS/src/tmss/tmssapp/schemas/UC1-scheduling-unit-observation-strategy.json new file mode 100644 index 0000000000000000000000000000000000000000..760f43b19e2d240272508892b4248cf515187768 --- /dev/null +++ b/SAS/TMSS/src/tmss/tmssapp/schemas/UC1-scheduling-unit-observation-strategy.json @@ -0,0 +1,299 @@ +{ + "tasks": { + "Calibrator Observation 1": { + "description": "Calibrator Observation for UC1 HBA scheduling unit", + "tags": [], + "specifications_doc": { + "duration": 600, + "autoselect": false, + "pointing": { + "direction_type": "J2000", + "angle1": 0, + "angle2": 0, + "angle3": 0 + } + }, + "specifications_template": "calibrator schema" + }, + "Pipeline 1": { + "description": "Preprocessing Pipeline for Calibrator Observation 1", + "tags": [], + "specifications_doc": { + "flag": { + "rfi_strategy": "auto", + "outerchannels": true, + "autocorrelations": true + }, + "demix": { + "sources": {}, + "time_steps": 10, + "ignore_target": false, + "frequency_steps": 64 + }, + "average": { + "time_steps": 1, + "frequency_steps": 4 + }, + "storagemanager": "dysco" + }, + "specifications_template": "preprocessing schema" + }, + "Target Observation": { + "description": "Target Observation for UC1 HBA scheduling unit", + "tags": [], + "specifications_doc": { + "QA": { + "plots": { + "enabled": true, + "autocorrelation": true, + "crosscorrelation": true + }, + "file_conversion": { + "enabled": true, + "nr_of_subbands": -1, + "nr_of_timestamps": 256 + } + }, + "duration": 28800, + "correlator": { + "storage_cluster": "CEP4", + "integration_time": 1, + "channels_per_subband": 64 + }, + "antenna_set": "HBA_DUAL_INNER", + "filter": "HBA_110_190", + "stations": [ + { + "group": "ALL", + "min_stations": 1 + } + ], + "tile_beam": { + "direction_type": "J2000", + "angle1": 42, + "angle2": 42, + "angle3": 42 + }, + "SAPs": [ + { + "name": "target0", + "digital_pointing": { + "direction_type": "J2000", + "angle1": 24, + "angle2": 24, + "angle3": 24 + }, + "subbands": [ + 349, + 372 + ] + }, + { + "name": "target1", + "digital_pointing": { + "direction_type": "J2000", + "angle1": 24, + "angle2": 24, + "angle3": 24 + }, + "subbands": [ + 349, + 372 + ] + } + ] + }, + "specifications_template": "observation schema" + }, + "Pipeline SAP0": { + "description": "Preprocessing Pipeline for Target Observation SAP0", + "tags": [], + "specifications_doc": { + "flag": { + "rfi_strategy": "auto", + "outerchannels": true, + "autocorrelations": true + }, + "demix": { + "sources": {}, + "time_steps": 10, + "ignore_target": false, + "frequency_steps": 64 + }, + "average": { + "time_steps": 1, + "frequency_steps": 4 + }, + "storagemanager": "dysco" + }, + "specifications_template": "preprocessing schema" + }, + "Pipeline SAP1": { + "description": "Preprocessing Pipeline for Target Observation SAP1", + "tags": [], + "specifications_doc": { + "flag": { + "rfi_strategy": "auto", + "outerchannels": true, + "autocorrelations": true + }, + "demix": { + "sources": {}, + "time_steps": 10, + "ignore_target": false, + "frequency_steps": 64 + }, + "average": { + "time_steps": 1, + "frequency_steps": 4 + }, + "storagemanager": "dysco" + }, + "specifications_template": "preprocessing schema" + }, + "Calibrator Observation 2": { + "description": "Calibrator Observation for UC1 HBA scheduling unit", + "tags": [], + "specifications_doc": { + "duration": 600, + "autoselect": false, + "pointing": { + "direction_type": "J2000", + "angle1": 0, + "angle2": 0, + "angle3": 0 + } + }, + "specifications_template": "calibrator schema" + }, + "Pipeline 2": { + "description": "Preprocessing Pipeline for Calibrator Observation 2", + "tags": [], + "specifications_doc": { + "flag": { + "rfi_strategy": "auto", + "outerchannels": true, + "autocorrelations": true + }, + "demix": { + "sources": {}, + "time_steps": 10, + "ignore_target": false, + "frequency_steps": 64 + }, + "average": { + "time_steps": 1, + "frequency_steps": 4 + }, + "storagemanager": "dysco" + }, + "specifications_template": "preprocessing schema" + } + }, + "task_relations": [ + { + "producer": "Calibrator Observation 1", + "consumer": "Pipeline 1", + "tags": [], + "input": { + "role": "input", + "datatype": "visibilities" + }, + "output": { + "role": "correlator", + "datatype": "visibilities" + }, + "dataformat": "MeasurementSet", + "selection_doc": {}, + "selection_template": "All" + }, + { + "producer": "Calibrator Observation 2", + "consumer": "Pipeline 2", + "tags": [], + "input": { + "role": "input", + "datatype": "visibilities" + }, + "output": { + "role": "correlator", + "datatype": "visibilities" + }, + "dataformat": "MeasurementSet", + "selection_doc": {}, + "selection_template": "All" + }, + { + "producer": "Target Observation", + "consumer": "Pipeline SAP0", + "tags": [], + "input": { + "role": "input", + "datatype": "visibilities" + }, + "output": { + "role": "correlator", + "datatype": "visibilities" + }, + "dataformat": "MeasurementSet", + "selection_doc": { + "sap": [ + 0 + ] + }, + "selection_template": "SAP" + }, + { + "producer": "Target Observation", + "consumer": "Pipeline SAP1", + "tags": [], + "input": { + "role": "input", + "datatype": "visibilities" + }, + "output": { + "role": "correlator", + "datatype": "visibilities" + }, + "dataformat": "MeasurementSet", + "selection_doc": { + "sap": [ + 1 + ] + }, + "selection_template": "SAP" + } + ], + "task_scheduling_relations": [ + { + "first": "Calibrator Observation 1", + "second": "Target Observation", + "placement": "before", + "time_offset": 60 + }, + { + "first": "Calibrator Observation 2", + "second": "Target Observation", + "placement": "after", + "time_offset": 60 + } + ], + "parameters": [ + { + "refs": [ + "#/tasks/Target Observation/specifications_doc/SAPs/0/digital_pointing" + ], + "name": "Target Pointing 0" + },{ + "refs": [ + "#/tasks/Target Observation/specifications_doc/SAPs/1/digital_pointing" + ], + "name": "Target Pointing 1" + },{ + "refs": [ + "#/tasks/Target Observation/specifications_doc/tile_beam" + ], + "name": "Tile Beam" + } + ] +} \ No newline at end of file diff --git a/SAS/TMSS/src/tmss/tmssapp/schemas/scheduling-unit.json b/SAS/TMSS/src/tmss/tmssapp/schemas/scheduling-unit.json index ba879a079db4ee21158f0aa6363bc14e41ea5f29..d792ba7893922198058d75ff403561fe684e4a5c 100644 --- a/SAS/TMSS/src/tmss/tmssapp/schemas/scheduling-unit.json +++ b/SAS/TMSS/src/tmss/tmssapp/schemas/scheduling-unit.json @@ -7,6 +7,7 @@ "task_connector": { "type": "object", "additionalProperties": false, + "default": {}, "properties": { "role": { "type": "string", @@ -26,21 +27,15 @@ "properties": { "tasks": { "title": "Tasks", - "type": "array", - "additionalItems": false, + "type": "object", "uniqueItems": true, - "default": [], - "items": { + "default": {}, + "additionalProperties": { "type": "object", "title": "Task", "additionalProperties": false, "default": {}, "properties": { - "name": { - "type": "string", - "title": "Name (unique)", - "default": "Default Task" - }, "description": { "type": "string", "title": "Description", @@ -64,12 +59,11 @@ }, "specifications_template": { "type": "string", - "title": "Name of Template for Specifications", + "title": "URI of Template for Specifications", "default": "" } }, "required": [ - "name", "specifications_doc", "specifications_template" ] @@ -78,9 +72,9 @@ "task_relations": { "title": "Task Relations", "type": "array", + "default": [], "additionalItems": false, "uniqueItems": true, - "default": [], "items": { "type": "object", "title": "Task Relation", @@ -126,7 +120,7 @@ }, "selection_template": { "type": "string", - "title": "Name of Template for Selection" + "title": "URI of Template for Selection" } }, "required": [ @@ -141,9 +135,9 @@ "task_scheduling_relations": { "title": "Task Scheduling Relations", "type": "array", + "default": [], "additionalItems": false, "uniqueItems": true, - "default": [], "items": { "type": "object", "title": "Task Scheduling Relation", @@ -181,7 +175,47 @@ "placement" ] } - } - }, + }, + "parameters": { + "title": "Parameters", + "description": "Schema for instance-specific parameters", + "type": "array", + "additionalItems": false, + "uniqueItems": true, + "items": { + "type": "object", + "title": "Parameter", + "additionalProperties": false, + "properties": { + "refs": { + "title": "References", + "description": "JSON Pointers to locations within this schema that will hold this value", + "type": "array", + "additionalItems": false, + "uniqueItems": true, + "minItems": 1, + "items": { + "type": "string", + "title": "Reference", + "default": "#", + "description": "JSON Pointer to parameter location within this schema" + } + }, + "name": { + "type": "string", + "title": "Name", + "description": "Name override" + }, + "description": { + "type": "string", + "title": "Description", + "description": "Description override" + } + }, + "required": [ + "refs" + ] + }, "required": [] -} \ No newline at end of file + }} +} diff --git a/SAS/TMSS/src/tmss/tmssapp/serializers/specification.py b/SAS/TMSS/src/tmss/tmssapp/serializers/specification.py index 0e173fff9865a917e28757e03e1bd8cb0ecd6e52..0a3584ed2c7a82e0415ce201a60c4d2e58151fe0 100644 --- a/SAS/TMSS/src/tmss/tmssapp/serializers/specification.py +++ b/SAS/TMSS/src/tmss/tmssapp/serializers/specification.py @@ -100,6 +100,12 @@ class DefaultGeneratorTemplateSerializer(RelationalHyperlinkedModelSerializer): fields = '__all__' +class SchedulingUnitObservingStrategyTemplateSerializer(RelationalHyperlinkedModelSerializer): + class Meta: + model = models.SchedulingUnitObservingStrategyTemplate + fields = '__all__' + + class SchedulingUnitTemplateSerializer(RelationalHyperlinkedModelSerializer): class Meta: model = models.SchedulingUnitTemplate diff --git a/SAS/TMSS/src/tmss/tmssapp/subtasks.py b/SAS/TMSS/src/tmss/tmssapp/subtasks.py index 110a3c609b5e3bfc99f8f9439b5b04c793ee4e1c..9ea2b60535959f328fb44aafbbea754f4ed8302b 100644 --- a/SAS/TMSS/src/tmss/tmssapp/subtasks.py +++ b/SAS/TMSS/src/tmss/tmssapp/subtasks.py @@ -32,6 +32,17 @@ def create_subtasks_from_task_blueprint(task_blueprint: TaskBlueprint) -> [Subta '''Generic create-method for subtasks. Calls the appropriate create method based on the task_blueprint specifications_template name.''' check_prerequities_for_subtask_creation(task_blueprint) + subtasks = [] + + # recurse over predecessors, so that all dependencies in predecessor subtasks can be met. + for predecessor in task_blueprint.predecessors.all(): + subtasks.extend(create_subtasks_from_task_blueprint(predecessor)) + + if task_blueprint.subtasks.count() > 0: + logger.debug("skipping creation of subtasks because they already exist for task_blueprint id=%s, name='%s', task_template_name='%s'", + task_blueprint.id, task_blueprint.name, task_blueprint.specifications_template.name) + return subtasks + # fixed mapping from template name to generator functions which create the list of subtask(s) for this task_blueprint generators_mapping = {'observation schema': [create_observation_control_subtask_from_task_blueprint, create_qafile_subtask_from_task_blueprint, @@ -42,7 +53,6 @@ def create_subtasks_from_task_blueprint(task_blueprint: TaskBlueprint) -> [Subta template_name = task_blueprint.specifications_template.name if template_name in generators_mapping: generators = generators_mapping[template_name] - subtasks = [] for generator in generators: try: subtask = generator(task_blueprint) @@ -700,15 +710,16 @@ def schedule_observation_subtask(observation_subtask: Subtask): directory = "/data/%s/%s/L%s/uv" % ("projects" if isProductionEnvironment() else "test-projects", observation_subtask.task_blueprint.scheduling_unit_blueprint.draft.scheduling_set.project.name, observation_subtask.id) - for sb_nr in specifications_doc['stations']['digital_pointings'][0]['subbands']: - Dataproduct.objects.create(filename="L%d_SB%03d_uv.MS" % (observation_subtask.id, sb_nr), - directory=directory, - dataformat=Dataformat.objects.get(value="MeasurementSet"), - producer=subtask_output, - specifications_doc={"sap": [0]}, # todo: set correct value. This will be provided by the RA somehow - specifications_template=dataproduct_specifications_template, - feedback_doc="", - feedback_template=dataproduct_feedback_template) + for sap_nr, pointing in enumerate(specifications_doc['stations']['digital_pointings']): + for sb_nr in pointing['subbands']: + Dataproduct.objects.create(filename="L%d_SAP%03d_SB%03d_uv.MS" % (observation_subtask.id, sap_nr, sb_nr), + directory=directory, + dataformat=Dataformat.objects.get(value="MeasurementSet"), + producer=subtask_output, + specifications_doc={"sap": [sap_nr]}, # todo: set correct value. This will be provided by the RA somehow + specifications_template=dataproduct_specifications_template, + feedback_doc="", + feedback_template=dataproduct_feedback_template) # step 4: resource assigner (if possible) _assign_resources(observation_subtask) @@ -812,10 +823,6 @@ def schedule_independent_subtasks_in_task_blueprint(task_blueprint: TaskBlueprin '''Convenience method: Schedule the subtasks in the task_blueprint that are not dependend on predecessors''' subtasks = list(task_blueprint.subtasks.all()) - # sort them in 'data-flow'-order, - # because successors can depend on predecessors, so the first tbp's need to be subtask'd first. - subtasks.sort(key=cmp_to_key(lambda st_a, st_b: -1 if st_a in st_b.predecessors else 1 if st_b in st_a.predecessors else 0)) - for subtask in subtasks: if len(subtask.predecessors.all()) == len(subtask.predecessors.filter(state__value='finished').all()): schedule_subtask(subtask) diff --git a/SAS/TMSS/src/tmss/tmssapp/tasks.py b/SAS/TMSS/src/tmss/tmssapp/tasks.py index 67b821e6d26b061032862045684e3d216b3c623e..0bc760ad2318aab6228232365d78d75f2ef3f9d3 100644 --- a/SAS/TMSS/src/tmss/tmssapp/tasks.py +++ b/SAS/TMSS/src/tmss/tmssapp/tasks.py @@ -6,6 +6,7 @@ from lofar.sas.tmss.tmss.tmssapp.models.specification import TaskBlueprint, Sche from lofar.sas.tmss.tmss.tmssapp.subtasks import create_and_schedule_subtasks_from_task_blueprint, \ create_subtasks_from_task_blueprint, schedule_independent_subtasks_in_task_blueprint from functools import cmp_to_key +from lofar.common.json_utils import add_defaults_to_json_object_for_schema import logging logger = logging.getLogger(__name__) @@ -37,29 +38,32 @@ def create_task_drafts_from_scheduling_unit_draft(scheduling_unit_draft: models. """ logger.debug("create_task_drafts_from_scheduling_unit_draft(scheduling_unit_draft.id=%s, name='%s') ...", scheduling_unit_draft.pk, scheduling_unit_draft.name) - if len(scheduling_unit_draft.requirements_doc.get("tasks",[])) == 0: + if len(scheduling_unit_draft.requirements_doc.get("tasks", {})) == 0: raise BlueprintCreationException("create_task_drafts_from_scheduling_unit_draft: scheduling_unit_draft.id=%s has no tasks defined in its requirements_doc" % (scheduling_unit_draft.pk,)) - for task_definition in scheduling_unit_draft.requirements_doc["tasks"]: + for task_name, task_definition in scheduling_unit_draft.requirements_doc["tasks"].items(): task_template_name = task_definition["specifications_template"] task_template = models.TaskTemplate.objects.get(name=task_template_name) - if scheduling_unit_draft.task_drafts.filter(name=task_definition["name"], specifications_template=task_template).count() > 0: - logger.debug("skipping creation of task draft because it is already in the scheduling_unit... task_name='%s', task_template_name='%s'", task_definition["name"], task_template_name) + task_specifications_doc = task_definition["specifications_doc"] + task_specifications_doc = add_defaults_to_json_object_for_schema(task_specifications_doc, task_template.schema) + + if scheduling_unit_draft.task_drafts.filter(name=task_name, specifications_template=task_template).count() > 0: + logger.debug("skipping creation of task draft because it is already in the scheduling_unit... task_name='%s', task_template_name='%s'", task_name, task_template_name) continue - logger.debug("creating task draft... task_name='%s', task_template_name='%s'", task_definition["name"], task_template_name) + logger.debug("creating task draft... task_name='%s', task_template_name='%s'", task_template_name, task_template_name) - task_draft = models.TaskDraft.objects.create(name=task_definition["name"], + task_draft = models.TaskDraft.objects.create(name=task_name, description=task_definition.get("description",""), tags=task_definition.get("tags",[]), - specifications_doc=task_definition["specifications_doc"], + specifications_doc=task_specifications_doc, copy_reason=models.CopyReason.objects.get(value='template'), copies=None, scheduling_unit_draft=scheduling_unit_draft, specifications_template=task_template) - logger.info("created task draft id=%s task_name='%s', task_template_name='%s'", task_draft.pk, task_definition["name"], task_template_name) + logger.info("created task draft id=%s task_name='%s', task_template_name='%s'", task_draft.pk, task_name, task_template_name) # Now create task relations for task_relation_definition in scheduling_unit_draft.requirements_doc["task_relations"]: @@ -77,7 +81,7 @@ def create_task_drafts_from_scheduling_unit_draft(scheduling_unit_draft: models. output_role=output_role, selection_template=selection_template, selection_doc=task_relation_definition["selection_doc"]).count() > 0: - logger.debug("skipping creation of task_relation between task draft '%s' and '%s' because it is already in the scheduling_unit...", task_relation_definition["producer"], task_relation_definition["consumer"]) + logger.info("skipping creation of task_relation between task draft '%s' and '%s' because it is already in the scheduling_unit...", task_relation_definition["producer"], task_relation_definition["consumer"]) continue task_relation = models.TaskRelationDraft.objects.create(tags=task_relation_definition.get("tags",[]), @@ -103,7 +107,7 @@ def create_task_drafts_from_scheduling_unit_draft(scheduling_unit_draft: models. time_offset=time_offset, first=first_task_draft, second=second_task_draft).count() > 0: - logger.debug("skipping creation of task_scheduling_relation between task draft '%s' and '%s' because it is already in the scheduling_unit...", + logger.info("skipping creation of task_scheduling_relation between task draft '%s' and '%s' because it is already in the scheduling_unit...", task_scheduling_relation_definition["first"], task_scheduling_relation_definition["second"]) continue @@ -251,13 +255,7 @@ def create_task_blueprints_and_subtasks_from_scheduling_unit_blueprint(schedulin '''Convenience method: Create the scheduling_unit_blueprint's task_blueprint(s), then create each task_blueprint's subtasks''' scheduling_unit_blueprint = create_task_blueprints_from_scheduling_unit_blueprint(scheduling_unit_blueprint) - task_blueprints = list(scheduling_unit_blueprint.task_blueprints.all()) - - # sort task_blueprint(s) in 'data-flow'-order, - # because successors can depend on predecessors, so the first tbp's need to be subtask'd first. - task_blueprints.sort(key=cmp_to_key(lambda tbp_a, tbp_b: -1 if tbp_a in tbp_b.predecessors else 1 if tbp_b in tbp_a.predecessors else 0)) - - for task_blueprint in task_blueprints: + for task_blueprint in scheduling_unit_blueprint.task_blueprints.all(): create_subtasks_from_task_blueprint(task_blueprint) # refresh so all related fields are updated. diff --git a/SAS/TMSS/src/tmss/tmssapp/viewsets/specification.py b/SAS/TMSS/src/tmss/tmssapp/viewsets/specification.py index 4404f40d6265ef41461d8c6db5f2ee114c0e2f03..f7b2aeeafdd2d57ac65b7da5aa5d1df3e9b3fc2b 100644 --- a/SAS/TMSS/src/tmss/tmssapp/viewsets/specification.py +++ b/SAS/TMSS/src/tmss/tmssapp/viewsets/specification.py @@ -15,6 +15,7 @@ from rest_framework.permissions import IsAuthenticatedOrReadOnly, DjangoModelPer from rest_framework.decorators import action from drf_yasg.utils import swagger_auto_schema +from drf_yasg.openapi import Parameter from lofar.sas.tmss.tmss.tmssapp.viewsets.lofar_viewset import LOFARViewSet, LOFARNestedViewSet from lofar.sas.tmss.tmss.tmssapp import models @@ -50,6 +51,45 @@ class DefaultGeneratorTemplateViewSet(LOFARViewSet): queryset = models.DefaultGeneratorTemplate.objects.all() serializer_class = serializers.DefaultGeneratorTemplateSerializer + +class SchedulingUnitObservingStrategyTemplateViewSet(LOFARViewSet): + queryset = models.SchedulingUnitObservingStrategyTemplate.objects.all() + serializer_class = serializers.SchedulingUnitObservingStrategyTemplateSerializer + + @swagger_auto_schema(responses={status.HTTP_201_CREATED: 'The newly created scheduling unit', + status.HTTP_403_FORBIDDEN: 'forbidden'}, + operation_description="Create a new SchedulingUnit based on this SchedulingUnitObservingStrategyTemplate, with the given <name> and <description> and make it a child of the given <scheduling_set_id>", + manual_parameters=[Parameter(name='scheduling_set_id', required=True, type='integer', in_='query', + description="the id of the scheduling_set which will be the parent of the newly created scheduling_unit"), + Parameter(name='name', required=False, type='string', in_='query', + description="The name for the newly created scheduling_unit"), + Parameter(name='description', required=False, type='string', in_='query', + description="The description for the newly created scheduling_unit")]) + @action(methods=['get'], detail=True) + def create_scheduling_unit(self, request, pk=None): + strategy_template = get_object_or_404(models.SchedulingUnitObservingStrategyTemplate, pk=pk) + spec = add_defaults_to_json_object_for_schema(strategy_template.template, + strategy_template.scheduling_unit_template.schema) + + scheduling_set = get_object_or_404(models.SchedulingSet, pk=request.query_params['scheduling_set_id']) + + scheduling_unit_draft = models.SchedulingUnitDraft.objects.create(name=request.query_params.get('name', "scheduling unit"), + description=request.query_params.get('description', ""), + requirements_doc=spec, + scheduling_set=scheduling_set, + requirements_template=strategy_template.scheduling_unit_template, + observation_strategy_template=strategy_template) + + scheduling_unit_observation_strategy_template_path = request._request.path + base_path = scheduling_unit_observation_strategy_template_path[:scheduling_unit_observation_strategy_template_path.find('/scheduling_unit_observing_strategy_template')] + scheduling_unit_draft_path = '%s/scheduling_unit_draft/%s/' % (base_path, scheduling_unit_draft.id,) + + # return a response with the new serialized SchedulingUnitDraft, and a Location to the new instance in the header + return Response(serializers.SchedulingUnitDraftSerializer(scheduling_unit_draft, context={'request':request}).data, + status=status.HTTP_201_CREATED, + headers={'Location': scheduling_unit_draft_path}) + + class SchedulingUnitTemplateFilter(filters.FilterSet): class Meta: model = models.SchedulingUnitTemplate diff --git a/SAS/TMSS/src/tmss/urls.py b/SAS/TMSS/src/tmss/urls.py index 5d831d9cf5d96fef8b1733a29786192f3ce82c42..53146045e08986f1cb8930e993b04129df909610 100644 --- a/SAS/TMSS/src/tmss/urls.py +++ b/SAS/TMSS/src/tmss/urls.py @@ -101,6 +101,7 @@ router.register(r'task_type', viewsets.TaskTypeViewSet) # templates router.register(r'generator_template', viewsets.GeneratorTemplateViewSet) +router.register(r'scheduling_unit_observing_strategy_template', viewsets.SchedulingUnitObservingStrategyTemplateViewSet) router.register(r'scheduling_unit_template', viewsets.SchedulingUnitTemplateViewSet) router.register(r'task_template', viewsets.TaskTemplateViewSet) router.register(r'task_relation_selection_template', viewsets.TaskRelationSelectionTemplateViewSet) diff --git a/SAS/TMSS/test/t_scheduling.py b/SAS/TMSS/test/t_scheduling.py index f4de89666fd4bb05b82009ccc46d11fd578cc769..1eee84c252de5e3a2a1a10cbabf19b56c4501d93 100755 --- a/SAS/TMSS/test/t_scheduling.py +++ b/SAS/TMSS/test/t_scheduling.py @@ -91,7 +91,8 @@ class SchedulingTest(unittest.TestCase): subtask_data = test_data_creator.Subtask(specifications_template_url=subtask_template['url'], specifications_doc=spec, - cluster_url=cluster_url) + cluster_url=cluster_url, + task_blueprint_url=test_data_creator.post_data_and_get_url(test_data_creator.TaskBlueprint(), '/task_blueprint/')) subtask = test_data_creator.post_data_and_get_response_as_json_object(subtask_data, '/subtask/') subtask_id = subtask['id'] test_data_creator.post_data_and_get_url(test_data_creator.SubtaskOutput(subtask_url=subtask['url']), '/subtask_output/') @@ -128,7 +129,8 @@ class SchedulingTest(unittest.TestCase): subtask_data = test_data_creator.Subtask(specifications_template_url=subtask_template['url'], specifications_doc=spec, - cluster_url=cluster_url) + cluster_url=cluster_url, + task_blueprint_url=test_data_creator.post_data_and_get_url(test_data_creator.TaskBlueprint(), '/task_blueprint/')) subtask = test_data_creator.post_data_and_get_response_as_json_object(subtask_data, '/subtask/') subtask_id = subtask['id'] test_data_creator.post_data_and_get_url(test_data_creator.SubtaskOutput(subtask_url=subtask['url']), '/subtask_output/') @@ -153,7 +155,8 @@ class SchedulingTest(unittest.TestCase): obs_subtask_data = test_data_creator.Subtask(specifications_template_url=obs_subtask_template['url'], specifications_doc=obs_spec, - cluster_url=cluster_url) + cluster_url=cluster_url, + task_blueprint_url=test_data_creator.post_data_and_get_url(test_data_creator.TaskBlueprint(), '/task_blueprint/')) obs_subtask = test_data_creator.post_data_and_get_response_as_json_object(obs_subtask_data, '/subtask/') obs_subtask_output_url = test_data_creator.post_data_and_get_url(test_data_creator.SubtaskOutput(subtask_url=obs_subtask['url']), '/subtask_output/') test_data_creator.post_data_and_get_url(test_data_creator.Dataproduct(filename="L%s_SB000.MS"%obs_subtask['id'], @@ -194,14 +197,12 @@ class SchedulingTest(unittest.TestCase): obs_task['QA']['plots']['enabled'] = False obs_task['QA']['file_conversion']['enabled'] = False obs_task['SAPs'][0]['subbands'] = [0,1] - scheduling_unit_doc['tasks'].append({"name": "Observation", - "specifications_doc": obs_task, - "specifications_template": "observation schema"}) + scheduling_unit_doc['tasks']["Observation"] = {"specifications_doc": obs_task, + "specifications_template": "observation schema"} # define a pipeline - scheduling_unit_doc['tasks'].append({"name": "Pipeline", - "specifications_doc": get_default_json_object_for_schema(client.get_task_template(name="preprocessing schema")['schema']), - "specifications_template": "preprocessing schema"}) + scheduling_unit_doc['tasks']["Pipeline"] = { "specifications_doc": get_default_json_object_for_schema(client.get_task_template(name="preprocessing schema")['schema']), + "specifications_template": "preprocessing schema"} # connect obs to pipeline scheduling_unit_doc['task_relations'].append({"producer": "Observation", diff --git a/SAS/TMSS/test/t_tasks.py b/SAS/TMSS/test/t_tasks.py index d9f6c1b2a79eb78f03173fa38006b2c197bfde26..cc51eec0313d0ec53004e36e802bfbc8cb07495c 100755 --- a/SAS/TMSS/test/t_tasks.py +++ b/SAS/TMSS/test/t_tasks.py @@ -65,24 +65,24 @@ class CreationFromSchedulingUnitDraft(unittest.TestCase): 6. create_task_drafts_from_scheduling_unit_draft(scheduling_unit_draft: models.SchedulingUnitDraft) -> [TaskDraft]: 3. create_task_blueprints_and_subtasks_from_scheduling_unit_draft(scheduling_unit_draft: models.SchedulingUnitDraft) -> models.SchedulingUnitBlueprint: """ - @staticmethod - def create_scheduling_unit_draft_object(scheduling_unit_draft_name, requirements_doc=None): - """ - Helper function to create a scheduling unit object for testing - """ - scheduling_unit_draft_data = SchedulingUnitDraft_test_data(name=scheduling_unit_draft_name, - requirements_doc=requirements_doc, - template=models.SchedulingUnitTemplate.objects.get(name="scheduling unit schema")) - draft_obj = models.SchedulingUnitDraft.objects.create(**scheduling_unit_draft_data) - return draft_obj - def test_create_scheduling_unit_blueprint_from_scheduling_unit_draft(self): """ Create Scheduling Unit Draft Check if the name draft (specified) is equal to name blueprint (created) Check with REST-call if NO tasks are created """ - scheduling_unit_draft = self.create_scheduling_unit_draft_object("Test Scheduling Unit 1") + strategy_template = models.SchedulingUnitObservingStrategyTemplate.objects.get(name="UC1 observation strategy template") + strategy_template.template['tasks'] = {} + + scheduling_unit_draft = models.SchedulingUnitDraft.objects.create( + name="Test Scheduling Unit UC1", + requirements_doc=strategy_template.template, + requirements_template=strategy_template.scheduling_unit_template, + observation_strategy_template=strategy_template, + copy_reason=models.CopyReason.objects.get(value='template'), + generator_instance_doc="para", + copies=None, + scheduling_set=models.SchedulingSet.objects.create(**SchedulingSet_test_data())) scheduling_unit_blueprint = create_scheduling_unit_blueprint_from_scheduling_unit_draft(scheduling_unit_draft) self.assertEqual(scheduling_unit_draft.name, scheduling_unit_blueprint.draft.name) @@ -94,7 +94,19 @@ class CreationFromSchedulingUnitDraft(unittest.TestCase): Check if NO tasks are created Check with REST-call if NO tasks are created """ - scheduling_unit_draft = self.create_scheduling_unit_draft_object("Test Scheduling Unit 2", requirements_doc={'tasks': []}) + strategy_template = models.SchedulingUnitObservingStrategyTemplate.objects.get(name="UC1 observation strategy template") + strategy_template.template['tasks'] = {} + + scheduling_unit_draft = models.SchedulingUnitDraft.objects.create( + name="Test Scheduling Unit UC1", + requirements_doc=strategy_template.template, + requirements_template=strategy_template.scheduling_unit_template, + observation_strategy_template=strategy_template, + copy_reason=models.CopyReason.objects.get(value='template'), + generator_instance_doc="para", + copies=None, + scheduling_set=models.SchedulingSet.objects.create(**SchedulingSet_test_data())) + with self.assertRaises(BlueprintCreationException): create_task_drafts_from_scheduling_unit_draft(scheduling_unit_draft) @@ -109,14 +121,13 @@ class CreationFromSchedulingUnitDraft(unittest.TestCase): Create Task Blueprints (only) Check if tasks (7) are created """ - working_dir = os.path.dirname(os.path.abspath(__file__)) - with open(os.path.join(working_dir, "testdata/example_UC1_scheduling_unit.json")) as json_file: - json_requirements_doc = json.loads(json_file.read()) + strategy_template = models.SchedulingUnitObservingStrategyTemplate.objects.get(name="UC1 observation strategy template") scheduling_unit_draft = models.SchedulingUnitDraft.objects.create( name="Test Scheduling Unit UC1", - requirements_doc=json_requirements_doc, - requirements_template=models.SchedulingUnitTemplate.objects.get(name="scheduling unit schema"), + requirements_doc=strategy_template.template, + requirements_template=strategy_template.scheduling_unit_template, + observation_strategy_template=strategy_template, copy_reason=models.CopyReason.objects.get(value='template'), generator_instance_doc="para", copies=None, @@ -141,14 +152,13 @@ class CreationFromSchedulingUnitDraft(unittest.TestCase): Every Pipeline Task: 1 subtasks (1 control) makes 3x3 + 4x1 = 13 """ - working_dir = os.path.dirname(os.path.abspath(__file__)) - with open(os.path.join(working_dir, "testdata/example_UC1_scheduling_unit.json")) as json_file: - json_requirements_doc = json.loads(json_file.read()) + strategy_template = models.SchedulingUnitObservingStrategyTemplate.objects.get(name="UC1 observation strategy template") scheduling_unit_draft = models.SchedulingUnitDraft.objects.create( name="Test Scheduling Unit UC1", - requirements_doc=json_requirements_doc, - requirements_template=models.SchedulingUnitTemplate.objects.get(name="scheduling unit schema"), + requirements_doc=strategy_template.template, + requirements_template=strategy_template.scheduling_unit_template, + observation_strategy_template=strategy_template, copy_reason=models.CopyReason.objects.get(value='template'), generator_instance_doc="para", copies=None, @@ -177,7 +187,18 @@ class CreationFromSchedulingUnitDraft(unittest.TestCase): Check if the name draft (specified) is equal to name blueprint (created) Check with REST-call if NO tasks are created """ - scheduling_unit_draft = self.create_scheduling_unit_draft_object("Test Scheduling Unit 3", {'tasks': []}) + strategy_template = models.SchedulingUnitObservingStrategyTemplate.objects.get(name="UC1 observation strategy template") + strategy_template.template['tasks'] = {} + + scheduling_unit_draft = models.SchedulingUnitDraft.objects.create( + name="Test Scheduling Unit UC1", + requirements_doc=strategy_template.template, + requirements_template=strategy_template.scheduling_unit_template, + observation_strategy_template=strategy_template, + copy_reason=models.CopyReason.objects.get(value='template'), + generator_instance_doc="para", + copies=None, + scheduling_set=models.SchedulingSet.objects.create(**SchedulingSet_test_data())) with self.assertRaises(BlueprintCreationException): create_task_blueprints_and_subtasks_from_scheduling_unit_draft(scheduling_unit_draft) diff --git a/SAS/TMSS/test/t_tmssapp_specification_REST_API.py b/SAS/TMSS/test/t_tmssapp_specification_REST_API.py index 597ce1c04c433979c5e7b0ff0ada73720fb26686..6d922605dbb7a553227bd142e508d731bf620b47 100755 --- a/SAS/TMSS/test/t_tmssapp_specification_REST_API.py +++ b/SAS/TMSS/test/t_tmssapp_specification_REST_API.py @@ -1140,7 +1140,7 @@ class SchedulingUnitDraftTestCase(unittest.TestCase): GET_OK_and_assert_equal_expected_response(self, url, schedulingunitdraft_test_data) test_patch = {"description": "This is a new and improved description", - "requirements_doc": '{"para": "meter"}'} + "requirements_doc": '{"foo": "barbar"}'} # PATCH item and verify PATCH_and_assert_expected_response(self, url, test_patch, 200, test_patch) diff --git a/SAS/TMSS/test/tmss_test_data_django_models.py b/SAS/TMSS/test/tmss_test_data_django_models.py index 2e3c669605e640d380b5a82c77fcaaadde8456bf..dd093be160512794fd2c8a7025d4e8f6d0e2b5cf 100644 --- a/SAS/TMSS/test/tmss_test_data_django_models.py +++ b/SAS/TMSS/test/tmss_test_data_django_models.py @@ -50,14 +50,41 @@ def DefaultGeneratorTemplate_test_data(name=None, template=None) -> dict: 'template': template, 'tags':[]} -def SchedulingUnitTemplate_test_data(name="my_SchedulingUnitTemplate", version:str=None) -> dict: +def SchedulingUnitTemplate_test_data(name="my_SchedulingUnitTemplate", version:str=None, schema:dict=None) -> dict: if version is None: version = str(uuid.uuid4()) + if schema is None: + schema = { "$schema": "https://json-schema.org/draft/2019-09/schema", + "type": "object", + "properties": { "foo" : { "type": "string", "default": "bar" } }, + "required": ["foo"], + "default": {} + } + return {"name": name, "description": 'My SchedulingUnitTemplate description', "version": version, - "schema": {"mykey": "my value"}, + "schema": schema, + "tags": ["TMSS", "TESTING"]} + +def SchedulingUnitObservingStrategyTemplate_test_data(name="my_SchedulingUnitObservingStrategyTemplate", version:str=None, + scheduling_unit_template:models.SchedulingUnitTemplate=None, + template:dict=None) -> dict: + if version is None: + version = str(uuid.uuid4()) + + if scheduling_unit_template is None: + scheduling_unit_template = models.SchedulingUnitTemplate.objects.create(**SchedulingUnitTemplate_test_data()) + + if template is None: + template = get_default_json_object_for_schema(scheduling_unit_template.schema) + + return {"name": name, + "description": 'My SchedulingUnitTemplate description', + "version": version, + "template": template, + "scheduling_unit_template": scheduling_unit_template, "tags": ["TMSS", "TESTING"]} def TaskTemplate_test_data(name="my TaskTemplate", version:str=None) -> dict: @@ -135,7 +162,9 @@ def SchedulingSet_test_data(name="my_scheduling_set", project: models.Project=No "generator_template": models.GeneratorTemplate.objects.create(**GeneratorTemplate_test_data()), "generator_source": None} -def SchedulingUnitDraft_test_data(name="my_scheduling_unit_draft", scheduling_set: models.SchedulingSet=None, template: models.SchedulingUnitTemplate=None, requirements_doc: dict=None) -> dict: +def SchedulingUnitDraft_test_data(name="my_scheduling_unit_draft", scheduling_set: models.SchedulingSet=None, + template: models.SchedulingUnitTemplate=None, requirements_doc: dict=None, + observation_strategy_template: models.SchedulingUnitObservingStrategyTemplate=None) -> dict: if scheduling_set is None: scheduling_set = models.SchedulingSet.objects.create(**SchedulingSet_test_data()) @@ -145,6 +174,9 @@ def SchedulingUnitDraft_test_data(name="my_scheduling_unit_draft", scheduling_se if requirements_doc is None: requirements_doc = get_default_json_object_for_schema(template.schema) + if observation_strategy_template is None: + observation_strategy_template = models.SchedulingUnitObservingStrategyTemplate.objects.create(**SchedulingUnitObservingStrategyTemplate_test_data()) + return {"name": name, "description": "", "tags": [], @@ -153,7 +185,8 @@ def SchedulingUnitDraft_test_data(name="my_scheduling_unit_draft", scheduling_se "generator_instance_doc": "para", "copies": None, "scheduling_set": scheduling_set, - "requirements_template": template } + "requirements_template": template, + "observation_strategy_template": observation_strategy_template } def TaskDraft_test_data(name: str="my_task_draft", specifications_template: models.TaskTemplate=None, specifications_doc: dict=None, scheduling_unit_draft: models.SchedulingUnitDraft=None) -> dict: if specifications_template is None: diff --git a/SAS/TMSS/test/tmss_test_data_rest.py b/SAS/TMSS/test/tmss_test_data_rest.py index 64bf43f8744bd28b8add35d568597d95a53c42ba..d919fbbcc46cddd25b80ccc6e091b43802775c64 100644 --- a/SAS/TMSS/test/tmss_test_data_rest.py +++ b/SAS/TMSS/test/tmss_test_data_rest.py @@ -26,6 +26,7 @@ import uuid import requests import json from lofar.common.json_utils import get_default_json_object_for_schema +from http import HTTPStatus class TMSSRESTTestDataCreator(): def __init__(self, django_api_url: str, auth: requests.auth.HTTPBasicAuth): @@ -43,7 +44,10 @@ class TMSSRESTTestDataCreator(): def post_data_and_get_response_as_json_object(self, data, url_postfix): """POST the given data the self.django_api_url+url_postfix, and return the response""" - return json.loads(self.post_data_and_get_response(data, url_postfix).content.decode('utf-8')) + response = self.post_data_and_get_response(data, url_postfix) + if response.status_code == HTTPStatus.CREATED: + return json.loads(response.content.decode('utf-8')) + raise Exception("Error during POST request of '%s' status=%s content: %s" % (url_postfix, response.status_code, response.content.decode('utf-8'))) def post_data_and_get_url(self, data, url_postfix): """POST the given data the self.django_api_url+url_postfix, and return the response's url""" @@ -72,17 +76,45 @@ class TMSSRESTTestDataCreator(): "create_function": 'Funky', "tags": ["TMSS", "TESTING"]} - def SchedulingUnitTemplate(self, name="schedulingunittemplate1", version:str=None) -> dict: + def SchedulingUnitTemplate(self, name="schedulingunittemplate1", version:str=None, schema:dict=None) -> dict: if version is None: version = str(uuid.uuid4()) + if schema is None: + schema = {"$schema": "https://json-schema.org/draft/2019-09/schema", + "type": "object", + "properties": {"foo": {"type": "string", "default": "bar"}}, + "required": ["foo"], + "default": {} + } + return { "name": name, "description": 'My description', "version": version, - "schema": {"mykey": "my value"}, + "schema": schema, "tags": ["TMSS", "TESTING"]} - - def TaskTemplate(self, name="tasktemplate1", task_type_url:str=None, version:str=None) -> dict: + + def SchedulingUnitObservingStrategyTemplate(self, name="my_SchedulingUnitObservingStrategyTemplate", version:str=None, + scheduling_unit_template_url=None, + template:dict=None) -> dict: + if version is None: + version = str(uuid.uuid4()) + + if scheduling_unit_template_url is None: + scheduling_unit_template_url = self.post_data_and_get_url(self.SchedulingUnitTemplate(), '/scheduling_unit_template/') + + if template is None: + scheduling_unit_template = self.get_response_as_json_object(scheduling_unit_template_url) + template = get_default_json_object_for_schema(scheduling_unit_template['schema']) + + return {"name": name, + "description": 'My SchedulingUnitTemplate description', + "version": version, + "template": template, + "scheduling_unit_template": scheduling_unit_template_url, + "tags": ["TMSS", "TESTING"]} + + def TaskTemplate(self, name="tasktemplate1", task_type_url: str = None, version: str = None) -> dict: if version is None: version = str(uuid.uuid4()) @@ -185,7 +217,7 @@ class TMSSRESTTestDataCreator(): "generator_source": None, "scheduling_unit_drafts": []} - def SchedulingUnitDraft(self, name="my_scheduling_unit_draft", scheduling_set_url=None, template_url=None, requirements_doc=None): + def SchedulingUnitDraft(self, name="my_scheduling_unit_draft", scheduling_set_url=None, template_url=None, requirements_doc=None, observation_strategy_template_url=None): if scheduling_set_url is None: scheduling_set_url = self.post_data_and_get_url(self.SchedulingSet(), '/scheduling_set/') @@ -196,6 +228,9 @@ class TMSSRESTTestDataCreator(): scheduling_unit_template = self.get_response_as_json_object(template_url) requirements_doc = get_default_json_object_for_schema(scheduling_unit_template['schema']) + # if observation_strategy_template_url is None: + # observation_strategy_template_url = self.post_data_and_get_url(self.SchedulingUnitObservingStrategyTemplate(scheduling_unit_template_url=template_url), '/scheduling_unit_observing_strategy_template/') + return {"name": name, "description": "This is my run draft", "tags": [], @@ -205,6 +240,7 @@ class TMSSRESTTestDataCreator(): "copies": None, "scheduling_set": scheduling_set_url, "requirements_template": template_url, + "observation_strategy_template": observation_strategy_template_url, "scheduling_unit_blueprints": [], "task_drafts": []} @@ -256,17 +292,21 @@ class TMSSRESTTestDataCreator(): "selection_template": template_url, 'related_task_relation_blueprint': []} - def SchedulingUnitBlueprint(self, name="my_scheduling_unit_blueprint", scheduling_unit_draft_url=None, template_url=None): - if scheduling_unit_draft_url is None: - scheduling_unit_draft_url = self.post_data_and_get_url(self.SchedulingUnitDraft(), '/scheduling_unit_draft/') - + def SchedulingUnitBlueprint(self, name="my_scheduling_unit_blueprint", scheduling_unit_draft_url=None, template_url=None, requirements_doc:dict=None): if template_url is None: template_url = self.post_data_and_get_url(self.SchedulingUnitTemplate(), '/scheduling_unit_template/') - + + if scheduling_unit_draft_url is None: + scheduling_unit_draft_url = self.post_data_and_get_url(self.SchedulingUnitDraft(template_url=template_url), '/scheduling_unit_draft/') + + if requirements_doc is None: + scheduling_unit_template = self.get_response_as_json_object(template_url) + requirements_doc = get_default_json_object_for_schema(scheduling_unit_template['schema']) + return {"name": name, "description": "This is my run blueprint", "tags": [], - "requirements_doc": "{}", + "requirements_doc": requirements_doc, "do_cancel": False, "draft": scheduling_unit_draft_url, "requirements_template": template_url, @@ -410,9 +450,9 @@ class TMSSRESTTestDataCreator(): if cluster_url is None: cluster_url = self.post_data_and_get_url(self.Cluster(), '/cluster/') - if task_blueprint_url is None: - task_blueprint = self.TaskBlueprint() - task_blueprint_url = self.post_data_and_get_url(task_blueprint, '/task_blueprint/') + # if task_blueprint_url is None: + # task_blueprint = self.TaskBlueprint() + # task_blueprint_url = self.post_data_and_get_url(task_blueprint, '/task_blueprint/') if specifications_template_url is None: specifications_template_url = self.post_data_and_get_url(self.SubtaskTemplate(), '/subtask_template/')