aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorXinhui2017-01-18 13:52:49 +0100
committerXinhui2017-01-18 13:52:49 +0100
commit2522ecae50da95a2fb73a49e96993361e7b7e217 (patch)
treeef60ed49cc3102a5b85b30c0dcbde1884770afbe
parent02a6b47c69df2b8364f85c205cc4381f250bd177 (diff)
parent6ebc870abfe865b57c4df7a872f6a7a7cd71458e (diff)
downloadchouette-core-2522ecae50da95a2fb73a49e96993361e7b7e217.tar.bz2
Merge branch 'master' into staging
-rw-r--r--app/assets/images/loader.svg6
-rw-r--r--app/assets/javascripts/es6_browserified/journey_patterns/actions/index.js133
-rw-r--r--app/assets/javascripts/es6_browserified/journey_patterns/components/ConfirmModal.js4
-rw-r--r--app/assets/javascripts/es6_browserified/journey_patterns/components/CreateModal.js158
-rw-r--r--app/assets/javascripts/es6_browserified/journey_patterns/components/EditModal.js70
-rw-r--r--app/assets/javascripts/es6_browserified/journey_patterns/components/JourneyPattern.js88
-rw-r--r--app/assets/javascripts/es6_browserified/journey_patterns/components/JourneyPatterns.js40
-rw-r--r--app/assets/javascripts/es6_browserified/journey_patterns/components/Navigate.js54
-rw-r--r--app/assets/javascripts/es6_browserified/journey_patterns/containers/AddJourneyPattern.js3
-rw-r--r--app/assets/javascripts/es6_browserified/journey_patterns/containers/ConfirmModal.js2
-rw-r--r--app/assets/javascripts/es6_browserified/journey_patterns/containers/JourneyPatternList.js6
-rw-r--r--app/assets/javascripts/es6_browserified/journey_patterns/containers/Navigate.js42
-rw-r--r--app/assets/javascripts/es6_browserified/journey_patterns/containers/SaveJourneyPattern.js40
-rw-r--r--app/assets/javascripts/es6_browserified/journey_patterns/index.js8
-rw-r--r--app/assets/javascripts/es6_browserified/journey_patterns/reducers/index.js2
-rw-r--r--app/assets/javascripts/es6_browserified/journey_patterns/reducers/journeyPatterns.js16
-rw-r--r--app/assets/javascripts/es6_browserified/journey_patterns/reducers/modal.js4
-rw-r--r--app/assets/javascripts/es6_browserified/journey_patterns/reducers/pagination.js25
-rw-r--r--app/assets/javascripts/es6_browserified/journey_patterns/reducers/status.js16
-rw-r--r--app/assets/stylesheets/components/_form.sass17
-rw-r--r--app/assets/stylesheets/components/_loader.sass9
-rw-r--r--app/controllers/journey_patterns_collections_controller.rb60
-rw-r--r--app/controllers/journey_patterns_controller.rb7
-rw-r--r--app/controllers/routes_controller.rb7
-rw-r--r--app/models/chouette/journey_pattern.rb57
-rw-r--r--app/models/line_referential_sync.rb6
-rw-r--r--app/models/stop_area_referential_sync.rb4
-rw-r--r--app/models/user.rb12
-rw-r--r--app/policies/journey_pattern_policy.rb22
-rw-r--r--app/policies/route_policy.rb22
-rw-r--r--app/views/journey_patterns/show.html.slim13
-rw-r--r--app/views/journey_patterns_collections/show.html.slim3
-rw-r--r--app/views/referential_lines/_reflines_routes.html.slim16
-rw-r--r--app/views/referential_lines/show.html.slim3
-rw-r--r--app/views/routes/_route.html.slim10
-rw-r--r--app/views/routes/show.html.slim31
-rw-r--r--app/workers/line_referential_sync_worker.rb2
-rw-r--r--app/workers/stop_area_referential_sync_worker.rb1
-rw-r--r--config/initializers/sidekiq.rb7
-rw-r--r--config/locales/users.en.yml8
-rw-r--r--config/locales/users.fr.yml10
-rw-r--r--db/migrate/20170113155639_add_permissions_to_users.rb5
-rw-r--r--db/migrate/20170118104441_add_routes_jorney_patterns_permissions_to_users.rb8
-rw-r--r--db/schema.rb21
-rw-r--r--spec/features/journey_pattern_spec.rb47
-rw-r--r--spec/features/routes_spec.rb78
-rw-r--r--spec/javascripts/journey_patterns/actions_spec.js53
-rw-r--r--spec/javascripts/journey_patterns/reducers/modal_spec.js9
-rw-r--r--spec/javascripts/journey_patterns/reducers/pagination_spec.js38
-rw-r--r--spec/javascripts/journey_patterns/reducers/status_spec.js51
-rw-r--r--spec/javascripts/spec_helper.js2
-rw-r--r--spec/models/chouette/journey_pattern_spec.rb96
-rw-r--r--spec/models/user_spec.rb19
-rw-r--r--spec/support/devise.rb6
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