diff options
| author | Xinhui | 2017-01-18 13:52:49 +0100 |
|---|---|---|
| committer | Xinhui | 2017-01-18 13:52:49 +0100 |
| commit | 2522ecae50da95a2fb73a49e96993361e7b7e217 (patch) | |
| tree | ef60ed49cc3102a5b85b30c0dcbde1884770afbe | |
| parent | 02a6b47c69df2b8364f85c205cc4381f250bd177 (diff) | |
| parent | 6ebc870abfe865b57c4df7a872f6a7a7cd71458e (diff) | |
| download | chouette-core-2522ecae50da95a2fb73a49e96993361e7b7e217.tar.bz2 | |
Merge branch 'master' into staging
54 files changed, 1076 insertions, 401 deletions
diff --git a/app/assets/images/loader.svg b/app/assets/images/loader.svg new file mode 100644 index 000000000..2d677858a --- /dev/null +++ b/app/assets/images/loader.svg @@ -0,0 +1,6 @@ +<svg version="1.1" id="svgLoader" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="40px" height="40px" viewBox="0 0 40 40" enable-background="new 0 0 40 40" xml:space="preserve"> + <path fill="#66b4e0" opacity="0.2" id="loaderC" d="M20.201,5.169c-8.254,0-14.946,6.692-14.946,14.946c0,8.255,6.692,14.946,14.946,14.946s14.946-6.691,14.946-14.946C35.146,11.861,28.455,5.169,20.201,5.169z M20.201,31.749c-6.425,0-11.634-5.208-11.634-11.634c0-6.425,5.209-11.634,11.634-11.634c6.425,0,11.633,5.209,11.633,11.634C31.834,26.541,26.626,31.749,20.201,31.749z"/> + <path fill="#66b4e0" id="loaderM" d="M26.013,10.047l1.654-2.866c-2.198-1.272-4.743-2.012-7.466-2.012h0v3.312h0C22.32,8.481,24.301,9.057,26.013,10.047z"> + <animateTransform attributeType="xml" attributeName="transform" type="rotate" from="0 20 20" to="360 20 20" dur="0.5s" repeatCount="indefinite"/> + </path> +</svg> diff --git a/app/assets/javascripts/es6_browserified/journey_patterns/actions/index.js b/app/assets/javascripts/es6_browserified/journey_patterns/actions/index.js index 386955540..f56956b31 100644 --- a/app/assets/javascripts/es6_browserified/journey_patterns/actions/index.js +++ b/app/assets/javascripts/es6_browserified/journey_patterns/actions/index.js @@ -3,20 +3,23 @@ const actions = { type: "RECEIVE_JOURNEY_PATTERNS", json }), - loadFirstPage: (dispatch) => ({ - type: 'LOAD_FIRST_PAGE', - dispatch + receiveErrors : (json) => ({ + type: "RECEIVE_ERRORS", + json + }), + unavailableServer : () => ({ + type: 'UNAVAILABLE_SERVER' }), - goToPreviousPage : (dispatch, currentPage) => ({ + goToPreviousPage : (dispatch, pagination) => ({ type: 'GO_TO_PREVIOUS_PAGE', dispatch, - currentPage, + pagination, nextPage : false }), - goToNextPage : (dispatch, currentPage) => ({ + goToNextPage : (dispatch, pagination) => ({ type: 'GO_TO_NEXT_PAGE', dispatch, - currentPage, + pagination, nextPage : true }), updateCheckboxValue : (e, index) => ({ @@ -24,10 +27,17 @@ const actions = { id : e.currentTarget.id, index }), - openConfirmModal : (accept, cancel) => ({ + checkConfirmModal : (event, callback, stateChanged,dispatch) => { + if(stateChanged === true){ + return actions.openConfirmModal(callback) + }else{ + dispatch(actions.fetchingApi()) + return callback + } + }, + openConfirmModal : (callback) => ({ type : 'OPEN_CONFIRM_MODAL', - accept, - cancel + callback }), openEditModal : (index, journeyPattern) => ({ type : 'EDIT_JOURNEYPATTERN_MODAL', @@ -57,7 +67,39 @@ const actions = { type: 'SAVE_PAGE', dispatch }), + updateTotalCount: (diff) => ({ + type: 'UPDATE_TOTAL_COUNT', + diff + }), + fetchingApi: () =>({ + type: 'FETCH_API' + }), + resetValidation: (target) => { + $(target).parent().removeClass('has-error').children('.help-block').remove() + }, + validateFields : (fields) => { + const test = [] + + Object.keys(fields).map(function(key) { + test.push(fields[key].validity.valid) + }) + if(test.indexOf(false) >= 0) { + // Form is invalid + test.map(function(item, i) { + if(item == false) { + const k = Object.keys(fields)[i] + $(fields[k]).parent().addClass('has-error').children('.help-block').remove() + $(fields[k]).parent().append("<span class='small help-block'>" + fields[k].validationMessage + "</span>") + } + }) + return false + } else { + // Form is valid + return true + } + }, submitJourneyPattern : (dispatch, state, next) => { + dispatch(actions.fetchingApi()) let urlJSON = window.location.pathname + ".json" let req = new Request(urlJSON, { credentials: 'same-origin', @@ -69,13 +111,22 @@ const actions = { 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content') } }) + let hasError = false fetch(req) - .then(response => response.json()) - .then((json) => { - if(next){ - dispatch(next) - }else{ - dispatch(actions.receiveJourneyPatterns(json)) + .then(response => { + if(!response.ok) { + hasError = true + } + return response.json() + }).then((json) => { + if(hasError == true) { + dispatch(actions.receiveErrors(json)) + } else { + if(next) { + dispatch(next) + } else { + dispatch(actions.receiveJourneyPatterns(json)) + } } }) }, @@ -106,29 +157,41 @@ const actions = { let req = new Request(urlJSON, { credentials: 'same-origin', }) + let hasError = false fetch(req) - .then(response => response.json()) - .then((json) => { - let val - for (val of json){ - for (let stop_point of val.route_short_description.stop_points){ - stop_point.checked = false - val.stop_area_short_descriptions.map((element) => { - if(element.stop_area_short_description.id === stop_point.id){ - stop_point.checked = true - } + .then(response => { + if(response.status == 500) { + hasError = true + } + return response.json() + }).then((json) => { + if(hasError == true) { + dispatch(actions.unavailableServer()) + } else { + let val + for (val of json){ + for (let stop_point of val.route_short_description.stop_points){ + stop_point.checked = false + val.stop_area_short_descriptions.map((element) => { + if(element.stop_area_short_description.id === stop_point.id){ + stop_point.checked = true + } + }) + } + journeyPatterns.push({ + name: val.name, + object_id: val.object_id, + published_name: val.published_name, + registration_number: val.registration_number, + stop_points: val.route_short_description.stop_points, + deletable: false }) } - journeyPatterns.push({ - name: val.name, - object_id: val.object_id, - published_name: val.published_name, - registration_number: val.registration_number, - stop_points: val.route_short_description.stop_points, - deletable: false - }) + if(journeyPatterns.length != window.journeyPatternsPerPage){ + dispatch(actions.updateTotalCount(journeyPatterns.length - window.journeyPatternsPerPage)) + } + dispatch(actions.receiveJourneyPatterns(journeyPatterns)) } - dispatch(actions.receiveJourneyPatterns(journeyPatterns)) }) } } diff --git a/app/assets/javascripts/es6_browserified/journey_patterns/components/ConfirmModal.js b/app/assets/javascripts/es6_browserified/journey_patterns/components/ConfirmModal.js index 86bbe3acb..d9fbf07f8 100644 --- a/app/assets/javascripts/es6_browserified/journey_patterns/components/ConfirmModal.js +++ b/app/assets/javascripts/es6_browserified/journey_patterns/components/ConfirmModal.js @@ -14,7 +14,7 @@ const ConfirmModal = ({dispatch, modal, onModalAccept, onModalCancel, journeyPat className='btn btn-default' data-dismiss='modal' type='button' - onClick= {() => {onModalCancel(modal.confirmModal.cancel)}} + onClick= {() => {onModalCancel(modal.confirmModal.callback)}} > Ne pas enregistrer </button> @@ -22,7 +22,7 @@ const ConfirmModal = ({dispatch, modal, onModalAccept, onModalCancel, journeyPat className='btn btn-danger' data-dismiss='modal' type='button' - onClick = {() => {onModalAccept(modal.confirmModal.accept, journeyPatterns)}} + onClick = {() => {onModalAccept(modal.confirmModal.callback, journeyPatterns)}} > Enregistrer </button> diff --git a/app/assets/javascripts/es6_browserified/journey_patterns/components/CreateModal.js b/app/assets/javascripts/es6_browserified/journey_patterns/components/CreateModal.js index 2c75dd808..cb2b96766 100644 --- a/app/assets/javascripts/es6_browserified/journey_patterns/components/CreateModal.js +++ b/app/assets/javascripts/es6_browserified/journey_patterns/components/CreateModal.js @@ -1,102 +1,118 @@ var React = require('react') var Component = require('react').Component var PropTypes = require('react').PropTypes +var actions = require('../actions') class CreateModal extends Component { constructor(props) { super(props) } - handleSubmit(e) { - e.preventDefault() - this.props.onAddJourneyPattern(this.refs) + + handleSubmit() { + if(actions.validateFields(this.refs) == true) { + this.props.onAddJourneyPattern(this.refs) + $('#NewJourneyPatternModal').modal('hide') + } } render() { - return ( - <div className='pull-right'> - <button - type='button' - className='btn btn-primary btn-sm' - data-toggle='modal' - data-target='#NewJourneyPatternModal' - onClick={this.props.onOpenCreateModal} - > - <span className='fa fa-plus'></span> Ajouter une mission - </button> + if(this.props.status.isFetching == true) { + return false + } + if(this.props.status.fetchSuccess == true) { + return ( + <div className='pull-right'> + <button + type='button' + className='btn btn-primary btn-sm' + data-toggle='modal' + data-target='#NewJourneyPatternModal' + onClick={this.props.onOpenCreateModal} + > + <span className='fa fa-plus'></span> Ajouter une mission + </button> - <div className={ 'modal fade ' + ((this.props.modal.type == 'create') ? 'in' : '') } id='NewJourneyPatternModal'> - <div className='modal-dialog'> - <div className='modal-content'> - <div className='modal-header clearfix'> - <h4>Ajouter une mission</h4> - </div> + <div className={ 'modal fade ' + ((this.props.modal.type == 'create') ? 'in' : '') } id='NewJourneyPatternModal'> + <div className='modal-dialog'> + <div className='modal-content'> + <div className='modal-header clearfix'> + <h4>Ajouter une mission</h4> + </div> - <div className='modal-body'> - {(this.props.modal.type == 'create') && ( - <form> - <div className='form-group'> - <label>Nom</label> - <input - type='text' - ref='name' - className='form-control' - /> - </div> - <div className='row'> - <div className='col-lg-6 col-md-6 col-sm-6 col-xs-6'> + {(this.props.modal.type == 'create') && ( + <form> + <div className='modal-body'> <div className='form-group'> - <label>Nom public</label> + <label className='control-label is-required'>Nom</label> <input type='text' - ref='published_name' + ref='name' className='form-control' - /> + onKeyDown={(e) => actions.resetValidation(e.currentTarget)} + required + /> </div> - </div> - <div className='col-lg-6 col-md-6 col-sm-6 col-xs-6'> - <div className='form-group'> - <label>N° d'enregistrement</label> - <input - type='text' - ref='registration_number' - className='form-control' - /> + <div className='row'> + <div className='col-lg-6 col-md-6 col-sm-6 col-xs-6'> + <div className='form-group'> + <label className='control-label is-required'>Nom public</label> + <input + type='text' + ref='published_name' + className='form-control' + onKeyDown={(e) => actions.resetValidation(e.currentTarget)} + required + /> + </div> + </div> + <div className='col-lg-6 col-md-6 col-sm-6 col-xs-6'> + <div className='form-group'> + <label className='control-label is-required'>N° d'enregistrement</label> + <input + type='text' + ref='registration_number' + className='form-control' + onKeyDown={(e) => actions.resetValidation(e.currentTarget)} + required + /> + </div> + </div> </div> </div> - </div> - </form> - )} - </div> - - <div className='modal-footer'> - <button - className='btn btn-default' - data-dismiss='modal' - type='button' - onClick={this.props.onModalClose} - > - Annuler - </button> - <button - className='btn btn-danger' - data-dismiss='modal' - type='button' - onClick={this.handleSubmit.bind(this)} - > - Valider - </button> + <div className='modal-footer'> + <button + className='btn btn-default' + data-dismiss='modal' + type='button' + onClick={this.props.onModalClose} + > + Annuler + </button> + <button + className='btn btn-danger' + type='button' + onClick={this.handleSubmit.bind(this)} + > + Valider + </button> + </div> + </form> + )} + </div> </div> </div> </div> - </div> - </div> - ) + ) + } else { + return false + } } } CreateModal.propTypes = { index: PropTypes.number, - modal: PropTypes.object, + modal: PropTypes.object.isRequired, + status: PropTypes.object.isRequired, onOpenCreateModal: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired, onAddJourneyPattern: PropTypes.func.isRequired diff --git a/app/assets/javascripts/es6_browserified/journey_patterns/components/EditModal.js b/app/assets/javascripts/es6_browserified/journey_patterns/components/EditModal.js index d3dcd333d..7d7dd40a4 100644 --- a/app/assets/javascripts/es6_browserified/journey_patterns/components/EditModal.js +++ b/app/assets/javascripts/es6_browserified/journey_patterns/components/EditModal.js @@ -1,14 +1,18 @@ var React = require('react') var Component = require('react').Component var PropTypes = require('react').PropTypes +var actions = require('../actions') class EditModal extends Component { constructor(props) { super(props) } - handleSubmit(e) { - e.preventDefault() - this.props.saveModal(this.props.modal.modalProps.index, this.refs) + + handleSubmit() { + if(actions.validateFields(this.refs) == true) { + this.props.saveModal(this.props.modal.modalProps.index, this.refs) + $('#JourneyPatternModal').modal('hide') + } } render() { @@ -50,68 +54,74 @@ class EditModal extends Component { </ul> </div> </div> - <div className='modal-body'> - {(this.props.modal.type == 'edit') && ( - <form> + + {(this.props.modal.type == 'edit') && ( + <form> + <div className='modal-body'> <div className='form-group'> - <label>Nom</label> + <label className='control-label is-required'>Nom</label> <input type='text' ref='name' className='form-control' id={this.props.modal.modalProps.index} defaultValue={this.props.modal.modalProps.journeyPattern.name} + onKeyDown={(e) => actions.resetValidation(e.currentTarget)} + required /> </div> <div className='row'> <div className='col-lg-6 col-md-6 col-sm-6 col-xs-6'> <div className='form-group'> - <label>Nom public</label> + <label className='control-label is-required'>Nom public</label> <input type='text' ref='published_name' className='form-control' id={this.props.modal.modalProps.index} defaultValue={this.props.modal.modalProps.journeyPattern.published_name} + onKeyDown={(e) => actions.resetValidation(e.currentTarget)} + required /> </div> </div> <div className='col-lg-6 col-md-6 col-sm-6 col-xs-6'> <div className='form-group'> - <label>N° d'enregistrement</label> + <label className='control-label is-required'>N° d'enregistrement</label> <input type='text' ref='registration_number' className='form-control' id={this.props.modal.modalProps.index} defaultValue={this.props.modal.modalProps.journeyPattern.registration_number} + onKeyDown={(e) => actions.resetValidation(e.currentTarget)} + required /> </div> </div> </div> + </div> - </form> - )} - </div> - <div className='modal-footer'> - <button - className='btn btn-default' - data-dismiss='modal' - type='button' - onClick={this.props.onModalClose} - > - Annuler - </button> - <button - className='btn btn-danger' - data-dismiss='modal' - type='button' - onClick={this.handleSubmit.bind(this)} - > - Valider - </button> - </div> + <div className='modal-footer'> + <button + className='btn btn-default' + data-dismiss='modal' + type='button' + onClick={this.props.onModalClose} + > + Annuler + </button> + <button + className='btn btn-danger' + type='button' + onClick={this.handleSubmit.bind(this)} + > + Valider + </button> + </div> + </form> + )} </div> </div> </div> diff --git a/app/assets/javascripts/es6_browserified/journey_patterns/components/JourneyPattern.js b/app/assets/javascripts/es6_browserified/journey_patterns/components/JourneyPattern.js index 78e2e6d9c..3d439b6ab 100644 --- a/app/assets/javascripts/es6_browserified/journey_patterns/components/JourneyPattern.js +++ b/app/assets/javascripts/es6_browserified/journey_patterns/components/JourneyPattern.js @@ -4,43 +4,71 @@ var PropTypes = require('react').PropTypes const JourneyPattern = (props) => { return ( <div className={'list-group-item ' + (props.value.deletable ? 'disabled' : '') + (props.value.object_id ? '' : 'to_record')}> - <div style={{display: 'inline-block', verticalAlign: 'top', width: '40%'}}> - <p className='small'><strong>Index: </strong>{props.index}</p> - <p className='small'><strong>Name: </strong>{props.value.name}</p> - </div> + {/* Errors */} + {(props.value.errors) && ( + <ul className='alert alert-danger small' style={{paddingLeft: 30}}> + {Object.keys(props.value.errors).map(function(key, i) { + return ( + <li key={i} style={{listStyleType: 'disc'}}> + <strong>'{key}'</strong> {props.value.errors[key]} + </li> + ) + })} + </ul> + )} + + <div style={{display: 'inline-block', verticalAlign: 'top', width: 'calc(100% - 25px)'}}> + {/* Name */} + <p className='small'> + <strong>Name: </strong>{props.value.name} + </p> + + {/* Published name */} + <p className='small'> + <strong>Published name: </strong>{props.value.published_name} + </p> - <div style={{display: 'inline-block', verticalAlign: 'top', width: '40%'}}> - <p className='small'><strong>ObjectID: </strong>{props.value.object_id}</p> - <p className='small'><strong>Published name: </strong>{props.value.published_name}</p> + {/* Registration number */} + <p className='small'> + <strong>Registration number: </strong>{props.value.registration_number} + </p> + + {/* Object_id */} + <p className='small'> + <strong>ObjectID: </strong>{props.value.object_id} + </p> + + {/* Stop points */} + <p className='small'> + <strong>Stop points: </strong> + </p> + <ul className='list-group'> + {props.value.stop_points.map((stopPoint, i) => + <li + key={ i } + className='list-group-item clearfix' + > + <span className='label label-default' style={{marginRight: 5}}>{stopPoint.id}</span> + <span>{stopPoint.name}</span> + <span className='pull-right'> + <input + onChange = {(e) => props.onCheckboxChange(e)} + type='checkbox' + id={stopPoint.id} + checked={stopPoint.checked} + disabled={props.value.deletable ? 'disabled' : ''} + ></input> + </span> + </li> + )} + </ul> </div> - <div className='clearfix' style={{display: 'inline-block', verticalAlign: 'top', width: '20%'}}> + <div className='clearfix' style={{display: 'inline-block', verticalAlign: 'top', width: '25px'}}> <button className={(props.value.deletable ? 'disabled' : '') + ' btn btn-xs btn-danger pull-right'} onClick={props.onOpenEditModal} data-toggle='modal' data-target='#JourneyPatternModal'> <span className='fa fa-pencil'></span> </button> </div> - - <p className='small'><strong>Stop points: </strong></p> - <ul className='list-group'> - {props.value.stop_points.map((stopPoint, i) => - <li - key={ i } - className='list-group-item clearfix' - > - <span className='label label-default' style={{marginRight: 5}}>{stopPoint.id}</span> - <span>{stopPoint.name}</span> - <span className='pull-right'> - <input - onChange = {(e) => props.onCheckboxChange(e)} - type='checkbox' - id={stopPoint.id} - checked={stopPoint.checked} - disabled={props.value.deletable ? 'disabled' : ''} - ></input> - </span> - </li> - )} - </ul> </div> ) } diff --git a/app/assets/javascripts/es6_browserified/journey_patterns/components/JourneyPatterns.js b/app/assets/javascripts/es6_browserified/journey_patterns/components/JourneyPatterns.js index 160631697..9b13781f8 100644 --- a/app/assets/javascripts/es6_browserified/journey_patterns/components/JourneyPatterns.js +++ b/app/assets/javascripts/es6_browserified/journey_patterns/components/JourneyPatterns.js @@ -7,29 +7,43 @@ class JourneyPatterns extends Component{ constructor(props){ super(props) } - componentDidMount() { this.props.onLoadFirstPage() } render() { - return ( - <div className='list-group'> - {this.props.journeyPatterns.map((journeyPattern, index) => - <JourneyPattern - value={ journeyPattern } - key={ index } - onCheckboxChange= {(e) => this.props.onCheckboxChange(e, index)} - onOpenEditModal= {() => this.props.onOpenEditModal(index, journeyPattern)} - /> - )} - </div> - ) + if(this.props.status.isFetching == true) { + return ( + <div className="isLoading" style={{marginTop: 80, marginBottom: 80}}> + <div className="loader"></div> + </div> + ) + } else { + return ( + <div className='list-group'> + {(this.props.status.fetchSuccess == false) && ( + <div className="alert alert-danger"> + <strong>Erreur : </strong> + la récupération des missions a rencontré un problème. Rechargez la page pour tenter de corriger le problème + </div> + )} + {this.props.journeyPatterns.map((journeyPattern, index) => + <JourneyPattern + value={ journeyPattern } + key={ index } + onCheckboxChange= {(e) => this.props.onCheckboxChange(e, index)} + onOpenEditModal= {() => this.props.onOpenEditModal(index, journeyPattern)} + /> + )} + </div> + ) + } } } JourneyPatterns.propTypes = { journeyPatterns: PropTypes.array.isRequired, + status: PropTypes.object.isRequired, onCheckboxChange: PropTypes.func.isRequired, onLoadFirstPage: PropTypes.func.isRequired, onOpenEditModal: PropTypes.func.isRequired diff --git a/app/assets/javascripts/es6_browserified/journey_patterns/components/Navigate.js b/app/assets/javascripts/es6_browserified/journey_patterns/components/Navigate.js new file mode 100644 index 000000000..a5d6a5a9f --- /dev/null +++ b/app/assets/javascripts/es6_browserified/journey_patterns/components/Navigate.js @@ -0,0 +1,54 @@ +var React = require('react') +var Component = require('react').Component +var PropTypes = require('react').PropTypes +var actions = require('../actions') + +let Navigate = ({ dispatch, journeyPatterns, pagination, status }) => { + let firstPage = 1 + let lastPage = Math.ceil(pagination.totalCount / window.journeyPatternsPerPage) + + if(status.isFetching == true) { + return false + } + if(status.fetchSuccess == true) { + return ( + <form className='btn-group btn-group-sm' onSubmit={e => { + e.preventDefault() + }}> + <button + onClick={e => { + e.preventDefault() + dispatch(actions.checkConfirmModal(e, actions.goToPreviousPage(dispatch, pagination), pagination.stateChanged, dispatch)) + }} + type="submit" + data-toggle='' + data-target='#ConfirmModal' + className={ (pagination.page == firstPage ? "hidden" : "") + " btn btn-default" }> + <span className="fa fa-chevron-left"></span> + </button> + <button + onClick={e => { + e.preventDefault() + dispatch(actions.checkConfirmModal(e, actions.goToNextPage(dispatch, pagination), pagination.stateChanged, dispatch)) + }} + type="submit" + data-toggle='' + data-target='#ConfirmModal' + className={ (pagination.page == lastPage ? "hidden" : "") + " btn btn-default" }> + <span className="fa fa-chevron-right"></span> + </button> + </form> + ) + } else { + return false + } +} + +Navigate.propTypes = { + journeyPatterns: PropTypes.array.isRequired, + status: PropTypes.object.isRequired, + pagination: PropTypes.object.isRequired, + dispatch: PropTypes.func.isRequired +} + +module.exports = Navigate diff --git a/app/assets/javascripts/es6_browserified/journey_patterns/containers/AddJourneyPattern.js b/app/assets/javascripts/es6_browserified/journey_patterns/containers/AddJourneyPattern.js index eb4499e50..ee13819fd 100644 --- a/app/assets/javascripts/es6_browserified/journey_patterns/containers/AddJourneyPattern.js +++ b/app/assets/javascripts/es6_browserified/journey_patterns/containers/AddJourneyPattern.js @@ -5,7 +5,8 @@ var CreateModal = require('../components/CreateModal') const mapStateToProps = (state) => { return { modal: state.modal, - journeyPatterns: state.journeyPatterns + journeyPatterns: state.journeyPatterns, + status: state.status } } diff --git a/app/assets/javascripts/es6_browserified/journey_patterns/containers/ConfirmModal.js b/app/assets/javascripts/es6_browserified/journey_patterns/containers/ConfirmModal.js index b78a4f49d..d66425a3a 100644 --- a/app/assets/javascripts/es6_browserified/journey_patterns/containers/ConfirmModal.js +++ b/app/assets/javascripts/es6_browserified/journey_patterns/containers/ConfirmModal.js @@ -12,9 +12,11 @@ const mapStateToProps = (state) => { const mapDispatchToProps = (dispatch) => { return { onModalAccept: (next, state) =>{ + dispatch(actions.fetchingApi()) actions.submitJourneyPattern(dispatch, state, next) }, onModalCancel: (next) =>{ + dispatch(actions.fetchingApi()) dispatch(next) }, onModalClose: () =>{ diff --git a/app/assets/javascripts/es6_browserified/journey_patterns/containers/JourneyPatternList.js b/app/assets/javascripts/es6_browserified/journey_patterns/containers/JourneyPatternList.js index 73dc6a1c7..e7173bf6e 100644 --- a/app/assets/javascripts/es6_browserified/journey_patterns/containers/JourneyPatternList.js +++ b/app/assets/javascripts/es6_browserified/journey_patterns/containers/JourneyPatternList.js @@ -4,14 +4,16 @@ var JourneyPatterns = require('../components/JourneyPatterns') const mapStateToProps = (state) => { return { - journeyPatterns: state.journeyPatterns + journeyPatterns: state.journeyPatterns, + status: state.status } } const mapDispatchToProps = (dispatch) => { return { onLoadFirstPage: () =>{ - dispatch(actions.loadFirstPage(dispatch)) + dispatch(actions.fetchingApi()) + actions.fetchJourneyPatterns(dispatch) }, onCheckboxChange: (e, index) =>{ dispatch(actions.updateCheckboxValue(e, index)) diff --git a/app/assets/javascripts/es6_browserified/journey_patterns/containers/Navigate.js b/app/assets/javascripts/es6_browserified/journey_patterns/containers/Navigate.js index 1c9f7113f..ef9f8859c 100644 --- a/app/assets/javascripts/es6_browserified/journey_patterns/containers/Navigate.js +++ b/app/assets/javascripts/es6_browserified/journey_patterns/containers/Navigate.js @@ -1,49 +1,17 @@ var React = require('react') var connect = require('react-redux').connect var actions = require('../actions') +var NavigateComponent = require('../components/Navigate') -let Navigate = ({ dispatch, journeyPatterns, page, length, onOpenConfirmModal }) => { - let firstPage = 1 - let lastPage = Math.ceil(length / 12) - - return ( - <form className='btn-group btn-group-sm' onSubmit={e => { - e.preventDefault() - }}> - <button - onClick={e => { - e.preventDefault() - dispatch(actions.openConfirmModal(actions.goToPreviousPage(dispatch, page), actions.goToPreviousPage(dispatch, page))) - }} - type="submit" - data-toggle='modal' - data-target='#ConfirmModal' - className={ (page == firstPage ? "hidden" : "") + " btn btn-default" }> - <span className="fa fa-chevron-left"></span> - </button> - <button - onClick={e => { - e.preventDefault() - dispatch(actions.openConfirmModal(actions.goToNextPage(dispatch, page), actions.goToNextPage(dispatch, page))) - }} - type="submit" - data-toggle='modal' - data-target='#ConfirmModal' - className={ (page == lastPage ? "hidden" : "") + " btn btn-default" }> - <span className="fa fa-chevron-right"></span> - </button> - </form> - ) -} const mapStateToProps = (state) => { return { journeyPatterns: state.journeyPatterns, - page: state.pagination.page, - length: state.pagination.totalCount, - confirmModalActions: state.modal.confirmActions + status: state.status, + pagination: state.pagination } } -Navigate = connect(mapStateToProps)(Navigate) + +const Navigate = connect(mapStateToProps)(NavigateComponent) module.exports = Navigate diff --git a/app/assets/javascripts/es6_browserified/journey_patterns/containers/SaveJourneyPattern.js b/app/assets/javascripts/es6_browserified/journey_patterns/containers/SaveJourneyPattern.js index 041219f0d..35626d626 100644 --- a/app/assets/javascripts/es6_browserified/journey_patterns/containers/SaveJourneyPattern.js +++ b/app/assets/javascripts/es6_browserified/journey_patterns/containers/SaveJourneyPattern.js @@ -2,27 +2,35 @@ var React = require('react') var connect = require('react-redux').connect var actions = require('../actions') -let SaveJourneyPattern = ({ dispatch, journeyPatterns, page }) => { - return ( - <form className='clearfix' onSubmit={e => {e.preventDefault()}}> - <button - className='btn btn-danger pull-right' - type='submit' - onClick={e => { - e.preventDefault() - dispatch(actions.savePage(dispatch, page)) - }} - > - Valider - </button> - </form> - ) +let SaveJourneyPattern = ({ dispatch, journeyPatterns, page, status }) => { + if(status.isFetching == true) { + return false + } + if(status.fetchSuccess == true) { + return ( + <form className='clearfix' onSubmit={e => {e.preventDefault()}}> + <button + className='btn btn-danger pull-right' + type='submit' + onClick={e => { + e.preventDefault() + actions.submitJourneyPattern(dispatch, journeyPatterns) + }} + > + Valider + </button> + </form> + ) + } else { + return false + } } const mapStateToProps = (state) => { return { journeyPatterns: state.journeyPatterns, - page: state.pagination.page + page: state.pagination.page, + status: state.status } } diff --git a/app/assets/javascripts/es6_browserified/journey_patterns/index.js b/app/assets/javascripts/es6_browserified/journey_patterns/index.js index 449cd3202..c40d592a3 100644 --- a/app/assets/javascripts/es6_browserified/journey_patterns/index.js +++ b/app/assets/javascripts/es6_browserified/journey_patterns/index.js @@ -12,10 +12,16 @@ var App = require('./components/App') // var promise = require('redux-promise') var initialState = { + status: { + fetchSuccess: true, + isFetching: false + }, journeyPatterns: [], pagination: { page : 1, - totalCount: window.journeyPatternLength + totalCount: window.journeyPatternLength, + perPage: window.journeyPatternsPerPage, + stateChanged: false }, modal: { type: '', diff --git a/app/assets/javascripts/es6_browserified/journey_patterns/reducers/index.js b/app/assets/javascripts/es6_browserified/journey_patterns/reducers/index.js index 9e1d15e08..bc97ccb05 100644 --- a/app/assets/javascripts/es6_browserified/journey_patterns/reducers/index.js +++ b/app/assets/javascripts/es6_browserified/journey_patterns/reducers/index.js @@ -1,9 +1,11 @@ var combineReducers = require('redux').combineReducers +var status = require('./status') var journeyPatterns = require('./journeyPatterns') var pagination = require('./pagination') var modal = require('./modal') const journeyPatternsApp = combineReducers({ + status, journeyPatterns, pagination, modal diff --git a/app/assets/javascripts/es6_browserified/journey_patterns/reducers/journeyPatterns.js b/app/assets/javascripts/es6_browserified/journey_patterns/reducers/journeyPatterns.js index ba1a2cdc5..13f1100b3 100644 --- a/app/assets/javascripts/es6_browserified/journey_patterns/reducers/journeyPatterns.js +++ b/app/assets/javascripts/es6_browserified/journey_patterns/reducers/journeyPatterns.js @@ -33,16 +33,18 @@ const journeyPatterns = (state = [], action) => { switch (action.type) { case 'RECEIVE_JOURNEY_PATTERNS': return [...action.json] - case 'LOAD_FIRST_PAGE': - actions.fetchJourneyPatterns(action.dispatch) + case 'RECEIVE_ERRORS': + return [...action.json] case 'GO_TO_PREVIOUS_PAGE': - if(action.currentPage > 1){ - actions.fetchJourneyPatterns(action.dispatch, action.currentPage, action.nextPage) + $('#ConfirmModal').modal('hide') + if(action.pagination.page > 1){ + actions.fetchJourneyPatterns(action.dispatch, action.pagination.page, action.nextPage) } return state case 'GO_TO_NEXT_PAGE': - if (window.journeyPatternLength - (action.currentPage * 12) > 0){ - actions.fetchJourneyPatterns(action.dispatch, action.currentPage, action.nextPage) + $('#ConfirmModal').modal('hide') + if (action.pagination.totalCount - (action.pagination.page * action.pagination.perPage) > 0){ + actions.fetchJourneyPatterns(action.dispatch, action.pagination.page, action.nextPage) } return state case 'UPDATE_CHECKBOX_VALUE': @@ -78,8 +80,6 @@ const journeyPatterns = (state = [], action) => { return j } }) - case 'SAVE_PAGE': - actions.submitJourneyPattern(action.dispatch, state) default: return state } diff --git a/app/assets/javascripts/es6_browserified/journey_patterns/reducers/modal.js b/app/assets/javascripts/es6_browserified/journey_patterns/reducers/modal.js index 85df48954..cb274d767 100644 --- a/app/assets/javascripts/es6_browserified/journey_patterns/reducers/modal.js +++ b/app/assets/javascripts/es6_browserified/journey_patterns/reducers/modal.js @@ -1,11 +1,11 @@ const modal = (state = {}, action) => { switch (action.type) { case 'OPEN_CONFIRM_MODAL': + $('#ConfirmModal').modal('show') return Object.assign({}, state, { type: 'confirm', confirmModal: { - accept: action.accept, - cancel: action.cancel + callback: action.callback, } }) case 'EDIT_JOURNEYPATTERN_MODAL': diff --git a/app/assets/javascripts/es6_browserified/journey_patterns/reducers/pagination.js b/app/assets/javascripts/es6_browserified/journey_patterns/reducers/pagination.js index a17e9f292..48d95fdea 100644 --- a/app/assets/javascripts/es6_browserified/journey_patterns/reducers/pagination.js +++ b/app/assets/javascripts/es6_browserified/journey_patterns/reducers/pagination.js @@ -1,18 +1,35 @@ const pagination = (state = {}, action) => { switch (action.type) { + case 'RECEIVE_JOURNEY_PATTERNS': + return Object.assign({}, state, {stateChanged: false}) case 'GO_TO_PREVIOUS_PAGE': - if (action.currentPage > 1){ - return Object.assign({}, state, {page : action.currentPage - 1}) + if (action.pagination.page > 1){ + toggleOnConfirmModal() + return Object.assign({}, state, {page : action.pagination.page - 1, stateChanged: false}) } return state case 'GO_TO_NEXT_PAGE': - if (state.totalCount - (action.currentPage * 12) > 0){ - return Object.assign({}, state, {page : action.currentPage + 1}) + if (state.totalCount - (action.pagination.page * action.pagination.perPage) > 0){ + toggleOnConfirmModal() + return Object.assign({}, state, {page : action.pagination.page + 1, stateChanged: false}) } return state + case 'UPDATE_CHECKBOX_VALUE': + case 'ADD_JOURNEYPATTERN': + case 'SAVE_MODAL': + toggleOnConfirmModal('modal') + return Object.assign({}, state, {stateChanged: true}) + case 'UPDATE_TOTAL_COUNT': + return Object.assign({}, state, {totalCount : state.totalCount - action.diff }) default: return state } } +const toggleOnConfirmModal = (arg = '') =>{ + $('.confirm').each(function(){ + $(this).data('toggle','') + }) +} + module.exports = pagination diff --git a/app/assets/javascripts/es6_browserified/journey_patterns/reducers/status.js b/app/assets/javascripts/es6_browserified/journey_patterns/reducers/status.js new file mode 100644 index 000000000..973fab0f9 --- /dev/null +++ b/app/assets/javascripts/es6_browserified/journey_patterns/reducers/status.js @@ -0,0 +1,16 @@ +var actions = require("../actions") + +const status = (state = {}, action) => { + switch (action.type) { + case 'UNAVAILABLE_SERVER': + return Object.assign({}, state, {fetchSuccess: false}) + case 'FETCH_API': + return Object.assign({}, state, {isFetching: true}) + case 'RECEIVE_JOURNEY_PATTERNS': + return Object.assign({}, state, {fetchSuccess: true, isFetching: false}) + default: + return state + } +} + +module.exports = status diff --git a/app/assets/stylesheets/components/_form.sass b/app/assets/stylesheets/components/_form.sass index d5f4a1eae..97e92942e 100644 --- a/app/assets/stylesheets/components/_form.sass +++ b/app/assets/stylesheets/components/_form.sass @@ -11,3 +11,20 @@ // Add custom color: #555 background-color: #f5f5f5 + +// Validations +.control-label + &.is-required + &:after + content: ' *' + display: inline + color: #a94442 + +.form-control + &:required + &:focus:invalid + border-color: #d43f3a + box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px #d9534f + &:focus:valid + border-color: #4cae4c + box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px #5cb85c diff --git a/app/assets/stylesheets/components/_loader.sass b/app/assets/stylesheets/components/_loader.sass new file mode 100644 index 000000000..0593545b9 --- /dev/null +++ b/app/assets/stylesheets/components/_loader.sass @@ -0,0 +1,9 @@ +.isLoading + width: 40px + height: 40px + margin: 0 auto + + .loader + width: 40px + height: 40px + background: url(loader.svg) no-repeat center center diff --git a/app/controllers/journey_patterns_collections_controller.rb b/app/controllers/journey_patterns_collections_controller.rb index cc01c1580..23f6c3b70 100644 --- a/app/controllers/journey_patterns_collections_controller.rb +++ b/app/controllers/journey_patterns_collections_controller.rb @@ -10,19 +10,13 @@ class JourneyPatternsCollectionsController < ChouetteController alias_method :route, :parent def show - jps_state + @q = route.journey_patterns.includes(:stop_points) + @journey_patterns ||= @q.paginate(:page => params[:page]).order(:name) end def update - state = JSON.parse request.raw_post - state.each do |item| - jp = jp_by_objectid(item['object_id']) || create_jp(item) - if item['deletable'] - state.delete(item) if jp.destroy - next - end - update_journey_pattern(jp, item) - end + state = JSON.parse request.raw_post + Chouette::JourneyPattern.state_update route, state errors = state.any? {|item| item['errors']} respond_to do |format| @@ -31,50 +25,4 @@ class JourneyPatternsCollectionsController < ChouetteController end protected - def update_jp jp, item - jp.update_attributes(fetch_jp_attributes(item)) - end - - def fetch_jp_attributes item - { - name: item['name'], - published_name: item['published_name'], - registration_number: item['registration_number'] - } - end - - def create_jp item - jp = route.journey_patterns.create(fetch_jp_attributes(item)) - if jp.persisted? - item['object_id'] = jp.objectid - else - item['errors'] = jp.errors - end - jp - end - - def update_journey_pattern jp, item - item['stop_points'].each do |sp| - exist = jp.stop_area_ids.include?(sp['id']) - next if exist && sp['checked'] - - stop_point = route.stop_points.find_by(stop_area_id: sp['id']) - if !exist && sp['checked'] - jp.stop_points << stop_point - end - if exist && !sp['checked'] - jp.stop_points.delete(stop_point) - end - end - update_jp(jp, item) - end - - def jps_state - @q = route.journey_patterns.includes(:stop_points) - @journey_patterns ||= @q.paginate(:page => params[:page]).order(:name) - end - - def jp_by_objectid objectid - Chouette::JourneyPattern.find_by(objectid: objectid) - end end diff --git a/app/controllers/journey_patterns_controller.rb b/app/controllers/journey_patterns_controller.rb index b7cdccc72..69f16321e 100644 --- a/app/controllers/journey_patterns_controller.rb +++ b/app/controllers/journey_patterns_controller.rb @@ -15,6 +15,8 @@ class JourneyPatternsController < ChouetteController alias_method :route, :parent alias_method :journey_pattern, :resource + before_action :check_policy, only: [:edit, :update, :destroy] + def index index! do |format| format.html { redirect_to referential_line_route_path(@referential,@line,@route) } @@ -51,9 +53,12 @@ class JourneyPatternsController < ChouetteController @journey_patterns ||= @q.result(:distinct => true).order(:name) end - private + def check_policy + authorize resource + end + def journey_pattern_params params.require(:journey_pattern).permit(:route_id, :objectid, :object_version, :creation_time, :creator_id, :name, :comment, :registration_number, :published_name, :departure_stop_point_id, :arrival_stop_point_id, {:stop_point_ids => []}) end diff --git a/app/controllers/routes_controller.rb b/app/controllers/routes_controller.rb index 89d2ddef4..be6329006 100644 --- a/app/controllers/routes_controller.rb +++ b/app/controllers/routes_controller.rb @@ -10,10 +10,11 @@ class RoutesController < ChouetteController end before_action :define_candidate_opposite_routes, only: [:new, :edit, :create, :update] + before_action :check_policy, only: [:edit, :update, :destroy] def index index! do |format| - format.html { redirect_to referential_line_path(@referential,@line) } + format.html { redirect_to referential_line_path(@referential, @line) } end end @@ -85,6 +86,10 @@ class RoutesController < ChouetteController end end + def check_policy + authorize resource + end + private def route_params diff --git a/app/models/chouette/journey_pattern.rb b/app/models/chouette/journey_pattern.rb index 945ee4f70..c57d42e71 100644 --- a/app/models/chouette/journey_pattern.rb +++ b/app/models/chouette/journey_pattern.rb @@ -19,6 +19,63 @@ class Chouette::JourneyPattern < Chouette::TridentActiveRecord attr_accessor :control_checked after_update :control_route_sections, :unless => "control_checked" + + def self.state_update route, state + transaction do + state.each do |item| + item.delete('errors') + jp = find_by(objectid: item['object_id']) || state_create_instance(route, item) + if item['deletable'] && jp.persisted? + next if jp.destroy + end + + # Update attributes and stop_points associations + jp.update_attributes(state_permited_attributes(item)) + jp.state_stop_points_update(item) if !jp.errors.any? && jp.persisted? + item['errors'] = jp.errors if jp.errors.any? + end + + if state.any? {|item| item['errors']} + state.map {|item| item.delete('object_id') if item['new_record']} + raise ActiveRecord::Rollback + end + end + # clean + state.map {|item| item.delete('new_record')} + state.delete_if {|item| item['deletable']} + end + + def self.state_permited_attributes item + { + name: item['name'], + published_name: item['published_name'], + registration_number: item['registration_number'] + } + end + + def self.state_create_instance route, item + jp = route.journey_patterns.create(state_permited_attributes(item)) + # Flag new record, so we can unset object_id if transaction rollback + item['object_id'] = jp.objectid + item['new_record'] = true + jp + end + + def state_stop_points_update item + item['stop_points'].each do |sp| + exist = stop_area_ids.include?(sp['id']) + next if exist && sp['checked'] + + stop_point = route.stop_points.find_by(stop_area_id: sp['id']) + if !exist && sp['checked'] + stop_points << stop_point + end + if exist && !sp['checked'] + stop_points.delete(stop_point) + end + end + end + # TODO: this a workarround # otherwise, we loose the first stop_point # when creating a new journey_pattern diff --git a/app/models/line_referential_sync.rb b/app/models/line_referential_sync.rb index a54d61edb..6730ddd73 100644 --- a/app/models/line_referential_sync.rb +++ b/app/models/line_referential_sync.rb @@ -6,6 +6,8 @@ class LineReferentialSync < ActiveRecord::Base after_commit :perform_sync, :on => :create validate :multiple_process_validation, :on => :create + scope :pending, -> { where(status: [:new, :pending]) } + private def perform_sync create_sync_message :info, :new @@ -26,7 +28,7 @@ class LineReferentialSync < ActiveRecord::Base state :failed event :run, after: :log_pending do - transitions :from => [:new, :failed], :to => :pending + transitions :from => :new, :to => :pending end event :successful, after: :log_successful do @@ -34,7 +36,7 @@ class LineReferentialSync < ActiveRecord::Base end event :failed, after: :log_failed do - transitions :from => :pending, :to => :failed + transitions :from => [:new, :pending], :to => :failed end end diff --git a/app/models/stop_area_referential_sync.rb b/app/models/stop_area_referential_sync.rb index 4de5f396a..a4e3eae30 100644 --- a/app/models/stop_area_referential_sync.rb +++ b/app/models/stop_area_referential_sync.rb @@ -6,6 +6,8 @@ class StopAreaReferentialSync < ActiveRecord::Base after_commit :perform_sync, :on => :create validate :multiple_process_validation, :on => :create + scope :pending, -> { where(status: [:new, :pending]) } + private def perform_sync create_sync_message :info, :new @@ -26,7 +28,7 @@ class StopAreaReferentialSync < ActiveRecord::Base state :failed event :run, after: :log_pending do - transitions :from => [:new, :failed], :to => :pending + transitions :from => :new, :to => :pending end event :successful, after: :log_successful do diff --git a/app/models/user.rb b/app/models/user.rb index 5cfdf0605..3debf37dc 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -21,6 +21,7 @@ class User < ActiveRecord::Base validates :organisation, :presence => true validates :email, :presence => true, :uniqueness => true validates :name, :presence => true + validate :permissions_unique_and_nonempty before_validation(:on => :create) do self.password ||= Devise.friendly_token.first(6) @@ -65,6 +66,10 @@ class User < ActiveRecord::Base end end + def has_permission?(permission) + permissions && permissions.include?(permission) + end + private # remove organisation and referentials if last user of it @@ -73,4 +78,11 @@ class User < ActiveRecord::Base organisation.destroy end end + + def permissions_unique_and_nonempty + if permissions && permissions.any? + errors.add(:permissions, I18n.t('activerecord.errors.models.calendar.attributes.permissions.must_be_unique')) if permissions.uniq.length != permissions.length + errors.add(:permissions, I18n.t('activerecord.errors.models.calendar.attributes.permissions.must_be_nonempty')) if permissions.include? '' + end + end end diff --git a/app/policies/journey_pattern_policy.rb b/app/policies/journey_pattern_policy.rb new file mode 100644 index 000000000..95ab23318 --- /dev/null +++ b/app/policies/journey_pattern_policy.rb @@ -0,0 +1,22 @@ +class JourneyPatternPolicy < ApplicationPolicy + class Scope < Scope + def resolve + scope + end + end + + def create? + user.has_permission?('journey_patterns.create') + end + + def edit? + user.has_permission?('journey_patterns.edit') + end + + def destroy? + user.has_permission?('journey_patterns.destroy') + end + + def update? ; edit? end + def new? ; create? end +end diff --git a/app/policies/route_policy.rb b/app/policies/route_policy.rb new file mode 100644 index 000000000..232706d8f --- /dev/null +++ b/app/policies/route_policy.rb @@ -0,0 +1,22 @@ +class RoutePolicy < ApplicationPolicy + class Scope < Scope + def resolve + scope + end + end + + def create? + user.has_permission?('routes.create') + end + + def edit? + user.has_permission?('routes.edit') + end + + def destroy? + user.has_permission?('routes.destroy') + end + + def update? ; edit? end + def new? ; create? end +end diff --git a/app/views/journey_patterns/show.html.slim b/app/views/journey_patterns/show.html.slim index 0fee1a257..417e4dc16 100644 --- a/app/views/journey_patterns/show.html.slim +++ b/app/views/journey_patterns/show.html.slim @@ -30,11 +30,14 @@ h3.journey_pattern_stop_points = t('.stop_points') - content_for :sidebar do ul.actions li - = link_to t('journey_patterns.actions.new'), new_referential_line_route_journey_pattern_path(@referential, @line, @route), class: 'add' + - if policy(@journey_pattern).create? + = link_to t('journey_patterns.actions.new'), new_referential_line_route_journey_pattern_path(@referential, @line, @route), class: 'add' li - = link_to t('journey_patterns.actions.edit'), edit_referential_line_route_journey_pattern_path(@referential, @line, @route, @journey_pattern), class: 'edit' + - if policy(@journey_pattern).edit? + = link_to t('journey_patterns.actions.edit'), edit_referential_line_route_journey_pattern_path(@referential, @line, @route, @journey_pattern), class: 'edit' li - = link_to t('journey_patterns.actions.destroy'), referential_line_route_journey_pattern_path(@referential, @line, @route, @journey_pattern), :method => :delete, :data => {:confirm => t('journey_patterns.actions.destroy_confirm')}, class: 'remove' + - if policy(@journey_pattern).destroy? + = link_to t('journey_patterns.actions.destroy'), referential_line_route_journey_pattern_path(@referential, @line, @route, @journey_pattern), :method => :delete, :data => {:confirm => t('journey_patterns.actions.destroy_confirm')}, class: 'remove' li = link_to edit_referential_line_route_journey_pattern_route_sections_selector_path(@referential, @line, @route, @journey_pattern), class: "edit#{' control-shape' if @journey_pattern.control?}" do = t('journey_patterns.actions.edit_route_sections') @@ -43,5 +46,5 @@ h3.journey_pattern_stop_points = t('.stop_points') li = link_to t('journey_patterns.journey_pattern.vehicle_journey_at_stops'), referential_line_route_vehicle_journeys_path(@referential, @line, @route, :q => {:journey_pattern_id_eq => @journey_pattern.id}), class: 'clock' - - = creation_tag(@journey_pattern)
\ No newline at end of file + + = creation_tag(@journey_pattern) diff --git a/app/views/journey_patterns_collections/show.html.slim b/app/views/journey_patterns_collections/show.html.slim index 7b6f7ae7a..10ac476a5 100644 --- a/app/views/journey_patterns_collections/show.html.slim +++ b/app/views/journey_patterns_collections/show.html.slim @@ -1,4 +1,5 @@ #journey_patterns = javascript_tag do - | window.journeyPatternLength = #{@journey_patterns.total_entries()} + | window.journeyPatternLength = #{@journey_patterns.total_entries()}; + | window.journeyPatternsPerPage = 12 = javascript_include_tag 'es6_browserified/journey_patterns/index.js' diff --git a/app/views/referential_lines/_reflines_routes.html.slim b/app/views/referential_lines/_reflines_routes.html.slim index 77b350fa6..8dcae73b5 100644 --- a/app/views/referential_lines/_reflines_routes.html.slim +++ b/app/views/referential_lines/_reflines_routes.html.slim @@ -9,7 +9,7 @@ th.text-center = @routes.human_attribute_name(:wayback) th.text-center = @routes.human_attribute_name(:opposite_route) th.text-center = "Actions" - + tbody - @routes.each do |route| tr @@ -21,14 +21,16 @@ = route.opposite_route.name - else = "Aucune séquence d'arrêts associée en sens opposé" - + td.text-center .btn.btn-group.btn-group-sm = link_to [@referential, @line, route], class: 'btn btn-default preview', title: "#{Chouette::Route.model_name.human.capitalize} #{route.name}" do span.fa.fa-eye - - = link_to edit_referential_line_route_path(@referential, @line, route), class: 'btn btn-default' do - span.fa.fa-pencil - = link_to referential_line_route_path(@referential, @line, route), method: :delete, :data => {:confirm => t('routes.actions.destroy_confirm')}, class: 'btn btn-danger' do - span.fa.fa-trash-o + - if policy(route).edit? + = link_to edit_referential_line_route_path(@referential, @line, route), class: 'btn btn-default' do + span.fa.fa-pencil + + - if policy(route).destroy? + = link_to referential_line_route_path(@referential, @line, route), method: :delete, :data => {:confirm => t('routes.actions.destroy_confirm')}, class: 'btn btn-danger' do + span.fa.fa-trash-o diff --git a/app/views/referential_lines/show.html.slim b/app/views/referential_lines/show.html.slim index ad455862d..5c8e1b32d 100644 --- a/app/views/referential_lines/show.html.slim +++ b/app/views/referential_lines/show.html.slim @@ -139,6 +139,7 @@ p.after_map - if !@line.hub_restricted? || (@line.hub_restricted? && @line.routes.size < 2) / FIXME #825 li - = link_to t('routes.actions.new'), new_referential_line_route_path(@referential, @line), class: 'add' + - if policy(Chouette::Route).create? + = link_to t('routes.actions.new'), new_referential_line_route_path(@referential, @line), class: 'add' = creation_tag(@line) diff --git a/app/views/routes/_route.html.slim b/app/views/routes/_route.html.slim index 251c92000..e273bfcfd 100644 --- a/app/views/routes/_route.html.slim +++ b/app/views/routes/_route.html.slim @@ -2,11 +2,13 @@ .panel-heading .panel-title.clearfix .btn-group.btn-group-sm.pull-right - = link_to edit_referential_line_route_path(@referential, @line, route), class: 'btn btn-default' do - span.fa.fa-pencil + - if policy(route).edit? + = link_to edit_referential_line_route_path(@referential, @line, route), class: 'btn btn-default' do + span.fa.fa-pencil - = link_to referential_line_route_path(@referential, @line, route), method: :delete, :data => {:confirm => t('routes.actions.destroy_confirm')}, class: 'btn btn-danger' do - span.fa.fa-trash-o + - if policy(route).destroy? + = link_to referential_line_route_path(@referential, @line, route), method: :delete, :data => {:confirm => t('routes.actions.destroy_confirm')}, class: 'btn btn-danger' do + span.fa.fa-trash-o h5 = link_to [@referential, @line, route], class: 'preview', title: "#{Chouette::Route.model_name.human.capitalize} #{route.name}" do diff --git a/app/views/routes/show.html.slim b/app/views/routes/show.html.slim index 3f0e22006..e18ec295d 100644 --- a/app/views/routes/show.html.slim +++ b/app/views/routes/show.html.slim @@ -15,11 +15,11 @@ / p / label = "#{@route.human_attribute_name(:number)} : " / = " #{@route.number}" - / + / / p / label = "#{@route.human_attribute_name(:comment)} : " / = " #{@route.comment}" - / + / / p / label = "#{@route.human_attribute_name(:direction)} : " / - if @route.direction @@ -47,14 +47,14 @@ p.after_map .panel-heading h4.panel-title strong = t('.stop_points') - + .list-group - @route.stop_points.each do |point| - if point.stop_area.zip_code && point.stop_area.city_name - linktxt = "#{point.stop_area.name}, #{point.stop_area.zip_code} #{point.stop_area.city_name}" - else - linktxt = "#{point.stop_area.name}" - + = link_to [@referential, point.stop_area], { style: 'display: table;width: 100%;', class: 'list-group-item', title: "Voir l'arrêt '#{linktxt}'" } do div style='display: table-cell;vertical-align: middle;' div style='display: inline-block;width: 10%;vertical-align: middle;text-align: right;' @@ -67,7 +67,7 @@ p.after_map .panel-heading h4.panel-title strong = t('.journey_patterns') - + .list-group - @route.journey_patterns.each do |journey_pattern| .list-group-item.clearfix title="#{t('journey_patterns.journey_pattern.stop_count', count: journey_pattern.stop_points.count, route_count: @route.stop_points.count)} | #{t('journey_patterns.journey_pattern.vehicle_journeys_count', count: journey_pattern.vehicle_journeys.count)}" @@ -84,17 +84,28 @@ p.after_map span.caret ul.dropdown-menu li = link_to 'Voir', [@referential, @line, @route, journey_pattern], title: "#{Chouette::JourneyPattern.model_name.human.capitalize} #{journey_name(journey_pattern)}" - li = link_to 'Supprimer', referential_line_route_journey_pattern_path(@referential, @line, @route, journey_pattern), method: :delete, data: {confirm: t('journey_patterns.actions.destroy_confirm')} - + li + - if policy(journey_pattern).edit? + = link_to t('actions.edit'), edit_referential_line_route_journey_pattern_path(@referential, @line, @route, journey_pattern) + li + - if policy(journey_pattern).destroy? + = link_to t('actions.destroy'), referential_line_route_journey_pattern_path(@referential, @line, @route, journey_pattern), method: :delete, data: {confirm: t('journey_patterns.actions.destroy_confirm')} + / .panel-body / .journey_patterns.paginated_content / = paginated_content( @route.journey_patterns, "journey_patterns/journey_pattern") - content_for :sidebar do ul.actions - li = link_to t('routes.actions.new'), new_referential_line_route_path(@referential, @line), class: 'add' - li = link_to t('routes.actions.edit'), edit_referential_line_route_path(@referential, @line, @route), class: 'edit' - li = link_to t('routes.actions.destroy'), referential_line_route_path(@referential, @line, @route), method: :delete, :data => {:confirm => t('routes.actions.destroy_confirm')}, class: 'remove' + li + - if policy(@route).create? + = link_to t('routes.actions.new'), new_referential_line_route_path(@referential, @line), class: 'add' + li + - if policy(@route).edit? + = link_to t('routes.actions.edit'), edit_referential_line_route_path(@referential, @line, @route), class: 'edit' + li + - if policy(@route).destroy? + = link_to t('routes.actions.destroy'), referential_line_route_path(@referential, @line, @route), method: :delete, :data => {:confirm => t('routes.actions.destroy_confirm')}, class: 'remove' ul.actions - if @route.stop_points.size >= 2 diff --git a/app/workers/line_referential_sync_worker.rb b/app/workers/line_referential_sync_worker.rb index b883e5180..df65fc10d 100644 --- a/app/workers/line_referential_sync_worker.rb +++ b/app/workers/line_referential_sync_worker.rb @@ -1,5 +1,7 @@ class LineReferentialSyncWorker include Sidekiq::Worker + sidekiq_options :retry => false + def process_time Process.clock_gettime(Process::CLOCK_MONOTONIC, :second) end diff --git a/app/workers/stop_area_referential_sync_worker.rb b/app/workers/stop_area_referential_sync_worker.rb index f2c6746da..dede018dd 100644 --- a/app/workers/stop_area_referential_sync_worker.rb +++ b/app/workers/stop_area_referential_sync_worker.rb @@ -1,5 +1,6 @@ class StopAreaReferentialSyncWorker include Sidekiq::Worker + sidekiq_options :retry => false def process_time Process.clock_gettime(Process::CLOCK_MONOTONIC, :second) diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb new file mode 100644 index 000000000..52ec93250 --- /dev/null +++ b/config/initializers/sidekiq.rb @@ -0,0 +1,7 @@ +Sidekiq.configure_server do |config| + pendings = [ + LineReferential.find_by(name: 'CodifLigne').line_referential_syncs.pending.take, + StopAreaReferential.find_by(name: 'Reflex').stop_area_referential_syncs.pending.take + ] + pendings.compact.map{|sync| sync.failed({error: 'Failed by Sidekiq reboot', processing_time: 0})} +end diff --git a/config/locales/users.en.yml b/config/locales/users.en.yml index 19168f560..37d96e7a3 100644 --- a/config/locales/users.en.yml +++ b/config/locales/users.en.yml @@ -20,3 +20,11 @@ en: user: name: "Full name" username: "Username" + permissions: Permissions + errors: + models: + calendar: + attributes: + permissions: + must_be_unique: User's permissions must be unique. + must_be_nonempty: A permission can't be empty. diff --git a/config/locales/users.fr.yml b/config/locales/users.fr.yml index f50218605..b0329e77e 100644 --- a/config/locales/users.fr.yml +++ b/config/locales/users.fr.yml @@ -20,5 +20,11 @@ fr: user: name: "Nom complet" username: "Nom d'utilisateur" - - + permissions: Permissions + errors: + models: + calendar: + attributes: + permissions: + must_be_unique: Une permission ne peut pas apparaître deux fois chez un même utilisateur. + must_be_nonempty: Une permission ne peut pas être une chaine vide. diff --git a/db/migrate/20170113155639_add_permissions_to_users.rb b/db/migrate/20170113155639_add_permissions_to_users.rb new file mode 100644 index 000000000..abfe14ccd --- /dev/null +++ b/db/migrate/20170113155639_add_permissions_to_users.rb @@ -0,0 +1,5 @@ +class AddPermissionsToUsers < ActiveRecord::Migration + def change + add_column :users, :permissions, :string, array: true + end +end diff --git a/db/migrate/20170118104441_add_routes_jorney_patterns_permissions_to_users.rb b/db/migrate/20170118104441_add_routes_jorney_patterns_permissions_to_users.rb new file mode 100644 index 000000000..e6acf0068 --- /dev/null +++ b/db/migrate/20170118104441_add_routes_jorney_patterns_permissions_to_users.rb @@ -0,0 +1,8 @@ +class AddRoutesJorneyPatternsPermissionsToUsers < ActiveRecord::Migration + def change + User.find_each do |user| + user.permissions = ['routes.create', 'routes.edit', 'routes.destroy', 'journey_patterns.create', 'journey_patterns.edit', 'journey_patterns.destroy'] + user.save! + end + end +end diff --git a/db/schema.rb b/db/schema.rb index c9ef05ab1..258fb3c22 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170106135000) do +ActiveRecord::Schema.define(version: 20170118104441) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -161,22 +161,6 @@ ActiveRecord::Schema.define(version: 20170106135000) do add_index "connection_links", ["objectid"], :name => "connection_links_objectid_key", :unique => true - create_table "delayed_jobs", force: true do |t| - t.integer "priority", default: 0 - t.integer "attempts", default: 0 - t.text "handler" - t.text "last_error" - t.datetime "run_at" - t.datetime "locked_at" - t.datetime "failed_at" - t.string "locked_by" - t.string "queue" - t.datetime "created_at" - t.datetime "updated_at" - end - - add_index "delayed_jobs", ["priority", "run_at"], :name => "delayed_jobs_priority" - create_table "exports", force: true do |t| t.integer "referential_id", limit: 8 t.string "status" @@ -718,6 +702,7 @@ ActiveRecord::Schema.define(version: 20170106135000) do t.datetime "invitation_created_at" t.string "username" t.datetime "synced_at" + t.string "permissions", array: true end add_index "users", ["email"], :name => "index_users_on_email", :unique => true @@ -777,8 +762,6 @@ ActiveRecord::Schema.define(version: 20170106135000) do add_index "workbenches", ["stop_area_referential_id"], :name => "index_workbenches_on_stop_area_referential_id" Foreigner.load - add_foreign_key "access_links", "access_points", name: "aclk_acpt_fkey", dependent: :delete - add_foreign_key "group_of_lines_lines", "group_of_lines", name: "groupofline_group_fkey", dependent: :delete add_foreign_key "journey_frequencies", "timebands", name: "journey_frequencies_timeband_id_fk", dependent: :nullify diff --git a/spec/features/journey_pattern_spec.rb b/spec/features/journey_pattern_spec.rb index 1dbd2752d..380241099 100644 --- a/spec/features/journey_pattern_spec.rb +++ b/spec/features/journey_pattern_spec.rb @@ -8,6 +8,53 @@ describe "JourneyPatterns", :type => :feature do let!(:route) { create(:route, :line => line) } let!(:journey_pattern) { create(:journey_pattern, :route => route) } + describe 'show' do + context 'user has permission to create journey patterns' do + it 'shows the create link for journey pattern' do + visit referential_line_route_journey_pattern_path(referential, line, route, journey_pattern) + expect(page).to have_content(I18n.t('journey_patterns.actions.new')) + end + end + + context 'user does not have permission to create journey patterns' do + it 'does not show the create link for journey pattern' do + @user.update_attribute(:permissions, ['journey_patterns.edit', 'journey_patterns.destroy']) + visit referential_line_route_journey_pattern_path(referential, line, route, journey_pattern) + expect(page).not_to have_content(I18n.t('journey_patterns.actions.new')) + end + end + + context 'user has permission to edit journey patterns' do + it 'shows the edit link for journey pattern' do + visit referential_line_route_journey_pattern_path(referential, line, route, journey_pattern) + expect(page).to have_content(I18n.t('journey_patterns.actions.edit')) + end + end + + context 'user does not have permission to edit journey patterns' do + it 'does not show the edit link for journey pattern' do + @user.update_attribute(:permissions, ['journey_patterns.create', 'journey_patterns.destroy']) + visit referential_line_route_journey_pattern_path(referential, line, route, journey_pattern) + expect(page).not_to have_content(I18n.t('journey_patterns.actions.edit')) + end + end + + context 'user has permission to destroy journey patterns' do + it 'shows the destroy link for journey pattern' do + visit referential_line_route_journey_pattern_path(referential, line, route, journey_pattern) + expect(page).to have_content(I18n.t('journey_patterns.actions.destroy')) + end + end + + context 'user does not have permission to edit journey patterns' do + it 'does not show the destroy link for journey pattern' do + @user.update_attribute(:permissions, ['journey_patterns.create', 'journey_patterns.edit']) + visit referential_line_route_journey_pattern_path(referential, line, route, journey_pattern) + expect(page).not_to have_content(I18n.t('journey_patterns.actions.destroy')) + end + end + end + # describe "from routes page to a journey_pattern page" do # it "display route's journey_patterns" do # visit referential_line_route_path(referential,line,route) diff --git a/spec/features/routes_spec.rb b/spec/features/routes_spec.rb index 5aea98c90..bc2088712 100644 --- a/spec/features/routes_spec.rb +++ b/spec/features/routes_spec.rb @@ -54,4 +54,82 @@ describe "Routes", :type => :feature do end end + describe 'show' do + context 'user has permission to edit journey patterns' do + it 'shows edit links for journey patterns' do + visit referential_line_route_path(referential, line, route) + expect(page).to have_content(I18n.t('actions.edit')) + end + end + + context 'user does not have permission to edit journey patterns' do + it 'does not show edit links for journey patterns' do + @user.update_attribute(:permissions, ['journey_patterns.create', 'journey_patterns.destroy']) + visit referential_line_route_path(referential, line, route) + expect(page).not_to have_content(I18n.t('actions.edit')) + end + end + + context 'user has permission to destroy journey patterns' do + it 'shows destroy links for journey patterns' do + visit referential_line_route_path(referential, line, route) + expect(page).to have_content(I18n.t('actions.destroy')) + end + end + + context 'user does not have permission to edit journey patterns' do + it 'does not show destroy links for journey patterns' do + @user.update_attribute(:permissions, ['journey_patterns.create', 'journey_patterns.edit']) + visit referential_line_route_path(referential, line, route) + expect(page).not_to have_content(I18n.t('actions.destroy')) + end + end + end + + describe 'referential line show' do + context 'user has permission to edit routes' do + it 'shows edit buttons for routes' do + visit referential_line_path(referential, line) + expect(page).to have_css('span.fa.fa-pencil') + end + end + + context 'user does not have permission to edit routes' do + it 'does not show edit buttons for routes' do + @user.update_attribute(:permissions, ['routes.create', 'routes.destroy']) + visit referential_line_path(referential, line) + expect(page).not_to have_css('span.fa.fa-pencil') + end + end + + context 'user has permission to create routes' do + it 'shows link to a create route page' do + visit referential_line_path(referential, line) + expect(page).to have_content(I18n.t('routes.actions.new')) + end + end + + context 'user does not have permission to create routes' do + it 'does not show link to a create route page' do + @user.update_attribute(:permissions, ['routes.edit', 'routes.destroy']) + visit referential_line_path(referential, line) + expect(page).not_to have_content(I18n.t('routes.actions.new')) + end + end + + context 'user has permission to destroy routes' do + it 'shows destroy buttons for routes' do + visit referential_line_path(referential, line) + expect(page).to have_css('span.fa.fa-trash-o') + end + end + + context 'user does not have permission to destroy routes' do + it 'does not show destroy buttons for routes' do + @user.update_attribute(:permissions, ['routes.edit', 'routes.create']) + visit referential_line_path(referential, line) + expect(page).not_to have_css('span.fa.fa-trash-o') + end + end + end end diff --git a/spec/javascripts/journey_patterns/actions_spec.js b/spec/javascripts/journey_patterns/actions_spec.js index ced053935..07f83ca1b 100644 --- a/spec/javascripts/journey_patterns/actions_spec.js +++ b/spec/javascripts/journey_patterns/actions_spec.js @@ -14,25 +14,38 @@ describe('when receiveJourneyPatterns is triggered', () => { }) }) -describe('when landing on page', () => { - it('should create an action to load the n first missions', () => { +describe('when previous navigation button is clicked', () => { + it('should create an action to go to previous page', () => { + const nextPage = false + const pagination = { + totalCount: 25, + perPage: 12, + page:1 + } const expectedAction = { - type: 'LOAD_FIRST_PAGE', - dispatch + type: 'GO_TO_PREVIOUS_PAGE', + dispatch, + pagination, + nextPage } - expect(actions.loadFirstPage(dispatch)).toEqual(expectedAction) + expect(actions.goToPreviousPage(dispatch, pagination)).toEqual(expectedAction) }) }) describe('when next navigation button is clicked', () => { it('should create an action to go to next page', () => { const nextPage = true + const pagination = { + totalCount: 25, + perPage: 12, + page:1 + } const expectedAction = { type: 'GO_TO_NEXT_PAGE', dispatch, - currentPage, + pagination, nextPage } - expect(actions.goToNextPage(dispatch, currentPage)).toEqual(expectedAction) + expect(actions.goToNextPage(dispatch, pagination)).toEqual(expectedAction) }) }) describe('when clicking on a journey pattern checkbox', () => { @@ -53,13 +66,12 @@ describe('when clicking on a journey pattern checkbox', () => { }) describe('when clicking on next button', () => { it('should create an action to open a confirm modal', () => { - const accept = {}, cancel = {} + const callback = function(){} const expectedAction = { type: 'OPEN_CONFIRM_MODAL', - accept, - cancel, + callback } - expect(actions.openConfirmModal(accept, cancel)).toEqual(expectedAction) + expect(actions.openConfirmModal(callback)).toEqual(expectedAction) }) }) describe('when clicking on edit button', () => { @@ -122,12 +134,21 @@ describe('when clicking on validate button inside create modal', () => { expect(actions.addJourneyPattern(data)).toEqual(expectedAction) }) }) -describe('when clicking on validate button at the bottom of the page', () => { - it('should create an action to post data and save it into db', () => { +describe('when submitting new journeyPatterns', () => { + it('should create an action to update pagination totalCount', () => { + const diff = 1 + const expectedAction = { + type: 'UPDATE_TOTAL_COUNT', + diff + } + expect(actions.updateTotalCount(diff)).toEqual(expectedAction) + }) +}) +describe('when fetching api', () => { + it('should create an action to fetch api', () => { const expectedAction = { - type: 'SAVE_PAGE', - dispatch + type: 'FETCH_API', } - expect(actions.savePage(dispatch)).toEqual(expectedAction) + expect(actions.fetchingApi()).toEqual(expectedAction) }) }) diff --git a/spec/javascripts/journey_patterns/reducers/modal_spec.js b/spec/javascripts/journey_patterns/reducers/modal_spec.js index 46ab2d905..0bc7c9240 100644 --- a/spec/javascripts/journey_patterns/reducers/modal_spec.js +++ b/spec/javascripts/journey_patterns/reducers/modal_spec.js @@ -11,8 +11,7 @@ let fakeJourneyPattern = { deletable: false } -const accept = function(){} -const cancel = function(){} +const cb = function(){} describe('modal reducer', () => { beforeEach(() => { @@ -33,15 +32,13 @@ describe('modal reducer', () => { let newState = Object.assign({}, state, { type: 'confirm', confirmModal: { - accept: accept, - cancel: cancel + callback: cb } }) expect( modalReducer(state, { type: 'OPEN_CONFIRM_MODAL', - accept, - cancel + callback: cb }) ).toEqual(newState) }) diff --git a/spec/javascripts/journey_patterns/reducers/pagination_spec.js b/spec/javascripts/journey_patterns/reducers/pagination_spec.js index a99e8ff85..4800451e9 100644 --- a/spec/javascripts/journey_patterns/reducers/pagination_spec.js +++ b/spec/javascripts/journey_patterns/reducers/pagination_spec.js @@ -1,9 +1,13 @@ var reducer = require('es6_browserified/journey_patterns/reducers/pagination') + +const diff = 1 let state = { page : 2, - totalCount : 25 + totalCount : 25, + stateChanged: false, + perPage: 12 } -let currentPage = 2 +let pagination = Object.assign({}, state) const dispatch = function(){} describe('pagination reducer, given parameters allowing page change', () => { @@ -19,10 +23,10 @@ describe('pagination reducer, given parameters allowing page change', () => { reducer(state, { type: 'GO_TO_NEXT_PAGE', dispatch, - currentPage, + pagination, nextPage : true }) - ).toEqual(Object.assign({}, state, {page : state.page + 1})) + ).toEqual(Object.assign({}, state, {page : state.page + 1, stateChanged: false})) }) it('should return GO_TO_PREVIOUS_PAGE and change state', () => { @@ -30,10 +34,10 @@ describe('pagination reducer, given parameters allowing page change', () => { reducer(state, { type: 'GO_TO_PREVIOUS_PAGE', dispatch, - currentPage, + pagination, nextPage : false }) - ).toEqual(Object.assign({}, state, {page : state.page - 1})) + ).toEqual(Object.assign({}, state, {page : state.page - 1, stateChanged: false})) }) }) @@ -42,7 +46,7 @@ describe('pagination reducer, given parameters not allowing to go to previous pa beforeEach(()=>{ state.page = 1 - currentPage = 1 + pagination.page = 1 }) it('should return GO_TO_PREVIOUS_PAGE and not change state', () => { @@ -50,7 +54,7 @@ describe('pagination reducer, given parameters not allowing to go to previous pa reducer(state, { type: 'GO_TO_PREVIOUS_PAGE', dispatch, - currentPage, + pagination, nextPage : false }) ).toEqual(state) @@ -61,7 +65,7 @@ describe('pagination reducer, given parameters not allowing to go to next page', beforeEach(()=>{ state.page = 3 - currentPage = 3 + pagination.page = 3 }) it('should return GO_TO_NEXT_PAGE and not change state', () => { @@ -69,9 +73,21 @@ describe('pagination reducer, given parameters not allowing to go to next page', reducer(state, { type: 'GO_TO_NEXT_PAGE', dispatch, - currentPage, - nextPage : false + pagination, + nextPage : true }) ).toEqual(state) }) }) + +describe('pagination reducer, given parameters changing totalCount', () => { + + it('should return UPDATE_TOTAL_COUNT and update totalCount', () => { + expect( + reducer(state, { + type: 'UPDATE_TOTAL_COUNT', + diff + }) + ).toEqual(Object.assign({}, state, {totalCount: state.totalCount - diff})) + }) +}) diff --git a/spec/javascripts/journey_patterns/reducers/status_spec.js b/spec/javascripts/journey_patterns/reducers/status_spec.js new file mode 100644 index 000000000..91cbbb0b8 --- /dev/null +++ b/spec/javascripts/journey_patterns/reducers/status_spec.js @@ -0,0 +1,51 @@ +var statusReducer = require('es6_browserified/journey_patterns/reducers/status') + +let state = {} + +let pagination = { + page : 2, + totalCount : 25, + stateChanged: false, + perPage: 12 +} +const dispatch = function(){} + +describe('status reducer', () => { + beforeEach(() => { + state = { + fetchSuccess: true, + isFetching: false + } + }) + + it('should return the initial state', () => { + expect( + statusReducer(undefined, {}) + ).toEqual({}) + }) + + it('should handle UNAVAILABLE_SERVER', () => { + expect( + statusReducer(state, { + type: 'UNAVAILABLE_SERVER' + }) + ).toEqual(Object.assign({}, state, {fetchSuccess: false})) + }) + + it('should handle RECEIVE_JOURNEY_PATTERNS', () => { + expect( + statusReducer(state, { + type: 'RECEIVE_JOURNEY_PATTERNS' + }) + ).toEqual(Object.assign({}, state, {fetchSuccess: true, isFetching: false})) + }) + + it('should handle FETCH_API', () => { + expect( + statusReducer(state, { + type: 'FETCH_API' + }) + ).toEqual(Object.assign({}, state, {isFetching: true})) + }) + +}) diff --git a/spec/javascripts/spec_helper.js b/spec/javascripts/spec_helper.js index a2fde3860..a0285cccf 100644 --- a/spec/javascripts/spec_helper.js +++ b/spec/javascripts/spec_helper.js @@ -4,6 +4,8 @@ // require support/jasmine-jquery-2.1.0 // require support/sinon // require support/your-support-file +//= require jquery +//= require bootstrap-sass-official require('es6-object-assign').polyfill(); // // PhantomJS (Teaspoons default driver) doesn't have support for Function.prototype.bind, which has caused confusion. diff --git a/spec/models/chouette/journey_pattern_spec.rb b/spec/models/chouette/journey_pattern_spec.rb index 68c221c9a..19b5060d2 100644 --- a/spec/models/chouette/journey_pattern_spec.rb +++ b/spec/models/chouette/journey_pattern_spec.rb @@ -1,11 +1,101 @@ require 'spec_helper' describe Chouette::JourneyPattern, :type => :model do + + describe "state_update" do + def journey_pattern_to_state jp + jp.attributes.slice('name', 'published_name', 'registration_number').tap do |item| + item['object_id'] = jp.objectid + item['stop_points'] = jp.stop_points.map do |sp| + { 'id' => sp.stop_area_id } + end + end + end + + let(:route) { create :route } + let(:journey_pattern) { create :journey_pattern, route: route } + let(:state) { journey_pattern_to_state(journey_pattern) } + + it 'should delete unchecked stop_points' do + # Of 5 stop_points 2 are checked + state['stop_points'].take(2).each{|sp| sp['checked'] = true} + journey_pattern.state_stop_points_update(state) + expect(journey_pattern.stop_points.count).to eq(2) + end + + it 'should attach checked stop_points' do + state['stop_points'].each{|sp| sp['checked'] = true} + # Make sure journey_pattern has no stop_points + journey_pattern.stop_points.delete_all + expect(journey_pattern.reload.stop_points).to be_empty + + journey_pattern.state_stop_points_update(state) + expect(journey_pattern.reload.stop_points.count).to eq(5) + end + + it 'should create journey_pattern' do + new_state = journey_pattern_to_state(build(:journey_pattern, objectid: nil, route: route)) + Chouette::JourneyPattern.state_create_instance route, new_state + expect(new_state['object_id']).to be_truthy + expect(new_state['new_record']).to be_truthy + end + + it 'should delete journey_pattern' do + state['deletable'] = true + collection = [state] + expect { + Chouette::JourneyPattern.state_update route, collection + }.to change{Chouette::JourneyPattern.count}.from(1).to(0) + + expect(collection).to be_empty + end + + it 'should delete multiple journey_pattern' do + collection = 5.times.collect{journey_pattern_to_state(create(:journey_pattern, route: route))} + collection.map{|i| i['deletable'] = true} + + expect { + Chouette::JourneyPattern.state_update route, collection + }.to change{Chouette::JourneyPattern.count}.from(5).to(0) + end + + it 'should validate journey_pattern on update' do + journey_pattern.name = '' + collection = [state] + Chouette::JourneyPattern.state_update route, collection + expect(collection.first['errors']).to have_key(:name) + end + + it 'should validate journey_pattern on create' do + new_state = journey_pattern_to_state(build(:journey_pattern, name: '', objectid: nil, route: route)) + collection = [new_state] + expect { + Chouette::JourneyPattern.state_update route, collection + }.to_not change{Chouette::JourneyPattern.count} + + expect(collection.first['errors']).to have_key(:name) + expect(collection.first).to_not have_key('object_id') + end + + it 'should not save any journey_pattern of collection if one is invalid' do + journey_pattern.name = '' + valid_state = journey_pattern_to_state(build(:journey_pattern, objectid: nil, route: route)) + invalid_state = journey_pattern_to_state(journey_pattern) + collection = [valid_state, invalid_state] + + expect { + Chouette::JourneyPattern.state_update route, collection + }.to_not change{Chouette::JourneyPattern.count} + + expect(collection.first).to_not have_key('object_id') + end + end + describe "#stop_point_ids" do context "for a journey_pattern using only route's stop on odd position" do let!(:journey_pattern){ create( :journey_pattern_odd)} let!(:vehicle_journey){ create( :vehicle_journey_odd, :journey_pattern => journey_pattern)} - + # workaroud #subject { journey_pattern} subject { Chouette::JourneyPattern.find(vehicle_journey.journey_pattern_id)} @@ -23,7 +113,7 @@ describe Chouette::JourneyPattern, :type => :model do expect(subject.departure_stop_point_id).to be_nil end end - + context "when a route's stop has been removed from journey_pattern" do let!(:last_stop_id){ subject.stop_point_ids.last} before(:each) do @@ -41,7 +131,7 @@ describe Chouette::JourneyPattern, :type => :model do expect(subject.departure_stop_point_id).to eq(ordered.first.id) end end - + context "when a route's stop has been added in journey_pattern" do let!(:new_stop){ subject.route.stop_points[1]} before(:each) do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 7d0a548c1..bbeb0caf5 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -13,7 +13,8 @@ describe User, :type => :model do :email => 'john.doe@af83.com', :organisation_code => '0083', :organisation_name => 'af83', - :functional_scope => "[\"STIF:CODIFLIGNE:Line:C00840\", \"STIF:CODIFLIGNE:Line:C00086\"]" + :functional_scope => "[\"STIF:CODIFLIGNE:Line:C00840\", \"STIF:CODIFLIGNE:Line:C00086\"]", + :permissions => nil } ticket.user = "john.doe" ticket.success = true @@ -115,6 +116,22 @@ describe User, :type => :model do end end + describe 'validations' do + it 'validates uniqueness of pemissions' do + user = build :user, permissions: Array.new(2, 'calendars.shared') + expect { + user.save! + }.to raise_error(ActiveRecord::RecordInvalid) + end + + it 'validates no pemission is an empty string' do + user = build :user, permissions: [''] + expect { + user.save! + }.to raise_error(ActiveRecord::RecordInvalid) + end + end + describe "#destroy" do let!(:organisation){create(:organisation)} let!(:user){create(:user, :organisation => organisation)} diff --git a/spec/support/devise.rb b/spec/support/devise.rb index 2258fefbb..7cfa17f44 100644 --- a/spec/support/devise.rb +++ b/spec/support/devise.rb @@ -3,7 +3,8 @@ module DeviseRequestHelper def login_user organisation = Organisation.where(:code => "first").first_or_create(attributes_for(:organisation)) - @user ||= create(:user, :organisation => organisation) + @user ||= create(:user, :organisation => organisation, + :permissions => ['routes.create', 'routes.edit', 'routes.destroy', 'journey_patterns.create', 'journey_patterns.edit', 'journey_patterns.destroy']) login_as @user, :scope => :user # post_via_redirect user_session_path, 'user[email]' => @user.email, 'user[password]' => @user.password end @@ -34,7 +35,8 @@ module DeviseControllerHelper before(:each) do @request.env["devise.mapping"] = Devise.mappings[:user] organisation = Organisation.where(:code => "first").first_or_create(attributes_for(:organisation)) - user = create(:user, :organisation => organisation) + user = create(:user, :organisation => organisation, + :permissions => ['routes.create', 'routes.edit', 'routes.destroy', 'journey_patterns.create', 'journey_patterns.edit', 'journey_patterns.destroy']) sign_in user end end |
