diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..772182382fcc07c3b3b57de4515e7fbde30dc0ef Binary files /dev/null and b/.DS_Store differ diff --git a/Docker/lofar-ci/Dockerfile_ci_sas b/Docker/lofar-ci/Dockerfile_ci_sas index 8aafc26e5fd0455ee7d12d1e6fd83d461aa42c7e..527639e256c50c98b1ef0550b41a7cbf69b3e1c3 100644 --- a/Docker/lofar-ci/Dockerfile_ci_sas +++ b/Docker/lofar-ci/Dockerfile_ci_sas @@ -18,6 +18,9 @@ ENV PATH /usr/pgsql-9.6/bin:$PATH RUN pip3 install cython kombu lxml requests pygcn xmljson mysql-connector-python python-dateutil Django==3.0.9 djangorestframework==3.11.1 djangorestframework-xml ldap==1.0.2 flask fabric coverage python-qpid-proton PyGreSQL numpy h5py psycopg2 testing.postgresql Flask-Testing scipy Markdown django-filter python-ldap python-ldap-test ldap3 django-jsonforms django-json-widget django-jsoneditor drf-yasg flex swagger-spec-validator django-auth-ldap mozilla-django-oidc jsonschema comet pyxb==1.2.5 graphviz isodate astropy packaging +#Viewflow package +RUN pip3 install django-material django-viewflow + # Note: nodejs now comes with npm, do not install the npm package separately, since that will be taken from the epel repo and is conflicting. RUN echo "Installing Nodejs packages..." && \ curl -sL https://rpm.nodesource.com/setup_14.x | bash - && \ diff --git a/SAS/TMSS/frontend/tmss_webapp/package.json b/SAS/TMSS/frontend/tmss_webapp/package.json index 16d7fc74e69a967e758af3cb2f53b81557403897..0e86725052116a227147adefafb33d766db1d724 100644 --- a/SAS/TMSS/frontend/tmss_webapp/package.json +++ b/SAS/TMSS/frontend/tmss_webapp/package.json @@ -49,7 +49,7 @@ "test": "react-scripts test", "eject": "react-scripts eject" }, - "proxy": "http://192.168.99.100:8008/", + "proxy": "http://127.0.0.1:8008/", "eslintConfig": { "extends": "react-app" }, diff --git a/SAS/TMSS/frontend/tmss_webapp/src/App.js b/SAS/TMSS/frontend/tmss_webapp/src/App.js index 42705f27eab18c5738b2b4bd74a0c43686f2db7c..28dc4939d93ef1dcc6a112bb1da40a4a8fa79067 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/App.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/App.js @@ -24,9 +24,9 @@ class App extends Component { currentMenu: '', currentPath: '/', PageTitle:'', - staticMenuInactive: false, - overlayMenuActive: false, - mobileMenuActive: false, + staticMenuInactive: localStorage.getItem('staticMenuInactive') === 'true' ? true : false, + overlayMenuActive: localStorage.getItem('overlayMenuActive') === 'true' ? true : false, + mobileMenuActive: localStorage.getItem('mobileMenuActive') === 'true' ? true : false, }; this.onWrapperClick = this.onWrapperClick.bind(this); this.onToggleMenu = this.onToggleMenu.bind(this); @@ -35,11 +35,11 @@ class App extends Component { this.setPageTitle = this.setPageTitle.bind(this); this.menu = [ - {label: 'Dashboard', icon: 'pi pi-fw pi-home', to:'/dashboard'}, - {label: 'Cycle', icon:'pi pi-fw pi-spinner', to:'/cycle'}, - {label: 'Project', icon: 'fab fa-fw fa-wpexplorer', to:'/project'}, - {label: 'Scheduling Units', icon: 'pi pi-fw pi-calendar', to:'/schedulingunit'}, - {label: 'Timeline', icon: 'pi pi-fw pi-clock', to:'/schedulingunit/timelineview'}, + {label: 'Dashboard', icon: 'pi pi-fw pi-home', to:'/dashboard',section: 'dashboard'}, + {label: 'Cycle', icon:'pi pi-fw pi-spinner', to:'/cycle',section: 'cycle'}, + {label: 'Project', icon: 'fab fa-fw fa-wpexplorer', to:'/project',section: 'project'}, + {label: 'Scheduling Units', icon: 'pi pi-fw pi-calendar', to:'/schedulingunit',section: 'schedulingunit'}, + {label: 'Timeline', icon: 'pi pi-fw pi-clock', to:'/su/timelineview',section: 'su/timelineview'}, // {label: 'Tasks', icon: 'pi pi-fw pi-check-square', to:'/task'}, @@ -65,11 +65,16 @@ class App extends Component { if (this.state.layoutMode === 'overlay') { this.setState({ overlayMenuActive: !this.state.overlayMenuActive - }); + }, () => { + localStorage.setItem('overlayMenuActive', this.state.overlayMenuActive); + } + ); } else if (this.state.layoutMode === 'static') { this.setState({ staticMenuInactive: !this.state.staticMenuInactive + }, () => { + localStorage.setItem('staticMenuInactive', this.state.staticMenuInactive); }); } } @@ -77,7 +82,10 @@ class App extends Component { const mobileMenuActive = this.state.mobileMenuActive; this.setState({ mobileMenuActive: !mobileMenuActive - }); + },() => { + localStorage.setItem('mobileMenuActive', this.state.mobileMenuActive); + } + ); } event.preventDefault(); } @@ -101,24 +109,23 @@ class App extends Component { } render() { - const wrapperClass = classNames('layout-wrapper', { - 'layout-overlay': this.state.layoutMode === 'overlay', - 'layout-static': this.state.layoutMode === 'static', - 'layout-static-sidebar-inactive': this.state.staticMenuInactive && this.state.layoutMode === 'static', - 'layout-overlay-sidebar-active': this.state.overlayMenuActive && this.state.layoutMode === 'overlay', - 'layout-mobile-sidebar-active': this.state.mobileMenuActive - }); - const AppBreadCrumbWithRouter = withRouter(AppBreadcrumb); + const wrapperClass = classNames('layout-wrapper', { + 'layout-overlay': this.state.layoutMode === 'overlay', + 'layout-static': this.state.layoutMode === 'static', + 'layout-static-sidebar-inactive': this.state.staticMenuInactive && this.state.layoutMode === 'static', + 'layout-overlay-sidebar-active': this.state.overlayMenuActive && this.state.layoutMode === 'overlay', + 'layout-mobile-sidebar-active': this.state.mobileMenuActive + }); + const AppBreadCrumbWithRouter = withRouter(AppBreadcrumb); - - return ( + return ( <React.Fragment> <div className="App"> {/* <div className={wrapperClass} onClick={this.onWrapperClick}> */} <div className={wrapperClass}> <AppTopbar onToggleMenu={this.onToggleMenu}></AppTopbar> <Router basename={ this.state.currentPath }> - <AppMenu model={this.menu} onMenuItemClick={this.onMenuItemClick} /> + <AppMenu model={this.menu} onMenuItemClick={this.onMenuItemClick} layoutMode={this.state.la} active={this.state.menuActive}/> <div className="layout-main"> <AppBreadCrumbWithRouter setPageTitle={this.setPageTitle} /> <RoutedContent /> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/_overrides.scss b/SAS/TMSS/frontend/tmss_webapp/src/layout/_overrides.scss index 0fd0c0c45c82a891cf3f768fdaa0e1850baa130d..03b42a5aeb25770e4dc82919a4ee85d33cec416d 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/_overrides.scss +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/_overrides.scss @@ -23,3 +23,31 @@ border-top: none; } } +.disable-action { + pointer-events: none; + opacity: 0.5; +} +.nav-btn { + position: absolute; + height: 100%; + width: 100%; + top: 0; + left: 0; + background-color: transparent !important; + border: 0 !important; + box-shadow: none !important; + :focus { + background-color: transparent !important; + border: 0 !important; + box-shadow: none !important; + } + :hover { + background-color: transparent !important; + border: 0 !important; + box-shadow: none !important; + } + span { + display: none !important; + } +} + diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/components/AppMenu.js b/SAS/TMSS/frontend/tmss_webapp/src/layout/components/AppMenu.js index c46698462cfea5aff95a48cbf167c56a0dfd736e..9c0760c8e1a6dcd90fccae8e80939a172dbfb521 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/components/AppMenu.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/components/AppMenu.js @@ -2,6 +2,7 @@ import {NavLink} from 'react-router-dom' import PropTypes from 'prop-types'; import classNames from 'classnames'; +import { Button } from 'primereact/button'; class AppSubmenu extends Component { @@ -51,18 +52,35 @@ class AppSubmenu extends Component { }); } } + + componentDidMount() { + if (!this.props.items) { + return; + } + const pathname = window.location.pathname; + console.log(pathname); + for (let i = 0; i < this.props.items.length; i++) { + + if (pathname.indexOf(this.props.items[i].section) > -1) { + this.setState({activeIndex: i}); + break + } + } + } renderLinkContent(item) { let submenuIcon = item.items && <i className="pi pi-fw pi-angle-down menuitem-toggle-icon"></i>; let badge = item.badge && <span className="menuitem-badge">{item.badge}</span>; - + return ( <React.Fragment> - <i className={item.icon}></i> - <span>{item.label}</span> - {submenuIcon} - {badge} + <i className={item.icon}></i> + <Button className="nav-btn nav-btn-tooltip" tooltip={item.label}></Button> + <Button className="nav-btn nav-btn-notooltip"></Button> + <span>{item.label}</span> + {submenuIcon} + {badge} </React.Fragment> ); } diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/components/PageHeader.js b/SAS/TMSS/frontend/tmss_webapp/src/layout/components/PageHeader.js index 0c7274c3471c957a2b2d961eade605d8a444d1bd..a1ccb421b626562153020c49fab00e83c8c4db46 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/components/PageHeader.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/components/PageHeader.js @@ -51,7 +51,7 @@ export default ({ title, subTitle, actions, ...props}) => { ); } else { return ( - <Link to={{ ...action.props }} title={action.title || ''} onClick={() => onClickLink(action)}> + <Link className={action.classname} to={{ ...action.props }} title={action.title || ''} onClick={() => onClickLink(action)}> <i className={`fa ${action.icon}`}></i> </Link> ); diff --git a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_menu.scss b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_menu.scss index 9fea8e94f390367c23a1874b1d0e19060a43e5b5..6942fe6836f93ff1cb4b2b285f8136ff2c3189a2 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_menu.scss +++ b/SAS/TMSS/frontend/tmss_webapp/src/layout/sass/_menu.scss @@ -181,4 +181,20 @@ } } } +} + +.layout-static .nav-btn-tooltip { + display: none; +} + +.layout-static .nav-btn-notooltip { + display: block; +} + +.layout-static-sidebar-inactive .nav-btn-tooltip { + display: block; +} + +.layout-static-sidebar-inactive .nav-btn-notooltip { + display: none; } \ 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 index 29568c138c88fc43d4cd37e7936f5b4a0b813db9..8e11645ca22f57e71bf20c836ac45bf76423c39a 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/create.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/create.js @@ -339,7 +339,9 @@ export class CycleCreate extends Component { </div> </div> */ } - <PageHeader location={this.props.location} title={'Cycle - Add'} actions={[{icon:'fa-window-close',title:'Click to Close Add Cycle',props:{pathname: '/cycle' }}]}/> + <PageHeader location={this.props.location} title={'Cycle - Add'} actions={[{icon:'fa-window-close', + title:'Click to Close Add Cycle', + props:{pathname: '/cycle' }}]}/> { this.state.isLoading ? <AppLoader /> : <> <div> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/edit.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/edit.js index f0c8a30a14cf449356b2f4ae8872517d49e9a93e..d04ac553d56581a384a458044384e5b64a349845 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/edit.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/edit.js @@ -352,7 +352,7 @@ export class CycleEdit extends Component { * Cancel edit and redirect to Cycle View page */ cancelEdit() { - this.setState({redirect: `/cycle/view/${this.state.cycle.name}`}); + this.props.history.goBack(); } render() { @@ -373,7 +373,9 @@ export class CycleEdit extends Component { </Link> </div> </div> */} - <PageHeader location={this.props.location} title={'Cycle - Edit'} actions={[{icon:'fa-window-close',title:'Click to Close Cycle-Edit', props:{ pathname: `/cycle/view/${this.state.cycle.name}`}}]}/> + <PageHeader location={this.props.location} title={'Cycle - Edit'} actions={[{icon:'fa-window-close', + link: this.props.history.goBack,title:'Click to Close Cycle-Edit', + props:{ pathname: `/cycle/view/${this.state.cycle.name}`}}]}/> { this.state.isLoading ? <AppLoader/> : <> 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 8ef0b80b71ff458e74b46ea862067edcdae6054c..4ce2ebeb1cdfd4cfa707af03503f99c7fdcbcee6 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/list.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/list.js @@ -52,7 +52,6 @@ class CycleList extends Component{ this.defaultSortColumn = [{id: "Cycle Code", desc: false}]; } - getUnitConvertedQuotaValue(cycle, cycleQuota, resourceName) { const quota = _.find(cycleQuota, {'cycle_id': cycle.name, 'resource_type_id': resourceName}); const unitQuantity = this.state.resources.find(i => i.name === resourceName).quantity_value; diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/view.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/view.js index 34eef2047443a069cb60beecfe610a022de8c901..2d2b1cc5c039afd5b4759bb0391a6d2094825f4d 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/view.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/view.js @@ -97,7 +97,7 @@ export class CycleView extends Component { <PageHeader location={this.props.location} title={'Cycle - Details'} actions={[ {icon:'fa-edit', title:'Click to Edit Cycle', props:{ pathname: `/cycle/edit/${this.state.cycle.name}`, state: {id: this.state.cycle?this.state.cycle.name:''}}}, - {icon: 'fa-window-close',props:{ pathname: `/cycle`}}]}/> + {icon: 'fa-window-close',link: this.props.history.goBack}]}/> { this.state.isLoading && <AppLoader /> } { this.state.cycle && <React.Fragment> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.js index 38fb8b0813bd8adf07687769ea221ec3d1514914..b6dd8fadf65b41523c301e7845f09991132242dc 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.js @@ -320,7 +320,7 @@ export class ProjectCreate extends Component { * Function to cancel form creation and navigate to other page/component */ cancelCreate() { - this.setState({redirect: '/project'}); + this.props.history.goBack(); } /** @@ -373,7 +373,7 @@ export class ProjectCreate extends Component { return ( <React.Fragment> <Growl ref={(el) => this.growl = el} /> - <PageHeader location={this.props.location} title={'Project - Add'} actions={[{icon:'fa-window-close',title:'Click to Close Project', props:{ pathname: '/project'}}]}/> + <PageHeader location={this.props.location} title={'Project - Add'} actions={[{icon:'fa-window-close',link:this.props.history.goBack, title:'Click to Close Project', props:{ pathname: '/project'}}]}/> { this.state.isLoading ? <AppLoader /> : <> <div> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/edit.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/edit.js index bf0a21d382e7be73d805b87a545f74b008007e77..8d4ec839e244c148a7b601c04b9af0603cc502e7 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/edit.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/edit.js @@ -382,7 +382,7 @@ export class ProjectEdit extends Component { * Cancel edit and redirect to Project View page */ cancelEdit() { - this.setState({redirect: `/project/view/${this.state.project.name}`}); + this.props.history.goBack(); } render() { @@ -393,7 +393,7 @@ export class ProjectEdit extends Component { return ( <React.Fragment> <Growl ref={(el) => this.growl = el} /> - <PageHeader location={this.props.location} title={'Project - Edit'} actions={[{icon:'fa-window-close',title:'Click to Close Project Edit Page', props : { pathname: `/project/view/${this.state.project.name}`}}]}/> + <PageHeader location={this.props.location} title={'Project - Edit'} actions={[{icon:'fa-window-close',link: this.props.history.goBack,title:'Click to Close Project Edit Page', props : { pathname: `/project/view/${this.state.project.name}`}}]}/> { this.state.isLoading ? <AppLoader/> : <> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/view.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/view.js index 9f1c5e83cdd4e748a335f7bf4735c68fb004be41..0cb42a98add0d439122d25ecf637921ec73fc6c2 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/view.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/view.js @@ -114,13 +114,12 @@ export class ProjectView extends Component { <TieredMenu model={this.menuOptions} popup ref={el => this.optionsMenu = el} /> <PageHeader location={this.props.location} title={'Project - View'} actions={[ {icon:'fa-bars',title: '', type:'button', - actOn:'mouseOver', props : { callback: this.showOptionMenu}}, + actOn:'mouseOver', props : { callback: this.showOptionMenu}, + }, {icon: 'fa-edit',title:'Click to Edit Project', type:'link', props : { pathname: `/project/edit/${this.state.project.name}`, state: {id: this.state.project?this.state.project.name:''&& this.state.project}}}, - {icon:'fa-window-close',title: 'Click to Close Project View', type:'link', - props : { pathname: `/project`}}, - ]}/> + {icon:'fa-window-close',title: 'Click to Close Project View', link: this.props.history.goBack}]}/> { this.state.isLoading && <AppLoader /> } { this.state.project && <React.Fragment> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/SchedulingUnitList.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/SchedulingUnitList.js index 229aae4c091a8d2943b1780c2ad0b01090f58652..6cec1f3d60c3f628370e79557f3566549969dc22 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/SchedulingUnitList.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/SchedulingUnitList.js @@ -59,7 +59,7 @@ class SchedulingUnitList extends Component{ blueprintdata.map(blueP => { blueP.duration = moment.utc(blueP.duration*1000).format('HH:mm:ss'); blueP.type="Blueprint"; - blueP['actionpath'] = '/schedulingunit/view/blueprint/'+blueP.id; + blueP['actionpath'] ='/schedulingunit/view/blueprint/'+blueP.id; return blueP; }); output.push(...blueprintdata); diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/ViewSchedulingUnit.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/ViewSchedulingUnit.js index 23a829a506f8f89f56a9af3aee8319cf1ae2c9ee..c57445b61784420eceb8a9f538d5c113fea84909 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/ViewSchedulingUnit.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/ViewSchedulingUnit.js @@ -52,6 +52,15 @@ class ViewSchedulingUnit extends Component{ "Relative End Time (HH:mm:ss)": "filter-input-75", }] } + this.actions = [ + {icon: 'fa-window-close',title:'Click to Close Scheduling Unit View', link: this.props.history.goBack} + ]; + if (this.props.match.params.type === 'draft') { + this.actions.unshift({icon: 'fa-edit', title: 'Click to edit', props : { pathname:`/schedulingunit/edit/${ this.props.match.params.id}` + } }); + } else { + this.actions.unshift({icon: 'fa-lock', title: 'Cannot edit blueprint'}); + } if (this.props.match.params.id) { this.state.scheduleunitId = this.props.match.params.id; } @@ -120,11 +129,10 @@ class ViewSchedulingUnit extends Component{ <i className="fa fa-edit" style={{marginTop: "10px"}}></i> </Link> </div> - </div> */} + </div> */ + /*TMSS-363 Blueprint icon changes */} <PageHeader location={this.props.location} title={'Scheduling Unit - Details'} - actions={[{icon: 'fa-edit',title:'Click to Edit Scheduling Unit View', type:'link', - props : { pathname: `/schedulingunit/edit/${this.props.match.params.id}` }}, - {icon: 'fa-window-close',title:'Click to Close Scheduling Unit View', props : { pathname: '/schedulingunit'}}]}/> + actions={this.actions}/> { this.state.isLoading ? <AppLoader/> :this.state.scheduleunit && <> <div className="main-content"> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/create.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/create.js index 5aafacbe4360cb71c87c3741b62f5d6034aee552..c10c070b09658bd0874a3322a85c5172ae3690be 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/create.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/create.js @@ -262,7 +262,7 @@ export class SchedulingUnitCreate extends Component { * Cancel SU creation and redirect */ cancelCreate() { - this.setState({redirect: '/schedulingunit'}) + this.props.history.goBack(); } /** @@ -316,7 +316,7 @@ export class SchedulingUnitCreate extends Component { </div> <div className="p-col-2 p-lg-2 p-md-2"> <Link to={{ pathname: '/schedulingunit'}} tite="Close" style={{float: "right"}}> - <i className="fa fa-window-close" style={{marginTop: "10px"}}></i> + <i className="fa fa-window-close" link={this.props.history.goBack()} style={{marginTop: "10px"}}></i> </Link> </div> </div> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/edit.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/edit.js index 8ba417b69773952d7cd64e70c470e581aad19f8a..b333f66d341c9d18a5b4207ffcbdcc2fa5fda4ff 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/edit.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/edit.js @@ -269,7 +269,7 @@ export class EditSchedulingUnit extends Component { * Cancel SU creation and redirect */ cancelCreate() { - this.setState({redirect: `/schedulingunit/view/draft/${this.props.match.params.id}`}) + this.props.history.goBack(); } render() { @@ -292,7 +292,7 @@ export class EditSchedulingUnit extends Component { <React.Fragment> <Growl ref={el => (this.growl = el)} /> <PageHeader location={this.props.location} title={'Scheduling Unit - Edit'} - actions={[{icon: 'fa-window-close',title:'Click to Close Scheduling Unit View', props : { pathname: `/schedulingunit/view/draft/${this.props.match.params.id}`}}]}/> + actions={[{icon: 'fa-window-close',link: this.props.history.goBack,title:'Click to Close Scheduling Unit View', props : { pathname: `/schedulingunit/view/draft/${this.props.match.params.id}`}}]}/> { this.state.isLoading ? <AppLoader /> : <> <div> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/edit.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/edit.js index e27a10133358176a42bc1e5fd38154c1eee6fff9..19272db95bfe5ca90f4ba45105a1cc0689155583 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/edit.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/edit.js @@ -139,7 +139,7 @@ export class TaskEdit extends Component { } cancelEdit() { - this.setState({redirect: '/task/view/draft/' + this.state.task.id}); + this.props.history.goBack(); } componentDidMount() { @@ -202,7 +202,7 @@ export class TaskEdit extends Component { </Link> </div> </div> */} - <PageHeader location={this.props.location} title={'Task - Edit'} actions={[{icon: 'fa-window-close',title:'Click to Close Task Edit Page' ,props : { pathname: `/task/view/draft/${this.state.task?this.state.task.id:''}`}}]}/> + <PageHeader location={this.props.location} title={'Task - Edit'} actions={[{icon: 'fa-window-close',link: this.props.history.goBack,title:'Click to Close Task Edit Page' ,props : { pathname: `/task/view/draft/${this.state.task?this.state.task.id:''}`}}]}/> {isLoading ? <AppLoader/> : <div> <div className="p-fluid"> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/view.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/view.js index f5b80baaea6d1a8cde5731dc682ff3b7acdd27f9..f90a3db7820620c7ef72ae2796000a2f4ac76bd4 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/view.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/view.js @@ -124,6 +124,7 @@ export class TaskView extends Component { } }, { icon: 'fa-window-close', + link: this.props.history.goBack, title:'Click to Close Task', props : { pathname:'/task' }}]; } else { diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js index 9a017983a09b19a84e4b4f5e4e17620bb2176eca..38db5418a30031416ab3cd8770b326494ce49d2c 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/index.js @@ -128,7 +128,7 @@ export const routes = [ title:'Cycle-List' }, { - path: "/schedulingunit/timelineview", + path: "/su/timelineview", component: TimelineView, name: 'Scheduling Unit Timeline', title:'SU Timeline View' diff --git a/SAS/TMSS/src/tmss/settings.py b/SAS/TMSS/src/tmss/settings.py index db1f35dc924fac94922e19dfdd50268214daf5bb..3fcb6ea5e997dfabaa0357e8d62c9da6b4a54cac 100644 --- a/SAS/TMSS/src/tmss/settings.py +++ b/SAS/TMSS/src/tmss/settings.py @@ -116,6 +116,10 @@ INSTALLED_APPS = [ 'jsoneditor', 'drf_yasg', 'django_filters', + 'material', + 'material.frontend', + 'viewflow', + 'viewflow.frontend', ] MIDDLEWARE = [ diff --git a/SAS/TMSS/src/tmss/tmssapp/CMakeLists.txt b/SAS/TMSS/src/tmss/tmssapp/CMakeLists.txt index 311d21b6b56d1a4fa3b7b97021d0cde8ccd5d04e..e24af6998d0ad9240a454cd41fdb389a38cb4208 100644 --- a/SAS/TMSS/src/tmss/tmssapp/CMakeLists.txt +++ b/SAS/TMSS/src/tmss/tmssapp/CMakeLists.txt @@ -23,4 +23,5 @@ add_subdirectory(serializers) add_subdirectory(viewsets) add_subdirectory(adapters) add_subdirectory(schemas) +add_subdirectory(workflows) diff --git a/SAS/TMSS/src/tmss/tmssapp/migrations/0001_initial.py b/SAS/TMSS/src/tmss/tmssapp/migrations/0001_initial.py index 47a22d7eda99a988620bcc8f6b05919da46d5bd8..daa63f9369488f5e160485fbfec01af9cdb5121b 100644 --- a/SAS/TMSS/src/tmss/tmssapp/migrations/0001_initial.py +++ b/SAS/TMSS/src/tmss/tmssapp/migrations/0001_initial.py @@ -14,6 +14,7 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('viewflow', '0008_jsonfield_and_artifact'), ] operations = [ @@ -491,6 +492,26 @@ class Migration(migrations.Migration): 'abstract': False, }, ), + migrations.CreateModel( + name='SchedulingUnitDemo', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50)), + ('state', models.IntegerField()), + ], + ), + migrations.CreateModel( + name='SchedulingUnitDemoProcess', + fields=[ + ('process_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='viewflow.Process')), + ('text', models.CharField(max_length=150)), + ('approved', models.BooleanField(default=False)), + ], + options={ + 'abstract': False, + }, + bases=('viewflow.process',), + ), migrations.CreateModel( name='SchedulingUnitDraft', fields=[ @@ -743,6 +764,19 @@ class Migration(migrations.Migration): 'abstract': False, }, ), + migrations.CreateModel( + name='HelloWorldProcess', + fields=[ + ], + options={ + 'verbose_name': 'World Request', + 'verbose_name_plural': 'World Requests', + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('viewflow.process',), + ), migrations.CreateModel( name='Setting', fields=[ @@ -1059,6 +1093,11 @@ class Migration(migrations.Migration): name='scheduling_set', field=models.ForeignKey(help_text='Set to which this scheduling unit draft belongs.', on_delete=django.db.models.deletion.CASCADE, related_name='scheduling_unit_drafts', to='tmssapp.SchedulingSet'), ), + migrations.AddField( + model_name='schedulingunitdemoprocess', + name='su', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='tmssapp.SchedulingUnitDemo'), + ), migrations.AddField( model_name='schedulingunitblueprint', name='draft', diff --git a/SAS/TMSS/src/tmss/tmssapp/models/CMakeLists.txt b/SAS/TMSS/src/tmss/tmssapp/models/CMakeLists.txt index 7598bc12c79161c19b95275e001a28adb92d3b56..2ac64b115ecf2f4bc700c614a3ba9572f3af6aa6 100644 --- a/SAS/TMSS/src/tmss/tmssapp/models/CMakeLists.txt +++ b/SAS/TMSS/src/tmss/tmssapp/models/CMakeLists.txt @@ -5,6 +5,8 @@ set(_py_files __init__.py specification.py scheduling.py + helloworldflow.py + schedulingunitdemoflow.py ) python_install(${_py_files} diff --git a/SAS/TMSS/src/tmss/tmssapp/models/__init__.py b/SAS/TMSS/src/tmss/tmssapp/models/__init__.py index 93f3c7e6d54f95c40d6d9484aad802b13f9991ba..be7a174d740d60b255c47117cb8abfc657cc9bde 100644 --- a/SAS/TMSS/src/tmss/tmssapp/models/__init__.py +++ b/SAS/TMSS/src/tmss/tmssapp/models/__init__.py @@ -1,2 +1,4 @@ from .specification import * -from .scheduling import * \ No newline at end of file +from .scheduling import * +from .helloworldflow import * +from .schedulingunitdemoflow import * \ No newline at end of file diff --git a/SAS/TMSS/src/tmss/tmssapp/models/helloworldflow.py b/SAS/TMSS/src/tmss/tmssapp/models/helloworldflow.py new file mode 100644 index 0000000000000000000000000000000000000000..d92015dba2a5865f080c8a86b0bef28bd15e53ee --- /dev/null +++ b/SAS/TMSS/src/tmss/tmssapp/models/helloworldflow.py @@ -0,0 +1,13 @@ +import jsonstore +from viewflow.models import Process +from viewflow.compat import _ + + +class HelloWorldProcess(Process): + text = jsonstore.CharField(_('Message'), max_length=50) + approved = jsonstore.BooleanField(_('Approved'), default=False) + + class Meta: + proxy = True + verbose_name = _("World Request") + verbose_name_plural = _('World Requests') \ No newline at end of file diff --git a/SAS/TMSS/src/tmss/tmssapp/models/schedulingunitdemoflow.py b/SAS/TMSS/src/tmss/tmssapp/models/schedulingunitdemoflow.py new file mode 100644 index 0000000000000000000000000000000000000000..b9797a0b12e56ffb6f284da503f43263561522c4 --- /dev/null +++ b/SAS/TMSS/src/tmss/tmssapp/models/schedulingunitdemoflow.py @@ -0,0 +1,13 @@ +# Create your models here. + +from django.db.models import CharField, IntegerField,BooleanField, ForeignKey, CASCADE, Model +from viewflow.models import Process + +class SchedulingUnitDemo(Model): + name = CharField(max_length=50) + state = IntegerField() + +class SchedulingUnitDemoProcess(Process): + text = CharField(max_length=150) + approved = BooleanField(default=False) + su = ForeignKey(SchedulingUnitDemo, blank=True, null=True, on_delete=CASCADE) diff --git a/SAS/TMSS/src/tmss/tmssapp/viewsets/CMakeLists.txt b/SAS/TMSS/src/tmss/tmssapp/viewsets/CMakeLists.txt index fc0325a523508e371b2456d96b3467274dae748d..445e0bbe4672e5cdad3a5a41be8575dbf2169ff0 100644 --- a/SAS/TMSS/src/tmss/tmssapp/viewsets/CMakeLists.txt +++ b/SAS/TMSS/src/tmss/tmssapp/viewsets/CMakeLists.txt @@ -6,6 +6,8 @@ set(_py_files lofar_viewset.py specification.py scheduling.py + helloworldflow.py + schedulingunitdemoflow.py ) python_install(${_py_files} diff --git a/SAS/TMSS/src/tmss/tmssapp/viewsets/__init__.py b/SAS/TMSS/src/tmss/tmssapp/viewsets/__init__.py index 93f3c7e6d54f95c40d6d9484aad802b13f9991ba..882458975ee4be50507620471ed1026433ddf589 100644 --- a/SAS/TMSS/src/tmss/tmssapp/viewsets/__init__.py +++ b/SAS/TMSS/src/tmss/tmssapp/viewsets/__init__.py @@ -1,2 +1,3 @@ from .specification import * -from .scheduling import * \ No newline at end of file +from .scheduling import * +from .schedulingunitdemoflow import * \ No newline at end of file diff --git a/SAS/TMSS/src/tmss/tmssapp/viewsets/helloworldflow.py b/SAS/TMSS/src/tmss/tmssapp/viewsets/helloworldflow.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/SAS/TMSS/src/tmss/tmssapp/viewsets/schedulingunitdemoflow.py b/SAS/TMSS/src/tmss/tmssapp/viewsets/schedulingunitdemoflow.py new file mode 100644 index 0000000000000000000000000000000000000000..ea117c0f9c27a4324fe76c77fe1256e1b1eca446 --- /dev/null +++ b/SAS/TMSS/src/tmss/tmssapp/viewsets/schedulingunitdemoflow.py @@ -0,0 +1,22 @@ +from django.shortcuts import render +from rest_framework import viewsets +from rest_framework.response import Response +from rest_framework.decorators import action +from rest_framework.serializers import ModelSerializer +from lofar.sas.tmss.tmss.tmssapp import models + +# Create your views here. + +class SchedulingUnitDemoSerializer(ModelSerializer): + class Meta: + model = models.SchedulingUnitDemo + fields = '__all__' + +class SchedulingUnitFlowViewSet(viewsets.ModelViewSet): + queryset = models.SchedulingUnitDemo.objects.all() + serializer_class = SchedulingUnitDemoSerializer + + @action(methods=['get'], detail=True) + def trigger(self, request, pk=None): + SchedulingUnitDemoFlow + return Response("ok") \ No newline at end of file diff --git a/SAS/TMSS/src/tmss/tmssapp/workflows/CMakeLists.txt b/SAS/TMSS/src/tmss/tmssapp/workflows/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..474aada33041160e598ac2b1a126d68971d75afd --- /dev/null +++ b/SAS/TMSS/src/tmss/tmssapp/workflows/CMakeLists.txt @@ -0,0 +1,11 @@ + +include(PythonInstall) + +set(_py_files + __init__.py + helloworldflow.py + schedulingunitdemoflow.py + ) + +python_install(${_py_files} + DESTINATION lofar/sas/tmss/tmss/tmssapp/workflows) diff --git a/SAS/TMSS/src/tmss/tmssapp/workflows/__init__.py b/SAS/TMSS/src/tmss/tmssapp/workflows/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..45516795a25730483ebfa40c1fbdb5f533df8ebe --- /dev/null +++ b/SAS/TMSS/src/tmss/tmssapp/workflows/__init__.py @@ -0,0 +1,2 @@ +from .helloworldflow import * +from .schedulingunitdemoflow import * \ No newline at end of file diff --git a/SAS/TMSS/src/tmss/tmssapp/workflows/helloworldflow.py b/SAS/TMSS/src/tmss/tmssapp/workflows/helloworldflow.py new file mode 100644 index 0000000000000000000000000000000000000000..d3307efe5f773359de58c89bea4a8728fa809c05 --- /dev/null +++ b/SAS/TMSS/src/tmss/tmssapp/workflows/helloworldflow.py @@ -0,0 +1,69 @@ +import os + +from viewflow import flow, frontend, lock +from viewflow.base import this, Flow +from viewflow.compat import _ +from viewflow.flow import views as flow_views + + +from lofar.sas.tmss.tmss.tmssapp import models + + + +@frontend.register +class HelloWorldFlow(Flow): + """ + Hello world + This process demonstrates hello world approval request flow. + """ + process_class = models.HelloWorldProcess + process_title = _('Hello world') + process_description = _('This process demonstrates hello world approval request flow.') + + lock_impl = lock.select_for_update_lock + + summary_template = _("'{{ process.text }}' message to the world") + + start = ( + flow.Start( + flow_views.CreateProcessView, + fields=['text'], + task_title=_('New message')) + .Permission(auto_create=True) + .Next(this.approve) + ) + + approve = ( + flow.View( + flow_views.UpdateProcessView, fields=['approved'], + task_title=_('Approve'), + task_description=_("{{ process.text }} approvement required"), + task_result_summary=_("Messsage was {{ process.approved|yesno:'Approved,Rejected' }}")) + .Permission(auto_create=True) + .Next(this.check_approve) + ) + + check_approve = ( + flow.If( + cond=lambda act: act.process.approved, + task_title=_('Approvement check'), + ) + .Then(this.send) + .Else(this.end) + ) + + send = ( + flow.Handler( + this.send_hello_world_request, + task_title=_('Send message'), + ) + .Next(this.end) + ) + + end = flow.End( + task_title=_('End'), + ) + + def send_hello_world_request(self, activation): + with open(os.devnull, "w") as world: + world.write(activation.process.text) \ No newline at end of file diff --git a/SAS/TMSS/src/tmss/tmssapp/workflows/schedulingunitdemoflow.py b/SAS/TMSS/src/tmss/tmssapp/workflows/schedulingunitdemoflow.py new file mode 100644 index 0000000000000000000000000000000000000000..a35c72db8e9430b929e9ada4f424bbf6a58527c9 --- /dev/null +++ b/SAS/TMSS/src/tmss/tmssapp/workflows/schedulingunitdemoflow.py @@ -0,0 +1,187 @@ +from django.utils.decorators import method_decorator +from django.db.models.signals import post_save +from viewflow import flow +from viewflow.models import Task +from viewflow.base import this, Flow +from viewflow.flow.views import CreateProcessView, UpdateProcessView, AssignTaskView +from viewflow.activation import FuncActivation, ViewActivation +from viewflow.flow.nodes import Signal +from viewflow import mixins + +from lofar.sas.tmss.tmss.tmssapp import models + +from viewflow import frontend, ThisObject +from viewflow.activation import STATUS + +class ConditionActivation(FuncActivation): + @classmethod + def activate(cls, flow_task, prev_activation, token): + activation = super(ConditionActivation, cls).activate(flow_task, prev_activation, token) + + if flow_task.condition_check(activation, None): + # condition holds on activation + activation.prepare() + activation.done() + + return activation + +class Condition(Signal): + #task_type = "HUMAN" # makes it show up in the unassigned task lists + activation_class = ConditionActivation + + def __init__(self, condition_check, signal, sender=None, task_loader=None, **kwargs): + """ + Instantiate a Signal task. + + :param signal: A django signal to connect + :param receiver: Callable[activation, **kwargs] + :param sender: Optional signal sender + :param task_loader: Callable[**kwargs] -> Task + :param allow_skip: If True task_loader can return None if + signal could be skipped. + + You can skip a `task_loader` if the signal going to be + sent with Task instance. + """ + self.condition_check = condition_check + + super(Condition, self).__init__(signal, self.signal_handler, sender, task_loader, **kwargs) + + @method_decorator(flow.flow_signal) + def signal_handler(self, activation, sender, instance, **signal_kwargs): + if activation.get_status() == STATUS.DONE: + # race condition -- condition was true on activation but we also receive the signal now + return + + activation.prepare() + if activation.flow_task.condition_check(activation, instance): + activation.done() + + def ready(self): + """Resolve internal `this`-references. and subscribe to the signal.""" + if isinstance(self.condition_check, ThisObject): + self.condition_check = getattr(self.flow_class.instance, self.condition_check.name) + + super(Condition, self).ready() + +@frontend.register +class SchedulingUnitDemoFlow(Flow): + process_class = models.SchedulingUnitDemoProcess + + # 0. Start on SU instantiation + # 1. To be Manually scheduled? -> Go to 1a + # 1a. Present view to manually schedule. + # 2. Wait on signal SU got finished/error/cancelled (might have already!!) -> + # - Wait for assignment to RO user + # View: - Present any quality plots + # - Present any error info + # - Present fixing options + # - Present choice to fix & redo, discard, or continue. + # Continue: + # View: - Present any quality plots + # - Present any error info + # - Submit quality report/score + # - Submit recommendation + # 3. - Assign ticket to Contact Author + # - Present quality plots to user + # - Present quality report/score, and recommendation + # - Submit acceptance & report + # 4. - Assign ticket to owner in step 2. + # - Present quality report/score, and recommendation + # - Present acceptance & report + # - Present choice to ingest or discard. + # Ingest: + # Set ingestable flag on SU. + # Discard: - Cancel SU (triggering garbage collection + # + # Fix & Redo: + # - Wait for user to confirm SU is fixed + # - Go to 2 + # + + # Consider adding to any/all views: + # - Present any opened JIRA tickets + # - Present opportunity to open JIRA ticket + # Note that previously submitted info can be found by clicking through the task. So + # we only need to show whats nominally needed. + # Note that orthogonally to the above flow: + # - Users need to be informed tasks are assigned to them (e-mail?) + # - Users already have an overview in viewflow of tickets assigned to them + # - We likely want to control what e-mails are sent. + + start = ( + flow.StartSignal( + post_save, + this.on_save_can_start, + sender=models.SchedulingUnitDemo + ).Next(this.wait_schedulable) + ) + + wait_schedulable = ( + Condition( + this.check_condition, + post_save, + sender=models.SchedulingUnitDemo, + task_loader=this.get_scheduling_unit_task + ) + .Next(this.form) + ) + + form = ( + flow.View( + UpdateProcessView, + fields=["text"] + ).Permission( + auto_create=True + ).Next(this.approve) + ) + + approve = ( + flow.View( + UpdateProcessView, + fields=["approved"] + ).Permission( + auto_create=True + ).Next(this.check_approve) + ) + + check_approve = ( + flow.If(lambda activation: activation.process.approved) + .Then(this.send) + .Else(this.end) + ) + + send = ( + flow.Handler( + this.send_hello_world_request + ).Next(this.end) + ) + + end = flow.End() + + @method_decorator(flow.flow_start_signal) + def on_save_can_start(self, activation, sender, instance, created, **signal_kwargs): + if created: + activation.prepare() + activation.process.su = instance + activation.done() + print("workflow started") + else: + print("no workflow started") + return activation + + def send_hello_world_request(self, activation): + print(activation.process.text) + + def check_condition(self, activation, instance): + if instance is None: + instance = activation.process.su + + condition = instance.state == 5 + print("condition is ",condition) + return condition + + def get_scheduling_unit_task(self, flow_task, sender, instance, **kwargs): + print(kwargs) + process = models.SchedulingUnitDemoProcess.objects.get(su=instance) + return Task.objects.get(process=process,flow_task=flow_task) diff --git a/SAS/TMSS/src/tmss/urls.py b/SAS/TMSS/src/tmss/urls.py index a0bc0c1d45236db1ef65ac43083d4ac1235a5ac9..37d9d081a3a3ecbee61e876ea3ee365b7c111ace 100644 --- a/SAS/TMSS/src/tmss/urls.py +++ b/SAS/TMSS/src/tmss/urls.py @@ -23,13 +23,16 @@ from django.views.generic.base import TemplateView, RedirectView from collections import OrderedDict from rest_framework import routers, permissions -from .tmssapp import viewsets, models, serializers, views +from .tmssapp import viewsets, models, serializers, views, workflows from rest_framework.documentation import include_docs_urls from drf_yasg.views import get_schema_view from drf_yasg import openapi from datetime import datetime +from material.frontend import urls as frontend_urls +from viewflow.flow.viewset import FlowViewSet + # # Django style patterns # @@ -197,6 +200,11 @@ frontend_urlpatterns = [ ] -# prefix everything for proxy -#urlpatterns = [url(r'^api/', include(urlpatterns)), url(r'^oidc/', include('mozilla_django_oidc.urls')),] -urlpatterns = [url(r'^api$', RedirectView.as_view(url='/api/')), url(r'^api/', include(urlpatterns)), url(r'^oidc$', RedirectView.as_view(url='/oidc/')), url(r'^oidc/', include('mozilla_django_oidc.urls')), url(r'^.*', include(frontend_urlpatterns)),] +urlpatterns = [url(r'^api$', RedirectView.as_view(url='/api/')), + url(r'^api/', include(urlpatterns)), url(r'^oidc$', + RedirectView.as_view(url='/oidc/')), + url(r'^oidc/', include('mozilla_django_oidc.urls')), + url(r'^workflow$', RedirectView.as_view(url='/workflow/', permanent=False)), + url(r'', include(frontend_urls)), + url(r'^.*', include(frontend_urlpatterns)), +]