diff --git a/SAS/TMSS/frontend/tmss_webapp/src/App.js b/SAS/TMSS/frontend/tmss_webapp/src/App.js index f73f01cd8ac520904cb940463c0333101a8a3e49..0d028b94c866d1375b2fff83dad07463d1bf8822 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/App.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/App.js @@ -37,7 +37,8 @@ class App extends Component { {label: 'Cycle', icon:'pi pi-fw pi-spinner', to:'/cycle'}, {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: 'Project', icon: 'fa fa-fw fa-binoculars', to:'/project'}, + ]; // this.menuComponent = {'Dashboard': Dashboard} diff --git a/SAS/TMSS/frontend/tmss_webapp/src/__mocks__/cycle.service.data.js b/SAS/TMSS/frontend/tmss_webapp/src/__mocks__/cycle.service.data.js index e69c3c9bd88800a7bf105b5c7f2c0f7dd7b1e42f..f1542016c49e26279f873ee947f1e46ab89325cf 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/__mocks__/cycle.service.data.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/__mocks__/cycle.service.data.js @@ -1,4 +1,326 @@ -export default{ + +const CycleServiceMock= { + project_categories: [{url: "Regular", value: 'Regular'}, {url: "User Shared Support", value: 'User Shared Support'}], + period_categories: [{url: "Single Cycle", value: 'Single Cycle'}, {url: "Long Term", value: 'Long Term'}], + resources: [{ + "name": "LOFAR Observing Time", + "url": "http://localhost:3000/api/resource_type/LOFAR%20Observing%20Time/", + "created_at": "2020-07-29T07:31:21.708296", + "description": "LOFAR Observing Time", + "resource_unit": "http://localhost:3000/api/resource_unit/second/", + "resource_unit_id": "second", + "tags": [ + ], + "updated_at": "2020-07-29T07:31:21.708316", + "resourceUnit": { + "name": "second", + "url": "http://localhost:3000/api/resource_unit/second/", + "created_at": "2020-07-29T07:31:21.070088", + "description": "Unit of time or duration", + "tags": [ + ], + "updated_at": "2020-07-29T07:31:21.070114" + } + }, + { + "name": "LOFAR Observing Time prio A", + "url": "http://localhost:3000/api/resource_type/LOFAR%20Observing%20Time%20prio%20A/", + "created_at": "2020-07-29T07:31:21.827537", + "description": "LOFAR Observing Time prio A", + "resource_unit": "http://localhost:3000/api/resource_unit/second/", + "resource_unit_id": "second", + "tags": [ + ], + "updated_at": "2020-07-29T07:31:21.827675", + "resourceUnit": { + "name": "second", + "url": "http://localhost:3000/api/resource_unit/second/", + "created_at": "2020-07-29T07:31:21.070088", + "description": "Unit of time or duration", + "tags": [ + ], + "updated_at": "2020-07-29T07:31:21.070114" + } + }, + { + "name": "LOFAR Observing Time prio B", + "url": "http://localhost:3000/api/resource_type/LOFAR%20Observing%20Time%20prio%20B/", + "created_at": "2020-07-29T07:31:21.950948", + "description": "LOFAR Observing Time prio B", + "resource_unit": "http://localhost:3000/api/resource_unit/second/", + "resource_unit_id": "second", + "tags": [ + ], + "updated_at": "2020-07-29T07:31:21.950968", + "resourceUnit": { + "name": "second", + "url": "http://localhost:3000/api/resource_unit/second/", + "created_at": "2020-07-29T07:31:21.070088", + "description": "Unit of time or duration", + "tags": [ + ], + "updated_at": "2020-07-29T07:31:21.070114" + } + }, + { + "name": "CEP Processing Time", + "url": "http://localhost:3000/api/resource_type/CEP%20Processing%20Time/", + "created_at": "2020-07-29T07:31:22.097916", + "description": "CEP Processing Time", + "resource_unit": "http://localhost:3000/api/resource_unit/second/", + "resource_unit_id": "second", + "tags": [ + ], + "updated_at": "2020-07-29T07:31:22.097941", + "resourceUnit": { + "name": "second", + "url": "http://localhost:3000/api/resource_unit/second/", + "created_at": "2020-07-29T07:31:21.070088", + "description": "Unit of time or duration", + "tags": [ + ], + "updated_at": "2020-07-29T07:31:21.070114" + } + }, + { + "name": "LTA Storage", + "url": "http://localhost:3000/api/resource_type/LTA%20Storage/", + "created_at": "2020-07-29T07:31:22.210071", + "description": "LTA Storage", + "resource_unit": "http://localhost:3000/api/resource_unit/byte/", + "resource_unit_id": "byte", + "tags": [ + ], + "updated_at": "2020-07-29T07:31:22.210091", + "resourceUnit": { + "name": "byte", + "url": "http://localhost:3000/api/resource_unit/byte/", + "created_at": "2020-07-29T07:31:21.500997", + "description": "Unit of data storage", + "tags": [ + ], + "updated_at": "2020-07-29T07:31:21.501028" + } + }, + { + "name": "Number of triggers", + "url": "http://localhost:3000/api/resource_type/Number%20of%20triggers/", + "created_at": "2020-07-29T07:31:22.317313", + "description": "Number of triggers", + "resource_unit": "http://localhost:3000/api/resource_unit/number/", + "resource_unit_id": "number", + "tags": [ + ], + "updated_at": "2020-07-29T07:31:22.317341", + "resourceUnit": { + "name": "number", + "url": "http://localhost:3000/api/resource_unit/number/", + "created_at": "2020-07-29T07:31:21.596364", + "description": "Unit of count", + "tags": [ + ], + "updated_at": "2020-07-29T07:31:21.596385" + } + }, + { + "name": "LOFAR Support Time", + "url": "http://localhost:3000/api/resource_type/LOFAR%20Support%20Time/", + "created_at": "2020-07-29T07:31:22.437945", + "description": "LOFAR Support Time", + "resource_unit": "http://localhost:3000/api/resource_unit/second/", + "resource_unit_id": "second", + "tags": [ + ], + "updated_at": "2020-07-29T07:31:22.437964", + "resourceUnit": { + "name": "second", + "url": "http://localhost:3000/api/resource_unit/second/", + "created_at": "2020-07-29T07:31:21.070088", + "description": "Unit of time or duration", + "tags": [ + ], + "updated_at": "2020-07-29T07:31:21.070114" + } + }, + { + "name": "LOFAR Support hours", + "url": "http://localhost:3000/api/resource_type/LOFAR%20Support%20hours/", + "created_at": "2020-07-29T07:31:22.571850", + "description": "LOFAR Support hours", + "resource_unit": "http://localhost:3000/api/resource_unit/second/", + "resource_unit_id": "second", + "tags": [ + ], + "updated_at": "2020-07-29T07:31:22.571869", + "resourceUnit": { + "name": "second", + "url": "http://localhost:3000/api/resource_unit/second/", + "created_at": "2020-07-29T07:31:21.070088", + "description": "Unit of time or duration", + "tags": [ + ], + "updated_at": "2020-07-29T07:31:21.070114" + } + }, + { + "name": "Support hours", + "url": "http://localhost:3000/api/resource_type/Support%20hours/", + "created_at": "2020-07-29T07:31:22.694438", + "description": "Support hours", + "resource_unit": "http://localhost:3000/api/resource_unit/second/", + "resource_unit_id": "second", + "tags": [ + ], + "updated_at": "2020-07-29T07:31:22.694514", + "resourceUnit": { + "name": "second", + "url": "http://localhost:3000/api/resource_unit/second/", + "created_at": "2020-07-29T07:31:21.070088", + "description": "Unit of time or duration", + "tags": [ + ], + "updated_at": "2020-07-29T07:31:21.070114" + } + } + ], + projectResourceDefaults: { + 'LOFAR Observing Time': 3600, + 'LOFAR Observing Time prio A': 3600, + 'LOFAR Observing Time prio B': 3600, + 'CEP Processing Time': 3600, + 'LTA Storage': 1024*1024*1024*1024, + 'Number of triggers': 1, + 'LOFAR Support Time': 3600 + }, + cycle: [{ + "name": "test7", + "url": "http://192.168.99.100:8008/api/cycle/test7/", + "created_at": "2020-08-10T11:50:15.427875", + "description": "test7", + "duration": 1778443.978, + "projects": [], + "projects_ids": [], + "quota": [ + "http://192.168.99.100:8008/api/cycle_quota/136/" + ], + "quota_ids": [ + 136 + ], + "start": "2020-08-10T14:49:11.405000", + "stop": "2020-08-31T04:49:55.383000", + "tags": [], + "updated_at": "2020-08-10T11:50:15.427895" + }], + cycleQuota: [ + { + "id": 1, + "url": "http://192.168.99.100:8008/api/cycle_quota/1/", + "cycle": "http://192.168.99.100:8008/api/cycle/Cycle%2000/", + "cycle_id": "Cycle 00", + "resource_type": "http://192.168.99.100:8008/api/resource_type/observing_time/", + "resource_type_id": "observing_time", + "value": 10575360 + }, + { + "id": 2, + "url": "http://192.168.99.100:8008/api/cycle_quota/2/", + "cycle": "http://192.168.99.100:8008/api/cycle/Cycle%2000/", + "cycle_id": "Cycle 00", + "resource_type": "http://192.168.99.100:8008/api/resource_type/cep_processing_time/", + "resource_type_id": "cep_processing_time", + "value": 10575360 + }, + { + "id": 3, + "url": "http://192.168.99.100:8008/api/cycle_quota/3/", + "cycle": "http://192.168.99.100:8008/api/cycle/Cycle%2000/", + "cycle_id": "Cycle 00", + "resource_type": "http://192.168.99.100:8008/api/resource_type/lta_storage/", + "resource_type_id": "lta_storage", + "value": 0 + }, + { + "id": 4, + "url": "http://192.168.99.100:8008/api/cycle_quota/4/", + "cycle": "http://192.168.99.100:8008/api/cycle/Cycle%2000/", + "cycle_id": "Cycle 00", + "resource_type": "http://192.168.99.100:8008/api/resource_type/support_time/", + "resource_type_id": "support_time", + "value": 0 + }, + { + "id": 5, + "url": "http://192.168.99.100:8008/api/cycle_quota/5/", + "cycle": "http://192.168.99.100:8008/api/cycle/Cycle%2000/", + "cycle_id": "Cycle 00", + "resource_type": "http://192.168.99.100:8008/api/resource_type/observing_time_commissioning/", + "resource_type_id": "observing_time_commissioning", + "value": 660960 + }, + { + "id": 6, + "url": "http://192.168.99.100:8008/api/cycle_quota/6/", + "cycle": "http://192.168.99.100:8008/api/cycle/Cycle%2000/", + "cycle_id": "Cycle 00", + "resource_type": "http://192.168.99.100:8008/api/resource_type/observing_time_prio_a/", + "resource_type_id": "observing_time_prio_a", + "value": 0 + }, + { + "id": 7, + "url": "http://192.168.99.100:8008/api/cycle_quota/7/", + "cycle": "http://192.168.99.100:8008/api/cycle/Cycle%2000/", + "cycle_id": "Cycle 00", + "resource_type": "http://192.168.99.100:8008/api/resource_type/observing_time_prio_b/", + "resource_type_id": "observing_time_prio_b", + "value": 0 + }, + { + "id": 8, + "url": "http://192.168.99.100:8008/api/cycle_quota/8/", + "cycle": "http://192.168.99.100:8008/api/cycle/Cycle%2001/", + "cycle_id": "Cycle 01", + "resource_type": "http://192.168.99.100:8008/api/resource_type/observing_time/", + "resource_type_id": "observing_time", + "value": 14653440 + }, + { + "id": 9, + "url": "http://192.168.99.100:8008/api/cycle_quota/9/", + "cycle": "http://192.168.99.100:8008/api/cycle/Cycle%2001/", + "cycle_id": "Cycle 01", + "resource_type": "http://192.168.99.100:8008/api/resource_type/cep_processing_time/", + "resource_type_id": "cep_processing_time", + "value": 14653440 + }, + { + "id": 10, + "url": "http://192.168.99.100:8008/api/cycle_quota/10/", + "cycle": "http://192.168.99.100:8008/api/cycle/Cycle%2001/", + "cycle_id": "Cycle 01", + "resource_type": "http://192.168.99.100:8008/api/resource_type/lta_storage/", + "resource_type_id": "lta_storage", + "value": 0 + }, + { + "id": 11, + "url": "http://192.168.99.100:8008/api/cycle_quota/11/", + "cycle": "http://192.168.99.100:8008/api/cycle/Cycle%2001/", + "cycle_id": "Cycle 01", + "resource_type": "http://192.168.99.100:8008/api/resource_type/support_time/", + "resource_type_id": "support_time", + "value": 0 + }, + { + "id": 12, + "url": "http://192.168.99.100:8008/api/cycle_quota/12/", + "cycle": "http://192.168.99.100:8008/api/cycle/Cycle%2001/", + "cycle_id": "Cycle 01", + "resource_type": "http://192.168.99.100:8008/api/resource_type/observing_time_commissioning/", + "resource_type_id": "observing_time_commissioning", + "value": 915840 + } + ], getProjects: { "results": [{ "name": "TMSS-Commissioning", @@ -158,3 +480,4 @@ export default{ } } +export default CycleServiceMock; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/ViewTable.js b/SAS/TMSS/frontend/tmss_webapp/src/components/ViewTable.js index 3fbebefc8bc019b84d613c4472574d169bb1b78f..685256e8d07e5c4a9cdeb22e8a89529f305aa38f 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/components/ViewTable.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/components/ViewTable.js @@ -348,7 +348,7 @@ function ViewTable(props) { }) return retval; }else if(typeof value == "string"){ - const dateval = moment(value, moment.ISO_8601).format("YYYY-MMM-DD HH:mm:SS"); + const dateval = moment(value, moment.ISO_8601).format("YYYY-MMM-DD HH:mm:ss"); if(dateval !== 'Invalid date'){ return dateval; } diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/ResourceDisplayList.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/ResourceDisplayList.js new file mode 100644 index 0000000000000000000000000000000000000000..c1a00381eeb63e93e97479edecf8a7e33988fd03 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/ResourceDisplayList.js @@ -0,0 +1,31 @@ +import React, {Component} from 'react'; + +/** + * Component to get input for Resource allocation while creating and editing Cycle + */ +class ResourceDisplayList extends Component { + constructor(props) { + super(props); + this.state = { + cycleQuota: props.cycleQuota + } + } + + render(){ + return ( + <> + {this.props.cycleQuota.length>0 && this.props.cycleQuota.map((item, index) => ( + <React.Fragment key={index+10}> + <label key={'label1-'+ index} className="col-lg-3 col-md-3 col-sm-12">{item.resource.name}</label> + <span key={'div1-'+ index} className="col-lg-3 col-md-3 col-sm-12"> + {item.value/(this.props.unitMap[item.resource.quantity_value]?this.props.unitMap[item.resource.quantity_value].conversionFactor:1)} + {` ${this.props.unitMap[item.resource.quantity_value]?this.props.unitMap[item.resource.quantity_value].display:''}`} + </span> + </React.Fragment> + ))} + </> + ); + } +} + +export default ResourceDisplayList; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/ResourceInputList.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/ResourceInputList.js new file mode 100644 index 0000000000000000000000000000000000000000..940dec9e217daa5f9a9f103a59a874c4e5e6f526 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/ResourceInputList.js @@ -0,0 +1,58 @@ +import React, {Component} from 'react'; +import {InputNumber} from 'primereact/inputnumber'; + +/** + * Component to get input for Resource allocation while creating and editing Cycle + */ +export class ResourceInputList extends Component { + constructor(props) { + super(props); + this.state = { + list: props.list, + cycleQuota: props.cycleQuota + } + this.updateEnabled = this.props.list.length===0?true:false; + this.onInputChange = this.onInputChange.bind(this); + } + + shouldComponentUpdate() { + return true; + } + + onInputChange(field, event) { + if (this.props.callback) { + this.props.callback(field, event); + } + } + + removeInput(field) { + if (this.props.removeInputCallback) { + this.props.removeInputCallback(field); + } + } + + render(){ + return ( + <> + {this.props.list.length>0 && this.props.list.map((item, index) => ( + <React.Fragment key={index+10}> + <label key={'label1-'+ index} className="col-lg-2 col-md-2 col-sm-12">{item.name}</label> + <div key={'div1-'+ index} className="col-lg-3 col-md-3 col-sm-12"> + <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]} + onChange={(e) => this.onInputChange(item.name, e)} + onBlur={(e) => this.onInputChange(item.name, e)} + style={{width:"90%", marginRight: "5px"}} + /> + <button className="p-link" data-testid={`${item.name}-btn`} onClick={(e) => this.removeInput(item.name)}> + <i className="fa fa-trash pi-error"></i></button> + </div> + <div className="col-lg-1 col-md-1 col-sm-12"></div> + </React.Fragment> + ))} + </> + ); + } +} \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/create.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/create.js new file mode 100644 index 0000000000000000000000000000000000000000..73268577c74802b207eaad26edb399fded4885ee --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/create.js @@ -0,0 +1,475 @@ +import React, {Component} from 'react'; +import { Link, Redirect } from 'react-router-dom'; +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 moment from 'moment' +import _ from 'lodash'; + +import AppLoader from '../../layout/components/AppLoader'; +import CycleService from '../../services/cycle.service'; +import UnitConverter from '../../utils/unit.converter'; +import UIConstants from '../../utils/ui.constants'; + +/** + * Component to create a new Cycle + */ +export class CycleCreate extends Component { + constructor(props) { + super(props); + this.state = { + isLoading: true, + dialog: { header: '', detail: ''}, + cycle: { + projects: [], + quota: [], + start: "", + stop: "", + }, + cycleQuota: {}, // Resource Allocations + validFields: {}, // For Validation + validForm: false, // To enable Save Button + errors: {}, // Validation Errors + resources: [], // Selected Resources for Allocation + resourceList: [], // Available Resources for Allocation + } + // Validateion 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.defaultResourcesEnabled = true; // This property and functionality to be concluded based on PO input + 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 = {}; // Default values for default resources + this.resourceUnitMap = UnitConverter.resourceUnitMap; // Resource unit conversion factor and constraints + this.tooltipOptions = UIConstants.tooltipOptions; + this.setCycleQuotaDefaults = this.setCycleQuotaDefaults.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.cancelCreate = this.cancelCreate.bind(this); + this.reset = this.reset.bind(this); + } + + componentDidMount() { + CycleService.getResources() + .then(resourceList => { + const defaultResources = this.defaultResources; + resourceList = _.sortBy(resourceList, "name"); + const resources = _.remove(resourceList, function(resource) { return _.find(defaultResources, {'name': resource.name})!=null }); + const cycleQuota = this.setCycleQuotaDefaults(resources); + this.setState({resourceList: resourceList, resources: resources, cycleQuota: cycleQuota, isLoading: false}); + }); + } + + /** + * 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; + cycleQuota[resource['name']] = 0; + } + 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; + 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]); + resourceList = _.sortBy(resourceList, 'name'); + 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; + } + } + } + } + + this.setState({errors: errors, validFields: validFields}); + 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 can not be before Start date`; + validForm = false; + }else{ + validForm = true; + } + } + return validForm; + } + + /** + * Function to call when 'Save' button is clicked to save the Cycle. + */ + saveCycle() { + if (this.validateForm) { + let cycleQuota = []; + let cycle = this.state.cycle; + let stoptime = _.replace(this.state.cycle['stop'],'00:00:00', '23:59:59'); + cycle['start'] = moment(cycle['start']).format("YYYY-MM-DDTHH:mm:ss"); + cycle['stop'] = moment(stoptime).format("YYYY-MM-DDTHH:mm:ss"); + this.setState({cycle: cycle}); + for (const resource in this.state.cycleQuota) { + let resourceType = _.find(this.state.resources, {'name': resource}); + if(resourceType){ + let quota = { cycle: this.state.cycle.name, + resource_type: resourceType['url'], + value: this.state.cycleQuota[resource] * (this.resourceUnitMap[resourceType.quantity_value]?this.resourceUnitMap[resourceType.quantity_value].conversionFactor:1)}; + cycleQuota.push(quota); + } + + } + + CycleService.saveCycle(this.state.cycle, this.defaultResourcesEnabled?cycleQuota:[]) + .then(cycle => { + if (cycle.url) { + let dialog = {}; + if (this.defaultResourcesEnabled) { + dialog = {header: 'Success', detail: 'Cycle saved successfully. Do you want to create another Cycle?'}; + } else { + dialog = {header: 'Success', detail: 'Cycle saved successfully with default Resource allocations. Do you want to view and edit them?'}; + } + this.setState({cycle:cycle, dialogVisible: true, dialog: dialog}) + } else { + this.growl.show({severity: 'error', summary: 'Error Occured', detail: 'Unable to save Cycle'}); + this.setState({errors: cycle}); + } + }); + } + } + + /** + * Function to cancel form creation and navigate to other page/component + */ + cancelCreate() { + this.setState({redirect: '/cycle'}); + } + + /** + * Reset function to be called to reset the form fields + */ + reset() { + if (this.defaultResourcesEnabled) { + let prevResources = this.state.resources; + let resourceList = []; + let resources = []; + if (resources) { + const defaultResources = this.defaultResources; + resourceList = _.sortBy(prevResources.concat(this.state.resourceList), "name"); + resources = _.remove(resourceList, function(resource) { return _.find(defaultResources, {'name': resource.name})!=null }); + } + const cycleQuota = this.setCycleQuotaDefaults(resources); + this.setState({ + dialog: { header: '', detail: ''}, + cycle: { + name: '', + description: '', + start: '', + stop: '', + projects: [], + quota: [], + }, + cycleQuota: cycleQuota, + validFields: {}, + validForm: false, + errors: {}, + dialogVisible: false, + resources: resources, + resourceList: resourceList + }); + } else { + this.setState({redirect: `/cycle/edit/${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 - Add</h2> + </div> + <div className="p-col-2 p-lg-2 p-md-2"> + <Link to={{ pathname: '/cycle'}} tite="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" style={{display: 'none'}}> + <label htmlFor="cycleId" className="col-lg-2 col-md-2 col-sm-12">URL </label> + <div className="col-lg-4 col-md-4 col-sm-12"> + <input id="cycleId" data-testid="cycleId" value={this.state.cycle.url} /> + </div> + </div> + <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" + value= {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= {this.state.cycle.stop} + onChange= {e => this.setCycleParams('stop', e.value)} + onBlur= {e => this.setCycleParams('stop',e.value)} + 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.defaultResourcesEnabled && 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 data-testid="resource_alloc">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={this.state.resourceList} + 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} data-testid="add_res_btn" /> + </div> + </div> + <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.cancelCreate} /> + </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: '25vw'}} inputId="confirm_dialog" + modal={true} onHide={() => {this.setState({dialogVisible: false})}} + footer={<div> + <Button key="back" onClick={() => {this.setState({dialogVisible: false}); this.cancelCreate();}} label="No" /> + <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/create.test.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/create.test.js new file mode 100644 index 0000000000000000000000000000000000000000..d738a4a909a0efe7fa9079432fb34aa161fb84e3 --- /dev/null +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/create.test.js @@ -0,0 +1,346 @@ +import React from 'react'; +import { BrowserRouter as Router } from 'react-router-dom'; +import { act } from "react-dom/test-utils"; +import { render, cleanup, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; + +import {CycleCreate} from './create'; +import CycleService from '../../services/cycle.service'; + +import CycleServiceMock from '../../__mocks__/cycle.service.data'; + +let saveCycleSpy, resourcesSpy, cycleResourceDefaultsSpy; + +beforeEach(() => { + setMockSpy(); +}); + +afterEach(() => { + // cleanup on exiting + clearMockSpy(); + cleanup(); +}); + +/** + * To set mock spy for Services that have API calls to the back end to fetch data + */ +const setMockSpy = (() => { + + resourcesSpy = jest.spyOn(CycleService, 'getResources'); + resourcesSpy.mockImplementation(() => { + return Promise.resolve(CycleServiceMock.resources); + }); + + + saveCycleSpy = jest.spyOn(CycleService, 'saveCycle'); + saveCycleSpy.mockImplementation((cycle, cycleQuota) => { + cycle.url = `http://localhost:3000/api/cycle/${cycle.name}`; + return Promise.resolve(cycle) + }); +}); + +const clearMockSpy = (() => { + saveCycleSpy.mockRestore(); +}); + +it("renders without crashing with all back-end data loaded", async () => { + console.log("renders without crashing with all back-end data loaded ------------------------"); + + let content; + await act(async () => { + content = render(<Router><CycleCreate /></Router>); + }); + + expect(content.queryByText('Cycle - Add')).not.toBe(null); // Page loaded successfully + expect(content.queryAllByText('Add Resources').length).toBe(2); // Resource Dropdown loaded successfully + expect(content.queryByText('Support hours')).toBeInTheDocument(); // Resources other than Default Resources listed in dropdown + expect(content.queryByPlaceholderText('Support Hours')).toBe(null); // No resources other than Default Resources listed to get input + expect(content.queryByPlaceholderText('LOFAR Observing Time').value).toBe('1 Hours'); // Default Resource Listed with default value +}); + +it("Save button disabled initially when no data entered", async () => { + console.log("Save button disabled initially when no data entered -----------------------"); + let content; + await act(async () => { + content = render(<Router><CycleCreate /></Router>); + }); + expect(content.queryByTestId('save-btn')).toHaveAttribute("disabled"); +}); + +it("Save button enabled when mandatory data entered", async () => { + console.log("Save button enabled when mandatory data entered -----------------------"); + let content; + await act(async () => { + content = render(<Router><CycleCreate /></Router>); + }); + const nameInput = content.queryByTestId('name'); + const descInput = content.queryByTestId('description'); + const startInput = content.queryByTestId('start'); + const stopInput = content.queryByTestId('stop'); + + fireEvent.change(nameInput, { target: { value: 'Cycle-12' } }); + expect(nameInput.value).toBe("Cycle-12"); + + fireEvent.change(descInput, { target: { value: 'Cycle-12' } }); + expect(descInput.value).toBe("Cycle-12"); + + fireEvent.change(startInput, { target: { value: '2020-07-29 11:12:15' } }); + expect(startInput.value).toBe("2020-07-29 11:12:15"); + + fireEvent.change(stopInput, { target: { value: '2020-07-30 11:12:15' } }); + expect(stopInput.value).toBe("2020-07-30 11:12:15"); + expect(content.queryByTestId('save-btn').hasAttribute("disabled")).toBeFalsy(); +}); + +it("renders Save button enabled when all data entered", async () => { + console.log("renders Save button enabled when all data entered -----------------------"); + let content; + await act(async () => { + content = render(<Router><CycleCreate /></Router>); + }); + + const nameInput = content.queryByTestId('name'); + const descInput = content.queryByTestId('description'); + const startInput = content.queryByTestId('start'); + const stopInput = content.queryByTestId('stop'); + + fireEvent.change(nameInput, { target: { value: 'Cycle-12' } }); + expect(nameInput.value).toBe("Cycle-12"); + + fireEvent.change(descInput, { target: { value: 'Cycle-12' } }); + expect(descInput.value).toBe("Cycle-12"); + + fireEvent.change(startInput, { target: { value: '2020-07-29 11:12:15' } }); + expect(startInput.value).toBe("2020-07-29 11:12:15"); + + fireEvent.change(stopInput, { target: { value: '2020-07-30 11:12:15' } }); + expect(stopInput.value).toBe("2020-07-30 11:12:15"); + + expect(content.queryByTestId('save-btn').hasAttribute("disabled")).toBeFalsy(); + +}); + +it("save cycle with mandatory fields", async () => { + console.log("save cycle -----------------------"); + let content; + await act(async () => { + content = render(<Router><CycleCreate /></Router>); + }); + + const nameInput = content.queryByTestId('name'); + const descInput = content.queryByTestId('description'); + const startInput = content.queryByTestId('start'); + const stopInput = content.queryByTestId('stop'); + + fireEvent.change(nameInput, { target: { value: 'Cycle-12' } }); + expect(nameInput.value).toBe("Cycle-12"); + fireEvent.change(descInput, { target: { value: 'Cycle-12' } }); + expect(descInput.value).toBe("Cycle-12"); + + fireEvent.change(startInput, { target: { value: '2020-07-29 11:12:15' } }); + expect(startInput.value).toBe("2020-07-29 11:12:15"); + + fireEvent.change(stopInput, { target: { value: '2020-07-30 11:12:15' } }); + expect(stopInput.value).toBe("2020-07-30 11:12:15"); + + expect(content.queryByTestId('save-btn').hasAttribute("disabled")).toBeFalsy(); + expect(content.queryByTestId('cycleId').value).toBe(""); + expect(content.queryByText("Success")).toBe(null); + + await act(async () => { + fireEvent.click(content.queryByTestId('save-btn')); + }); + + // After saving cycle, URL should be available and Success dialog should be displayed + expect(content.queryByTestId('cycleId').value).toBe("http://localhost:3000/api/cycle/Cycle-12"); + expect(content.queryByText("Success")).not.toBe(null); +}); + +it("save cycle with default resources", async () => { + console.log("save cycle with default resources -----------------------"); + let content; + await act(async () => { + content = render(<Router><CycleCreate /></Router>); + }); + + const nameInput = content.queryByTestId('name'); + const descInput = content.queryByTestId('description'); + const startInput = content.queryByTestId('start'); + const stopInput = content.queryByTestId('stop'); + + fireEvent.change(nameInput, { target: { value: 'Cycle-12' } }); + expect(nameInput.value).toBe("Cycle-12"); + fireEvent.change(descInput, { target: { value: 'Cycle-12' } }); + expect(descInput.value).toBe("Cycle-12"); + + fireEvent.change(startInput, { target: { value: '2020-07-29 11:12:15' } }); + expect(startInput.value).toBe("2020-07-29 11:12:15"); + + fireEvent.change(stopInput, { target: { value: '2020-07-30 11:12:15' } }); + expect(stopInput.value).toBe("2020-07-30 11:12:15"); + + expect(content.queryByTestId('save-btn').hasAttribute("disabled")).toBeFalsy(); + expect(content.queryByTestId('cycleId').value).toBe(""); + expect(content.queryByText("Success")).toBe(null); + + const lofarObsTimeInput = content.queryByPlaceholderText('LOFAR Observing Time'); + fireEvent.change(lofarObsTimeInput, { target: { value: 10 } }); + expect(lofarObsTimeInput.value).toBe('10'); + + const lofarObsTimeAInput = content.queryByPlaceholderText('LOFAR Observing Time prio A'); + fireEvent.change(lofarObsTimeAInput, { target: { value: 15 } }); + expect(lofarObsTimeAInput.value).toBe('15'); + + const lofarObsTimeBInput = content.queryByPlaceholderText('LOFAR Observing Time prio B'); + fireEvent.change(lofarObsTimeBInput, { target: { value: 20 } }); + expect(lofarObsTimeBInput.value).toBe('20'); + + const cepProcTimeInput = content.queryByPlaceholderText('CEP Processing Time'); + fireEvent.change(cepProcTimeInput, { target: { value: 5 } }); + expect(cepProcTimeInput.value).toBe('5'); + + const ltaStorageInput = content.queryByPlaceholderText('LTA Storage'); + fireEvent.change(ltaStorageInput, { target: { value: 2 } }); + expect(ltaStorageInput.value).toBe('2'); + + const noOfTriggerInput = content.queryByPlaceholderText('Number of triggers'); + fireEvent.change(noOfTriggerInput, { target: { value: 3 } }); + expect(noOfTriggerInput.value).toBe('3'); + + const lofarSupTimeInput = content.queryByPlaceholderText('LOFAR Support Time'); + fireEvent.change(lofarSupTimeInput, { target: { value: 25 } }); + expect(lofarSupTimeInput.value).toBe('25'); + + await act(async () => { + fireEvent.click(content.queryByTestId('save-btn')); + }); + + // After saving cycle, URL should be available and Success dialog should be displayed + expect(content.queryByTestId('cycleId').value).toBe("http://localhost:3000/api/cycle/Cycle-12"); + expect(content.queryByText("Success")).not.toBe(null); +}); + +it("save cycle with added resources", async () => { + console.log("save cycle with added resources -----------------------"); + let content; + await act(async () => { + content = render(<Router><CycleCreate /></Router>); + }); + + const nameInput = content.queryByTestId('name'); + const descInput = content.queryByTestId('description'); + const startInput = content.queryByTestId('start'); + const stopInput = content.queryByTestId('stop'); + + fireEvent.change(nameInput, { target: { value: 'Cycle-12' } }); + expect(nameInput.value).toBe("Cycle-12"); + fireEvent.change(descInput, { target: { value: 'Cycle-12' } }); + expect(descInput.value).toBe("Cycle-12"); + + fireEvent.change(startInput, { target: { value: '2020-07-29 11:12:15' } }); + expect(startInput.value).toBe("2020-07-29 11:12:15"); + + fireEvent.change(stopInput, { target: { value: '2020-07-30 11:12:15' } }); + expect(stopInput.value).toBe("2020-07-30 11:12:15"); + + expect(content.queryByTestId('save-btn').hasAttribute("disabled")).toBeFalsy(); + expect(content.queryByTestId('cycleId').value).toBe(""); + expect(content.queryByText("Success")).toBe(null); + + const lofarObsTimeInput = content.queryByPlaceholderText('LOFAR Observing Time'); + fireEvent.change(lofarObsTimeInput, { target: { value: 10 } }); + expect(lofarObsTimeInput.value).toBe('10'); + + const lofarObsTimeAInput = content.queryByPlaceholderText('LOFAR Observing Time prio A'); + fireEvent.change(lofarObsTimeAInput, { target: { value: 15 } }); + expect(lofarObsTimeAInput.value).toBe('15'); + + const lofarObsTimeBInput = content.queryByPlaceholderText('LOFAR Observing Time prio B'); + fireEvent.change(lofarObsTimeBInput, { target: { value: 20 } }); + expect(lofarObsTimeBInput.value).toBe('20'); + + const cepProcTimeInput = content.queryByPlaceholderText('CEP Processing Time'); + fireEvent.change(cepProcTimeInput, { target: { value: 5 } }); + expect(cepProcTimeInput.value).toBe('5'); + + const ltaStorageInput = content.queryByPlaceholderText('LTA Storage'); + fireEvent.change(ltaStorageInput, { target: { value: 2 } }); + expect(ltaStorageInput.value).toBe('2'); + + const noOfTriggerInput = content.queryByPlaceholderText('Number of triggers'); + fireEvent.change(noOfTriggerInput, { target: { value: 3 } }); + expect(noOfTriggerInput.value).toBe('3'); + + const lofarSupTimeInput = content.queryByPlaceholderText('LOFAR Support Time'); + fireEvent.change(lofarSupTimeInput, { target: { value: 25 } }); + expect(lofarSupTimeInput.value).toBe('25'); + + // Before selecting New Resource + expect(content.queryAllByText('Add Resources').length).toBe(2); + expect(content.queryAllByText('Support hours').length).toBe(1); + expect(content.getAllByRole("listbox")[3].children.length).toBe(2); + expect(content.queryByPlaceholderText('Support hours')).toBe(null); + const addResourceInput = content.getAllByRole("listbox")[3].children[1] ; + fireEvent.click(addResourceInput); + // After selecting New Resource + expect(content.queryAllByText('Add Resources').length).toBe(1); + expect(content.queryAllByText('Support hours').length).toBe(3); + + const addResourceBtn = content.queryByTestId('add_res_btn'); + fireEvent.click(addResourceBtn); + expect(content.queryAllByText('Add Resources').length).toBe(2); + expect(content.queryByPlaceholderText('Support hours')).not.toBe(null); + + const newResourceInput = content.queryByPlaceholderText('Support hours'); + fireEvent.change(newResourceInput, { target: { value: 30 } }); + expect(newResourceInput.value).toBe('30'); + + + await act(async () => { + fireEvent.click(content.queryByTestId('save-btn')); + }); + + // After saving cycle, URL should be available and Success dialog should be displayed + expect(content.queryByTestId('cycleId').value).toBe("http://localhost:3000/api/cycle/Cycle-12"); + expect(content.queryByText("Success")).not.toBe(null); +}); + +it("remove default resource and added resource", async () => { + console.log("remove default resource and added resource -----------------------"); + let content; + await act(async () => { + content = render(<Router><CycleCreate /></Router>); + }); + + // Before selecting New Resource + expect(content.queryAllByText('Add Resources').length).toBe(2); + expect(content.queryAllByText('Support hours').length).toBe(1); + expect(content.getAllByRole("listbox")[3].children.length).toBe(2); + expect(content.queryByPlaceholderText('Support hours')).toBe(null); + const addResourceInput = content.getAllByRole("listbox")[3].children[1] ; + fireEvent.click(addResourceInput); + // After selecting New Resource + expect(content.queryAllByText('Add Resources').length).toBe(1); + expect(content.queryAllByText('Support hours').length).toBe(3); + + const addResourceBtn = content.queryByTestId('add_res_btn'); + fireEvent.click(addResourceBtn); + expect(content.queryAllByText('Add Resources').length).toBe(2); + expect(content.queryByPlaceholderText('Support hours')).not.toBe(null); + + expect(content.queryByPlaceholderText('CEP Processing Time')).not.toBe(null); + expect(content.queryByTestId('CEP Processing Time-btn')).not.toBe(null); + const removeDefResBtn = content.queryByTestId('CEP Processing Time-btn'); + await act(async () => { + fireEvent.click(content.queryByTestId('CEP Processing Time-btn')); + }); + expect(content.queryByPlaceholderText('CEP Processing Time')).toBe(null); + expect(content.queryByTestId('CEP Processing Time-btn')).toBe(null); + + const removeResourceBtn = content.queryByTestId('Support hours-btn'); + fireEvent.click(removeResourceBtn); + expect(content.queryAllByText('Add Resources').length).toBe(2); + expect(content.queryAllByText('Support hours').length).toBe(1); + expect(content.getAllByRole("listbox")[3].children.length).toBe(3); + +}); \ 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 25679b1caca80e9d2eddaa82342370f11b3a5bf9..09c0ab83797cff44dcaea7d193e408a362e94f10 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/index.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/index.js @@ -1,3 +1,5 @@ -import Cyclelist from './list'; +import CycleList from './list'; +import {CycleCreate} from './create'; +import CycleView from './view'; -export {Cyclelist}; \ No newline at end of file +export {CycleList, CycleCreate, CycleView}; 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 7d243c21e43a16d740c22ee350ec45cc90d1030f..55dcecbd82c16b036fe00205e82e9ccb28802b41 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/list.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/list.js @@ -91,7 +91,7 @@ class CycleList extends Component{ const promises = [CycleService.getCycleQuota(), CycleService.getResources()] Promise.all(promises).then(responses => { const cycleQuota = responses[0]; - this.setState({ resources: responses[1].data.results }); + this.setState({ resources: responses[1] }); CycleService.getAllCycles().then(cyclelist => { this.getCycles(cyclelist, cycleQuota) }); diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/list.test.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/list.test.js index 8ce4fe1b1c06133cb25a5d07a1cbf26b6a7b69b4..36858e8cacfeeae8a71778ffac061b4f7dea55b1 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/list.test.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/list.test.js @@ -4,14 +4,14 @@ import { MemoryRouter } from 'react-router-dom'; import { render, fireEvent } from '@testing-library/react'; import CycleList from './list'; import UnitConversion from '../../utils/unit.converter'; -import mockData from '../../__mocks__/cycle.service.data'; +import CycleServiceMock from '../../__mocks__/cycle.service.data'; jest.mock('../../services/cycle.service', () => { return { - getProjects: () => Promise.resolve({ data: mockData.getProjects }), - getCycleQuota: () => Promise.resolve({ data: mockData.getCycleQuota }), - getAllCycles: () => Promise.resolve(mockData.getAllCycle.results ), - getResources: () => Promise.resolve({ data: mockData.getresources }) + getProjects: () => Promise.resolve({ data: CycleServiceMock.getProjects }), + getCycleQuota: () => Promise.resolve({ data: CycleServiceMock.getCycleQuota }), + getAllCycles: () => Promise.resolve(CycleServiceMock.getAllCycle.results ), + getResources: () => Promise.resolve({ data: CycleServiceMock.getresources }) } }); @@ -45,14 +45,14 @@ describe('<CycleList />', () => { test('render observing time in hours', async () => { const { container } = render(<MemoryRouter><CycleList /></MemoryRouter>); await flushPromises(); - const observing_time = Math.floor(Number(mockData.getCycleQuota.results[0].value) / 3600); + const observing_time = Math.floor(Number(CycleServiceMock.getCycleQuota.results[0].value) / 3600); expect(container.querySelectorAll('tr')[1].innerHTML.includes(observing_time)).toBeTruthy(); }); test('render commissioning time in hours', async () => { const { container } = render(<MemoryRouter><CycleList /></MemoryRouter>); await flushPromises(); - const commissioning_time = UnitConversion.getUIResourceUnit('bytes',Number(mockData.getCycleQuota.results[1].value)); + const commissioning_time = UnitConversion.getUIResourceUnit('bytes',Number(CycleServiceMock.getCycleQuota.results[1].value)); expect(container.querySelectorAll('tr')[1].innerHTML.includes(commissioning_time)).toBeTruthy(); }); diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js index b1907ea2737c343f5783ea660847f7d31f6afefb..f6fc99eb3ad6877bbff4216e8787d7e7a10882c8 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js @@ -11,8 +11,7 @@ import {Dashboard} from './Dashboard'; import {Scheduling} from './Scheduling'; import {TaskEdit, TaskView} from './Task'; import ViewSchedulingUnit from './Scheduling/ViewSchedulingUnit' -import CycleList from './Cycle/list'; -import CycleView from './Cycle/view'; +import { CycleCreate, CycleList, CycleView } from './Cycle'; export const routes = [ { @@ -66,6 +65,10 @@ export const routes = [ path: "/project/edit/:id", component: ProjectEdit, name: 'Project Edit' + },{ + path: "/cycle/create", + component: CycleCreate, + name: 'Cycle Add' }, { path: "/cycle", 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 722f46262f8d16cf3d3f8eccda6266027b67432c..1d01b7b6aa8825eb18da72c52592f5778cb68913 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/services/cycle.service.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/services/cycle.service.js @@ -65,7 +65,38 @@ const CycleService = { return res; }, - + getResources: async function() { + try { + const url = `/api/resource_type`; + const response = await axios.get(url); + return response.data.results; + } catch (error) { + console.error('[cycle.services.getResources]',error); + } + }, + saveCycle: async function(cycle, cycleQuota) { + try { + const response = await axios.post(('/api/cycle/'), cycle); + cycle = response.data + for (let quota of cycleQuota) { + quota.cycle = cycle.url; + this.saveCycleQuota(quota); + } + return response.data; + } catch (error) { + console.log(error.response.data); + return error.response.data; + } + }, + saveCycleQuota: async function(cycleQuota) { + try { + const response = await axios.post(('/api/cycle_quota/'),cycleQuota); + return response.data; + } catch (error) { + console.error(error); + return null; + } + }, } export default CycleService; 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/')