diff options
| author | Xinhui | 2017-06-16 17:58:08 +0200 |
|---|---|---|
| committer | Xinhui | 2017-06-16 17:58:08 +0200 |
| commit | 641b1458236d2718a76ffaf0c04a5998623276bf (patch) | |
| tree | db738af25bcdd7b3b400a0f8db5e6350ef373c51 | |
| parent | 6886441ce86bcd720b27cdd089567def5b9d771a (diff) | |
| parent | 9ef3d205aa091d509455b3607d5ecc74431c6196 (diff) | |
| download | chouette-core-641b1458236d2718a76ffaf0c04a5998623276bf.tar.bz2 | |
Merge branch 'master' into staging
156 files changed, 2308 insertions, 1035 deletions
@@ -20,8 +20,6 @@ gem 'browserify-rails' # Use jquery as the JavaScript library gem 'jquery-rails', '~> 3.1.4' # Update to v4 for Rails 4.2 -# Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks -gem 'turbolinks' # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder gem 'jbuilder', '~> 2.0' # bundle exec rake doc:rails generates the API under doc/api. @@ -112,6 +110,8 @@ gem 'ransack' gem "squeel", github: 'activerecord-hackery/squeel' gem 'active_attr' +gem 'sequel' + gem 'draper' gem 'enumerize', '~> 0.10.0' @@ -148,6 +148,8 @@ group :development do gem 'quiet_assets' gem 'license_finder' gem 'bundler-audit' + gem 'spring-commands-rspec' + gem 'dbshell-rails' platforms :ruby_20, :ruby_21, :ruby_22 do gem 'better_errors' diff --git a/Gemfile.lock b/Gemfile.lock index 4e1337ec5..e87e6bb3b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -9,10 +9,10 @@ GIT GIT remote: git@github.com:af83/stif-codifline-api.git - revision: 02108a647514ca36e4377deecf3ffcce99359139 + revision: c0efb26bf202e0770348bdac060b14c28e575ac2 specs: codifligne (0.0.2) - nokogiri (~> 1.6) + nokogiri (>= 1.7.2) GIT remote: git@github.com:af83/stif-reflex-api.git @@ -180,6 +180,7 @@ GEM daemons (1.2.4) database_cleaner (1.5.3) dbf (3.1.0) + dbshell-rails (0.0.2) debug_inspector (0.0.2) deep_cloneable (2.0.2) activerecord (>= 3.1.0, < 5.0.0) @@ -307,7 +308,7 @@ GEM mime-types-data (~> 3.2015) mime-types-data (3.2016.0521) mimemagic (0.3.2) - mini_portile2 (2.1.0) + mini_portile2 (2.2.0) minitest (5.10.1) multi_json (1.12.1) multi_test (0.1.2) @@ -321,8 +322,8 @@ GEM net-ssh-gateway (2.0.0) net-ssh (>= 4.0.0) newrelic_rpm (4.0.0.332) - nokogiri (1.7.1) - mini_portile2 (~> 2.1.0) + nokogiri (1.8.0) + mini_portile2 (~> 2.2.0) open4 (1.3.4) orm_adapter (0.5.0) parser (2.4.0.0) @@ -461,6 +462,7 @@ GEM rdoc (~> 4.0) select2-rails (4.0.3) thor (~> 0.14) + sequel (4.47.0) shoulda-matchers (3.1.1) activesupport (>= 4.0.0) sidekiq (4.2.10) @@ -492,6 +494,8 @@ GEM slop (3.6.0) spring (2.0.1) activesupport (>= 4.2) + spring-commands-rspec (1.0.4) + spring (>= 0.9.1) sprockets (2.12.4) hike (~> 1.2) multi_json (~> 1.0) @@ -523,9 +527,6 @@ GEM json (>= 1.8, < 3.0) parser (>= 2.3.0.7) rainbow (>= 1.99.1, < 3.0) - turbolinks (5.0.1) - turbolinks-source (~> 5) - turbolinks-source (5.0.0) tzinfo (1.2.3) thread_safe (~> 0.1) uglifier (2.7.2) @@ -580,6 +581,7 @@ DEPENDENCIES cucumber-rails daemons database_cleaner + dbshell-rails deep_cloneable (~> 2.0.0) devise (~> 3.5.4) devise-async @@ -646,6 +648,7 @@ DEPENDENCIES sawyer (~> 0.6.0) sdoc (~> 0.4.0) select2-rails (~> 4.0, >= 4.0.3) + sequel shoulda-matchers (~> 3.1) sidekiq simple_form (~> 3.1.0) @@ -654,12 +657,12 @@ DEPENDENCIES sinatra slim-rails (~> 3.1) spring + spring-commands-rspec sqlite3 squeel! teaspoon-jasmine therubyracer (~> 0.12) transpec - turbolinks uglifier (~> 2.7.2) webmock whenever! diff --git a/INSTALL.md b/INSTALL.md index 1d5badb79..330230a40 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -19,7 +19,7 @@ Go into your local repro and install the gems bundle -### Caveats +### Installation Caveats `libv8` might cause you troubles, depending on your local configuration. If you have `libv8` installed (probably because of `node.js`) you might need to tell bundler/Rubygems to use the system version. @@ -40,6 +40,18 @@ You will get the correct value of `<version>` from bundler's error message. As documented [here](https://github.com/dryade/georuby-ext/issues/2) we need some more libs before we can start the `rake` setup tasks. On mac/OS the easiest way is just to install `postgis` now with `homebrew` as this will install all needed libraries. +Also if on Linux you might discover a problem as late as when launching `rake`. +In case of a stacktrace similar to this one + +``` +$ bundle exec rake --trace -T +rake aborted! +LoadError: library names list must not be empty +``` + +you need to install `libproj4-dev` on your system. + + ### Postgres #### Create user diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index b90f7539d..0024b62b5 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -4,7 +4,6 @@ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the // the compiled file. // -//= require turbolinks //= require jquery //= require jquery_ujs //= require modernizr diff --git a/app/assets/javascripts/cleanup.coffee b/app/assets/javascripts/cleanup.coffee new file mode 100644 index 000000000..169a006a9 --- /dev/null +++ b/app/assets/javascripts/cleanup.coffee @@ -0,0 +1,9 @@ +$(document).on("change", 'input[name="clean_up[date_type]"]', (e) -> + type = $(this).val() + end_date = $('.cleanup_end_date_wrapper') + + if type == 'between' + end_date.removeClass('hidden').show() + else + end_date.hide() +) diff --git a/app/assets/javascripts/es6_browserified/itineraries/index.js b/app/assets/javascripts/es6_browserified/itineraries/index.js index 53f7bc7f9..2f1e9d180 100644 --- a/app/assets/javascripts/es6_browserified/itineraries/index.js +++ b/app/assets/javascripts/es6_browserified/itineraries/index.js @@ -38,7 +38,7 @@ const getInitialState = () => { for_alighting: v.for_alighting || "normal", longitude: v.longitude || 0, latitude: v.latitude || 0, - comment: v.comment, + comment: v.comment ? v.comment.replace("'", "\'") : '', olMap: { isOpened: false, json: {} @@ -66,11 +66,31 @@ render( document.querySelector('input[name=commit]').addEventListener('click', (event)=>{ let state = store.getState() - state.stopPoints.map((stopPoint, i) => { - addInput('id', (datas[i]) ? datas[i].stoppoint_id : '', i) - addInput('stop_area_id',stopPoint.stoparea_id, i) - addInput('position',i, i) - addInput('for_boarding',stopPoint.for_boarding, i) - addInput('for_alighting',stopPoint.for_alighting, i) - }) + + if(state.stopPoints.length >= 2) { + state.stopPoints.map((stopPoint, i) => { + addInput('id', (datas[i]) ? datas[i].stoppoint_id : '', i) + addInput('stop_area_id',stopPoint.stoparea_id, i) + addInput('position',i, i) + addInput('for_boarding',stopPoint.for_boarding, i) + addInput('for_alighting',stopPoint.for_alighting, i) + }) + if(state.stopPoints.length < datas.length){ + for(var j= state.stopPoints.length; j < datas.length; j++){ + updateFormForDeletion(datas[j]) + } + } + } else { + event.preventDefault() + let msg = "L'itinéraire doit comporter au moins deux arrêts" + $('#stop_points').find('.subform').after("<div class='alert alert-danger'><span class='fa fa-lg fa-exclamation-circle'></span><span>" + msg + "</span></div>") + } }) + +const updateFormForDeletion = (stop) =>{ + if (stop.stoppoint_id !== undefined){ + let now = Date.now() + addInput('id', stop.stoppoint_id, now) + addInput('_destroy', 'true', now) + } +} diff --git a/app/assets/javascripts/es6_browserified/itineraries/reducers/stopPoints.js b/app/assets/javascripts/es6_browserified/itineraries/reducers/stopPoints.js index 18fee8bd1..24c3e5d87 100644 --- a/app/assets/javascripts/es6_browserified/itineraries/reducers/stopPoints.js +++ b/app/assets/javascripts/es6_browserified/itineraries/reducers/stopPoints.js @@ -19,13 +19,6 @@ const stopPoint = (state = {}, action, length) => { return state } } -const updateFormForDeletion = (stop) =>{ - if (stop.stoppoint_id !== undefined){ - let now = Date.now() - addInput('id', stop.stoppoint_id, now) - addInput('_destroy', 'true', now) - } -} const stopPoints = (state = [], action) => { switch (action.type) { @@ -49,7 +42,6 @@ const stopPoints = (state = [], action) => { ...state.slice(action.index + 2) ] case 'DELETE_STOP': - updateFormForDeletion(state[action.index]) return [ ...state.slice(0, action.index), ...state.slice(action.index + 1).map((stopPoint)=>{ 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 aa2d208df..5aade3348 100644 --- a/app/assets/javascripts/es6_browserified/journey_patterns/components/CreateModal.js +++ b/app/assets/javascripts/es6_browserified/journey_patterns/components/CreateModal.js @@ -22,90 +22,90 @@ class CreateModal extends Component { } if(this.props.status.fetchSuccess == true) { return ( - <div className='row mt-md'> - <div className='col-lg-12 text-right'> - <button - type='button' - className='btn btn-outline-primary' - data-toggle='modal' - data-target='#NewJourneyPatternModal' - onClick={this.props.onOpenCreateModal} - > - Ajouter une mission - </button> + <div className="select_toolbox"> + <ul> + <li className='st_action'> + <button + type='button' + data-toggle='modal' + data-target='#NewJourneyPatternModal' + onClick={this.props.onOpenCreateModal} + > + <span className="fa fa-plus"></span> + </button> - <div className={ 'modal fade ' + ((this.props.modal.type == 'create') ? 'in' : '') } id='NewJourneyPatternModal'> - <div className='modal-container'> - <div className='modal-dialog'> - <div className='modal-content'> - <div className='modal-header'> - <h4 className='modal-title'>Ajouter une mission</h4> - </div> + <div className={ 'modal fade ' + ((this.props.modal.type == 'create') ? 'in' : '') } id='NewJourneyPatternModal'> + <div className='modal-container'> + <div className='modal-dialog'> + <div className='modal-content'> + <div className='modal-header'> + <h4 className='modal-title'>Ajouter une mission</h4> + </div> - {(this.props.modal.type == 'create') && ( - <form> - <div className='modal-body'> - <div className='form-group'> - <label className='control-label is-required'>Nom</label> - <input - type='text' - ref='name' - className='form-control' - 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 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> + {(this.props.modal.type == 'create') && ( + <form> + <div className='modal-body'> + <div className='form-group'> + <label className='control-label is-required'>Nom</label> + <input + type='text' + ref='name' + className='form-control' + onKeyDown={(e) => actions.resetValidation(e.currentTarget)} + required + /> </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'>Code mission</label> - <input - type='text' - ref='registration_number' - className='form-control' - onKeyDown={(e) => actions.resetValidation(e.currentTarget)} - required - /> + <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'>Code mission</label> + <input + type='text' + ref='registration_number' + className='form-control' + onKeyDown={(e) => actions.resetValidation(e.currentTarget)} + /> + </div> </div> </div> </div> - </div> - <div className='modal-footer'> - <button - className='btn btn-link' - data-dismiss='modal' - type='button' - onClick={this.props.onModalClose} - > - Annuler - </button> - <button - className='btn btn-primary' - type='button' - onClick={this.handleSubmit.bind(this)} - > - Valider - </button> - </div> - </form> - )} + <div className='modal-footer'> + <button + className='btn btn-link' + data-dismiss='modal' + type='button' + onClick={this.props.onModalClose} + > + Annuler + </button> + <button + className='btn btn-primary' + type='button' + onClick={this.handleSubmit.bind(this)} + > + Valider + </button> + </div> + </form> + )} + </div> </div> </div> </div> - </div> - </div> + </li> + </ul> </div> ) } else { 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 81c59b2ce..1fc935932 100644 --- a/app/assets/javascripts/es6_browserified/journey_patterns/components/EditModal.js +++ b/app/assets/javascripts/es6_browserified/journey_patterns/components/EditModal.js @@ -63,7 +63,7 @@ class EditModal extends Component { </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'>Code mission</label> + <label className='control-label'>Code mission</label> <input type='text' ref='registration_number' @@ -71,7 +71,6 @@ class EditModal extends Component { id={this.props.modal.modalProps.index} defaultValue={this.props.modal.modalProps.journeyPattern.registration_number} onKeyDown={(e) => actions.resetValidation(e.currentTarget)} - required /> </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 14ddf2b99..43c40a4d5 100644 --- a/app/assets/javascripts/es6_browserified/journey_patterns/components/JourneyPattern.js +++ b/app/assets/javascripts/es6_browserified/journey_patterns/components/JourneyPattern.js @@ -14,7 +14,7 @@ class JourneyPattern extends Component{ let vjURL = routeURL + '/vehicle_journeys?jp=' + jpOid return ( - <a data-turbolinks="false" href={vjURL}>Horaires des courses</a> + <a href={vjURL}>Horaires des courses</a> ) } diff --git a/app/assets/javascripts/es6_browserified/time_tables/actions/index.js b/app/assets/javascripts/es6_browserified/time_tables/actions/index.js index 41d247b21..951664129 100644 --- a/app/assets/javascripts/es6_browserified/time_tables/actions/index.js +++ b/app/assets/javascripts/es6_browserified/time_tables/actions/index.js @@ -122,6 +122,9 @@ const actions = { type : 'OPEN_CONFIRM_MODAL', callback }), + showErrorModal: () => ({ + type: 'OPEN_ERROR_MODAL' + }), closeModal : () => ({ type : 'CLOSE_MODAL' }), @@ -185,9 +188,13 @@ const actions = { return improvedCM }, - checkConfirmModal: (event, callback, stateChanged,dispatch) => { + checkConfirmModal: (event, callback, stateChanged, dispatch, metas, timetable) => { if(stateChanged === true){ - return actions.openConfirmModal(callback) + if(timetable.time_table_periods.length == 0 && _.some(metas.day_types)){ + return actions.showErrorModal() + }else{ + return actions.openConfirmModal(callback) + } }else{ dispatch(actions.fetchingApi()) return callback @@ -201,8 +208,8 @@ const actions = { start = new Date(start) end = new Date(end) _.each(periods, (period, i) => { - if(index != i && !period.deleted){ - if((new Date(period.period_start) <= start && new Date(period.period_end) >= start) || (new Date(period.period_start) <= end && new Date(period.period_end) >= end)) + if(index !== i && !period.deleted){ + if((new Date(period.period_start) <= start && new Date(period.period_end) >= start) || (new Date(period.period_start) <= end && new Date(period.period_end) >= end) || (start >= new Date(period.period_start) && end <= new Date(period.period_end)) || (start <= new Date(period.period_start) && end >= new Date(period.period_end))) error = 'Les périodes ne peuvent pas se chevaucher' } }) diff --git a/app/assets/javascripts/es6_browserified/time_tables/components/ConfirmModal.js b/app/assets/javascripts/es6_browserified/time_tables/components/ConfirmModal.js index c2229d991..40ae0eccf 100644 --- a/app/assets/javascripts/es6_browserified/time_tables/components/ConfirmModal.js +++ b/app/assets/javascripts/es6_browserified/time_tables/components/ConfirmModal.js @@ -2,7 +2,7 @@ var React = require('react') var Component = require('react').Component var PropTypes = require('react').PropTypes -const ConfirmModal = ({dispatch, modal, onModalAccept, onModalCancel, journeyPatterns}) => ( +const ConfirmModal = ({dispatch, modal, onModalAccept, onModalCancel, timetable, metas}) => ( <div className={ 'modal fade ' + ((modal.type == 'confirm') ? 'in' : '') } id='ConfirmModal'> <div className='modal-container'> <div className='modal-dialog'> @@ -28,7 +28,7 @@ const ConfirmModal = ({dispatch, modal, onModalAccept, onModalCancel, journeyPat className='btn btn-primary' data-dismiss='modal' type='button' - onClick = {() => {onModalAccept(modal.confirmModal.callback, journeyPatterns)}} + onClick = {() => {onModalAccept(modal.confirmModal.callback, timetable, metas)}} > Valider </button> diff --git a/app/assets/javascripts/es6_browserified/time_tables/components/ErrorModal.js b/app/assets/javascripts/es6_browserified/time_tables/components/ErrorModal.js new file mode 100644 index 000000000..31ed256ea --- /dev/null +++ b/app/assets/javascripts/es6_browserified/time_tables/components/ErrorModal.js @@ -0,0 +1,39 @@ +var React = require('react') +var Component = require('react').Component +var PropTypes = require('react').PropTypes + +const ErrorModal = ({dispatch, modal, onModalClose}) => ( + <div className={ 'modal fade ' + ((modal.type == 'error') ? 'in' : '') } id='ErrorModal'> + <div className='modal-container'> + <div className='modal-dialog'> + <div className='modal-content'> + <div className='modal-header'> + <h4 className='modal-title'>Erreur</h4> + </div> + <div className='modal-body'> + <div className='mt-md mb-md'> + <p>Un calendrier d'application ne peut pas avoir de journée(s) d'application sans période(s).</p> + </div> + </div> + <div className='modal-footer'> + <button + className='btn btn-link' + data-dismiss='modal' + type='button' + onClick= {() => {onModalClose()}} + > + Retour + </button> + </div> + </div> + </div> + </div> + </div> +) + +ErrorModal.propTypes = { + modal: PropTypes.object.isRequired, + onModalClose: PropTypes.func.isRequired +} + +module.exports = ErrorModal diff --git a/app/assets/javascripts/es6_browserified/time_tables/components/Navigate.js b/app/assets/javascripts/es6_browserified/time_tables/components/Navigate.js index 74ca36ea6..6a3690cb0 100644 --- a/app/assets/javascripts/es6_browserified/time_tables/components/Navigate.js +++ b/app/assets/javascripts/es6_browserified/time_tables/components/Navigate.js @@ -39,7 +39,7 @@ let Navigate = ({ dispatch, metas, timetable, pagination, status, filters}) => { value={month} onClick={e => { e.preventDefault() - dispatch(actions.checkConfirmModal(e, actions.changePage(dispatch, e.currentTarget.value), pagination.stateChanged, dispatch)) + dispatch(actions.checkConfirmModal(e, actions.changePage(dispatch, e.currentTarget.value), pagination.stateChanged, dispatch, metas, timetable)) }} > {actions.monthName(month) + ' ' + new Date(month).getFullYear()} @@ -56,7 +56,7 @@ let Navigate = ({ dispatch, metas, timetable, pagination, status, filters}) => { <button onClick={e => { e.preventDefault() - dispatch(actions.checkConfirmModal(e, actions.goToPreviousPage(dispatch, pagination), pagination.stateChanged, dispatch)) + dispatch(actions.checkConfirmModal(e, actions.goToPreviousPage(dispatch, pagination), pagination.stateChanged, dispatch, metas, timetable)) }} type='button' data-target='#ConfirmModal' @@ -66,7 +66,7 @@ let Navigate = ({ dispatch, metas, timetable, pagination, status, filters}) => { <button onClick={e => { e.preventDefault() - dispatch(actions.checkConfirmModal(e, actions.goToNextPage(dispatch, pagination), pagination.stateChanged, dispatch)) + dispatch(actions.checkConfirmModal(e, actions.goToNextPage(dispatch, pagination), pagination.stateChanged, dispatch, metas, timetable)) }} type='button' data-target='#ConfirmModal' diff --git a/app/assets/javascripts/es6_browserified/time_tables/components/SaveTimetable.js b/app/assets/javascripts/es6_browserified/time_tables/components/SaveTimetable.js index 24c91f282..e8c0aa3ba 100644 --- a/app/assets/javascripts/es6_browserified/time_tables/components/SaveTimetable.js +++ b/app/assets/javascripts/es6_browserified/time_tables/components/SaveTimetable.js @@ -2,6 +2,7 @@ var React = require('react') var Component = require('react').Component var PropTypes = require('react').PropTypes var actions = require('../actions') +var _ = require('lodash') class SaveTimetable extends Component{ constructor(props){ @@ -18,7 +19,11 @@ class SaveTimetable extends Component{ type='button' onClick={e => { e.preventDefault() - actions.submitTimetable(this.props.dispatch, this.props.timetable, this.props.metas) + if(this.props.timetable.time_table_periods.length == 0 && _.some(this.props.metas.day_types)){ + this.props.onShowErrorModal() + }else{ + actions.submitTimetable(this.props.getDispatch(), this.props.timetable, this.props.metas) + } }} > Valider diff --git a/app/assets/javascripts/es6_browserified/time_tables/containers/App.js b/app/assets/javascripts/es6_browserified/time_tables/containers/App.js index 7c75377ea..02f0ddbd8 100644 --- a/app/assets/javascripts/es6_browserified/time_tables/containers/App.js +++ b/app/assets/javascripts/es6_browserified/time_tables/containers/App.js @@ -8,6 +8,7 @@ var Navigate = require('./Navigate') var PeriodForm = require('./PeriodForm') var SaveTimetable = require('./SaveTimetable') var ConfirmModal = require('./ConfirmModal') +var ErrorModal = require('./ErrorModal') class App extends Component { componentDidMount(){ @@ -24,6 +25,7 @@ class App extends Component { <PeriodForm /> <SaveTimetable /> <ConfirmModal /> + <ErrorModal /> </div> </div> ) diff --git a/app/assets/javascripts/es6_browserified/time_tables/containers/ConfirmModal.js b/app/assets/javascripts/es6_browserified/time_tables/containers/ConfirmModal.js index 8095e1e7c..6282c1d1d 100644 --- a/app/assets/javascripts/es6_browserified/time_tables/containers/ConfirmModal.js +++ b/app/assets/javascripts/es6_browserified/time_tables/containers/ConfirmModal.js @@ -5,13 +5,14 @@ var ConfirmModal = require('../components/ConfirmModal') const mapStateToProps = (state) => { return { modal: state.modal, - journeyPatterns: state.journeyPatterns + timetable: state.timetable, + metas: state.metas } } const mapDispatchToProps = (dispatch) => { return { - onModalAccept: (next, timetable, metas, state) =>{ + onModalAccept: (next, timetable, metas) =>{ dispatch(actions.fetchingApi()) actions.submitTimetable(dispatch, timetable, metas, next) }, diff --git a/app/assets/javascripts/es6_browserified/time_tables/containers/ErrorModal.js b/app/assets/javascripts/es6_browserified/time_tables/containers/ErrorModal.js new file mode 100644 index 000000000..16a7d45dd --- /dev/null +++ b/app/assets/javascripts/es6_browserified/time_tables/containers/ErrorModal.js @@ -0,0 +1,21 @@ +var actions = require('../actions') +var connect = require('react-redux').connect +var ErrorModal = require('../components/ErrorModal') + +const mapStateToProps = (state) => { + return { + modal: state.modal + } +} + +const mapDispatchToProps = (dispatch) => { + return { + onModalClose: () =>{ + dispatch(actions.closeModal()) + } + } +} + +const ErrorModalContainer = connect(mapStateToProps, mapDispatchToProps)(ErrorModal) + +module.exports = ErrorModalContainer diff --git a/app/assets/javascripts/es6_browserified/time_tables/containers/SaveTimetable.js b/app/assets/javascripts/es6_browserified/time_tables/containers/SaveTimetable.js index 7d2684dde..b5539e7d8 100644 --- a/app/assets/javascripts/es6_browserified/time_tables/containers/SaveTimetable.js +++ b/app/assets/javascripts/es6_browserified/time_tables/containers/SaveTimetable.js @@ -11,6 +11,16 @@ const mapStateToProps = (state) => { } } -const SaveTimetable = connect(mapStateToProps)(SaveTimetableComponent) +const mapDispatchToProps = (dispatch) => { + return { + onShowErrorModal: () => { + dispatch(actions.showErrorModal()) + }, + getDispatch: () => { + return dispatch + } + } +} +const SaveTimetable = connect(mapStateToProps, mapDispatchToProps)(SaveTimetableComponent) module.exports = SaveTimetable diff --git a/app/assets/javascripts/es6_browserified/time_tables/reducers/metas.js b/app/assets/javascripts/es6_browserified/time_tables/reducers/metas.js index 4f1e7a528..2ce084efd 100644 --- a/app/assets/javascripts/es6_browserified/time_tables/reducers/metas.js +++ b/app/assets/javascripts/es6_browserified/time_tables/reducers/metas.js @@ -12,6 +12,9 @@ const metas = (state = {}, action) => { color: action.json.color, calendar: action.json.calendar ? action.json.calendar : null }) + case 'RECEIVE_MONTH': + let dt = (typeof state.day_types === 'string') ? actions.strToArrayDayTypes(state.day_types) : state.day_types + return _.assign({}, state, {day_types: dt}) case 'INCLUDE_DATE_IN_PERIOD': case 'EXCLUDE_DATE_FROM_PERIOD': case 'DELETE_PERIOD': diff --git a/app/assets/javascripts/es6_browserified/time_tables/reducers/modal.js b/app/assets/javascripts/es6_browserified/time_tables/reducers/modal.js index 56486a105..69f7b206e 100644 --- a/app/assets/javascripts/es6_browserified/time_tables/reducers/modal.js +++ b/app/assets/javascripts/es6_browserified/time_tables/reducers/modal.js @@ -19,6 +19,9 @@ const modal = (state = {}, action) => { callback: action.callback, } }) + case 'OPEN_ERROR_MODAL': + $('#ErrorModal').modal('show') + return _.assign({}, state, {type: 'error'}) case 'CLOSE_PERIOD_FORM': newModalProps = _.assign({}, state.modalProps, {active: false}) return _.assign({}, state, {modalProps: newModalProps}) diff --git a/app/assets/javascripts/es6_browserified/time_tables/reducers/pagination.js b/app/assets/javascripts/es6_browserified/time_tables/reducers/pagination.js index 660484c58..3d96fb7b7 100644 --- a/app/assets/javascripts/es6_browserified/time_tables/reducers/pagination.js +++ b/app/assets/javascripts/es6_browserified/time_tables/reducers/pagination.js @@ -5,8 +5,12 @@ const pagination = (state = {}, action) => { case 'RECEIVE_TIME_TABLES': return _.assign({}, state, { currentPage: action.json.current_periode_range, - periode_range: action.json.periode_range + periode_range: action.json.periode_range, + stateChanged: false }) + case 'RECEIVE_MONTH': + case 'RECEIVE_ERRORS': + return _.assign({}, state, {stateChanged: false}) case 'GO_TO_PREVIOUS_PAGE': case 'GO_TO_NEXT_PAGE': let nextPage = action.nextPage ? 1 : -1 @@ -22,7 +26,6 @@ const pagination = (state = {}, action) => { case 'VALIDATE_PERIOD_FORM': case 'UPDATE_COMMENT': case 'UPDATE_COLOR': - case 'UPDATE_DAY_TYPES': toggleOnConfirmModal('modal') return _.assign({}, state, {stateChanged: true}) default: diff --git a/app/assets/javascripts/es6_browserified/vehicle_journeys/components/Filters.js b/app/assets/javascripts/es6_browserified/vehicle_journeys/components/Filters.js index e2d03e195..f74bfa71d 100644 --- a/app/assets/javascripts/es6_browserified/vehicle_journeys/components/Filters.js +++ b/app/assets/javascripts/es6_browserified/vehicle_journeys/components/Filters.js @@ -107,7 +107,9 @@ const Filters = ({filters, pagination, onFilter, onResetFilters, onUpdateStartTi onChange={onToggleWithoutSchedule} checked={filters.query.withoutSchedule} ></input> - <span className='switch-label' data-checkedvalue='Non' data-uncheckedvalue='Oui'></span> + <span className='switch-label' data-checkedvalue='Non' data-uncheckedvalue='Oui'> + {filters.query.withoutSchedule ? 'Oui' : 'Non'} + </span> </label> </div> </div> @@ -117,7 +119,7 @@ const Filters = ({filters, pagination, onFilter, onResetFilters, onUpdateStartTi <div className="ffg-row"> {/* Switch avec/sans calendrier */} <div className='form-group has_switch'> - <label className='control-label pull-left'>Afficher les courses sans calendrier</label> + <label className='control-label pull-left'>Afficher les courses avec calendrier</label> <div className='form-group pull-left' style={{padding: 0}}> <div className='checkbox'> <label> @@ -126,7 +128,9 @@ const Filters = ({filters, pagination, onFilter, onResetFilters, onUpdateStartTi onChange={onToggleWithoutTimeTable} checked={filters.query.withoutTimeTable} ></input> - <span className='switch-label' data-checkedvalue='Oui' data-uncheckedvalue='Non'></span> + <span className='switch-label' data-checkedvalue='Non' data-uncheckedvalue='Oui'> + {filters.query.withoutTimeTable ? 'Oui' : 'Non'} + </span> </label> </div> </div> diff --git a/app/assets/javascripts/es6_browserified/vehicle_journeys/components/tools/DuplicateVehicleJourney.js b/app/assets/javascripts/es6_browserified/vehicle_journeys/components/tools/DuplicateVehicleJourney.js index f5e01de7c..0cf102693 100644 --- a/app/assets/javascripts/es6_browserified/vehicle_journeys/components/tools/DuplicateVehicleJourney.js +++ b/app/assets/javascripts/es6_browserified/vehicle_journeys/components/tools/DuplicateVehicleJourney.js @@ -53,72 +53,75 @@ class DuplicateVehicleJourney extends Component { <div className='modal-header'> <h4 className='modal-title'>Dupliquer une course</h4> {(this.props.modal.type == 'duplicate') && ( - <em>Dupliquer les horaires de la course {actions.getSelected(this.props.vehicleJourneys)[0].objectid}</em> + <em>Dupliquer les horaires de la course {actions.humanOID(actions.getSelected(this.props.vehicleJourneys)[0].objectid)}</em> )} </div> {(this.props.modal.type == 'duplicate') && ( - <form> + <form className='form-horizontal'> <div className='modal-body'> - <div className='row'> - <div className='col-lg-3 col-md-3 col-sm-3 col-xs-3'> - <div className='form-group'> - <label className='control-label is-required'>Horaire de départ</label> - <span className={'input-group time' + (actions.getSelected(this.props.vehicleJourneys).length > 1 ? ' disabled' : '')}> - <input - type='number' - ref='duplicate_time_hh' - min='00' - max='23' - className='form-control' - defaultValue={this.getDefaultValue('hour')} - disabled={(actions.getSelected(this.props.vehicleJourneys).length > 1 ? 'disabled' : '')} - /> - <span>:</span> - <input - type='number' - ref='duplicate_time_mm' - min='00' - max='59' - className='form-control' - defaultValue={this.getDefaultValue('minute')} - disabled={(actions.getSelected(this.props.vehicleJourneys).length > 1 ? 'disabled' : '')} - /> - </span> - </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'>Nombre de courses à créer et dupliquer</label> + <div className={'form-group ' + (actions.getSelected(this.props.vehicleJourneys).length > 1 ? 'hidden' : '' )}> + <label className='control-label is-required col-sm-8'>Horaire de départ indicatif</label> + <span className="col-sm-4"> + <span className={'input-group time' + (actions.getSelected(this.props.vehicleJourneys).length > 1 ? ' disabled' : '')}> <input type='number' - ref='duplicate_number' - min='1' - max='20' + ref='duplicate_time_hh' + min='00' + max='23' className='form-control' - onKeyDown={(e) => actions.resetValidation(e.currentTarget)} - required + defaultValue={this.getDefaultValue('hour')} + disabled={(actions.getSelected(this.props.vehicleJourneys).length > 1 ? 'disabled' : '')} /> - </div> - </div> - - <div className='col-lg-3 col-md-3 col-sm-3 col-xs-3'> - <div className='form-group'> - <label className='control-label is-required'>Avec un décalage de</label> + <span>:</span> <input type='number' - ref='additional_time' - min='-59' + ref='duplicate_time_mm' + min='00' max='59' className='form-control' - onKeyDown={(e) => actions.resetValidation(e.currentTarget)} - required + defaultValue={this.getDefaultValue('minute')} + disabled={(actions.getSelected(this.props.vehicleJourneys).length > 1 ? 'disabled' : '')} /> - </div> + </span> + </span> + </div> + + <div className='form-group'> + <label className='control-label is-required col-sm-8'>Nombre de courses à créer et dupliquer</label> + <div className="col-sm-4"> + <input + type='number' + style={{'width': 104}} + ref='duplicate_number' + min='1' + max='20' + defaultValue='1' + className='form-control' + onKeyDown={(e) => actions.resetValidation(e.currentTarget)} + required + /> + </div> + </div> + + <div className='form-group'> + <label className='control-label is-required col-sm-8'>Décalage à partir duquel on créé les courses</label> + <div className="col-sm-4"> + <input + type='number' + style={{'width': 104}} + ref='additional_time' + min='-59' + max='59' + defaultValue='0' + className='form-control' + onKeyDown={(e) => actions.resetValidation(e.currentTarget)} + required + /> </div> </div> </div> + <div className='modal-footer'> <button className='btn btn-link' diff --git a/app/assets/javascripts/es6_browserified/vehicle_journeys/components/tools/NotesEditVehicleJourney.js b/app/assets/javascripts/es6_browserified/vehicle_journeys/components/tools/NotesEditVehicleJourney.js index d4c3f4231..4c18ef96f 100644 --- a/app/assets/javascripts/es6_browserified/vehicle_journeys/components/tools/NotesEditVehicleJourney.js +++ b/app/assets/javascripts/es6_browserified/vehicle_journeys/components/tools/NotesEditVehicleJourney.js @@ -2,6 +2,7 @@ var React = require('react') var Component = require('react').Component var PropTypes = require('react').PropTypes var actions = require('../../actions') +var _ = require('lodash') class NotesEditVehicleJourney extends Component { constructor(props) { @@ -27,16 +28,28 @@ class NotesEditVehicleJourney extends Component { type='button' className='btn btn-outline-danger btn-xs' onClick={() => this.props.onToggleFootnoteModal(lf, false)} - ><span className="fa fa-trash"></span></button> + ><span className="fa fa-trash"></span> Retirer</button> }else{ return <button type='button' className='btn btn-outline-primary btn-xs' onClick={() => this.props.onToggleFootnoteModal(lf, true)} - ><span className="fa fa-plus"></span></button> + ><span className="fa fa-plus"></span> Ajouter</button> } } + filterFN() { + return _.filter(window.line_footnotes, (lf, i) => { + let bool = true + _.map(this.props.modal.modalProps.vehicleJourney.footnotes, (f, j) => { + if(lf.id === f.id) { + bool = false + } + }) + return bool + }) + } + render() { if(this.props.status.isFetching == true) { return false @@ -65,7 +78,24 @@ class NotesEditVehicleJourney extends Component { {(this.props.modal.type == 'notes_edit') && ( <form> <div className='modal-body'> - {window.line_footnotes.map((lf, i) => + <h3>Notes associées</h3> + {(this.props.modal.modalProps.vehicleJourney.footnotes).map((lf, i) => + <div + key={i} + className='panel panel-default' + > + <div className='panel-heading'> + <h4 className='panel-title clearfix'> + <div className='pull-left' style={{paddingTop: '3px'}}>{lf.code}</div> + <div className='pull-right'>{this.renderFootnoteButton(lf, this.props.modal.modalProps.vehicleJourney.footnotes)}</div> + </h4> + </div> + <div className='panel-body'><p>{lf.label}</p></div> + </div> + )} + + <h3 className='mt-lg'>Sélectionnez les notes à associer à cette course :</h3> + {this.filterFN().map((lf, i) => <div key={i} className='panel panel-default' diff --git a/app/assets/javascripts/es6_browserified/vehicle_journeys/components/tools/select2s/MissionSelect2.js b/app/assets/javascripts/es6_browserified/vehicle_journeys/components/tools/select2s/MissionSelect2.js index 36aaa5cfd..b3df767ab 100644 --- a/app/assets/javascripts/es6_browserified/vehicle_journeys/components/tools/select2s/MissionSelect2.js +++ b/app/assets/javascripts/es6_browserified/vehicle_journeys/components/tools/select2s/MissionSelect2.js @@ -50,7 +50,7 @@ class BSelect4 extends React.Component{ }, cache: true }, - minimumInputLength: 2, + minimumInputLength: 0, escapeMarkup: function (markup) { return markup; }, templateResult: formatRepo }} diff --git a/app/assets/javascripts/es6_browserified/vehicle_journeys/index.js b/app/assets/javascripts/es6_browserified/vehicle_journeys/index.js index 2a76ae43a..97aa60526 100644 --- a/app/assets/javascripts/es6_browserified/vehicle_journeys/index.js +++ b/app/assets/javascripts/es6_browserified/vehicle_journeys/index.js @@ -48,7 +48,7 @@ var initialState = { comment: '' }, withoutSchedule: true, - withoutTimeTable: false + withoutTimeTable: true } }, diff --git a/app/assets/javascripts/es6_browserified/vehicle_journeys/reducers/modal.js b/app/assets/javascripts/es6_browserified/vehicle_journeys/reducers/modal.js index 9f071069d..229fd2058 100644 --- a/app/assets/javascripts/es6_browserified/vehicle_journeys/reducers/modal.js +++ b/app/assets/javascripts/es6_browserified/vehicle_journeys/reducers/modal.js @@ -90,11 +90,15 @@ const modal = (state = {}, action) => { case 'CREATE_VEHICLEJOURNEY_MODAL': let selectedJP = {} if (window.jpOrigin){ + let stopAreas = _.map(window.jpOriginStopPoints, (sa, i) =>{ + return _.assign({}, {stop_area_short_description : {id : sa.stop_area_id}}) + }) selectedJP = { id: window.jpOrigin.id, name: window.jpOrigin.name, published_name: window.jpOrigin.published_name, - objectid: window.jpOrigin.objectid + objectid: window.jpOrigin.objectid, + stop_areas: stopAreas } } return { diff --git a/app/assets/javascripts/es6_browserified/vehicle_journeys/reducers/pagination.js b/app/assets/javascripts/es6_browserified/vehicle_journeys/reducers/pagination.js index ee59fca6d..a8189fc97 100644 --- a/app/assets/javascripts/es6_browserified/vehicle_journeys/reducers/pagination.js +++ b/app/assets/javascripts/es6_browserified/vehicle_journeys/reducers/pagination.js @@ -2,6 +2,7 @@ var _ = require('lodash') const pagination = (state = {}, action) => { switch (action.type) { case 'RECEIVE_JOURNEY_PATTERNS': + case 'RECEIVE_VEHICLE_JOURNEYS': return _.assign({}, state, {stateChanged: false}) case 'GO_TO_PREVIOUS_PAGE': if (action.pagination.page > 1){ diff --git a/app/assets/javascripts/es6_browserified/vehicle_journeys/reducers/vehicleJourneys.js b/app/assets/javascripts/es6_browserified/vehicle_journeys/reducers/vehicleJourneys.js index 9dea63e07..c7e8d58e7 100644 --- a/app/assets/javascripts/es6_browserified/vehicle_journeys/reducers/vehicleJourneys.js +++ b/app/assets/javascripts/es6_browserified/vehicle_journeys/reducers/vehicleJourneys.js @@ -42,7 +42,9 @@ const vehicleJourney= (state = {}, action, keep) => { time_tables: [], vehicle_journey_at_stops: pristineVjasList, selected: false, - deletable: false + deletable: false, + transport_mode: window.transportMode ? window.transportMode : 'undefined', + transport_submode: window.transportSubmode ? window.transportSubmode : 'undefined' } case 'DUPLICATE_VEHICLEJOURNEY': case 'SHIFT_VEHICLEJOURNEY': diff --git a/app/assets/javascripts/forms.coffee b/app/assets/javascripts/forms.coffee index 6b00e9c26..12d82fef1 100644 --- a/app/assets/javascripts/forms.coffee +++ b/app/assets/javascripts/forms.coffee @@ -41,10 +41,11 @@ isEdge = !isIE && !!window.StyleMedia else $(selectedStatus).css('color', selectedValue) -$(document).on 'turbolinks:load', togglableFilter -$(document).on 'turbolinks:load', submitMover -$(document).on 'turbolinks:load', switchInput -$(document).on 'turbolinks:load', colorSelector +$ -> + togglableFilter() + submitMover() + switchInput() + colorSelector() if isIE || isEdge $(document).on 'click', '.formSubmitr', (e)-> diff --git a/app/assets/javascripts/main_menu.coffee b/app/assets/javascripts/main_menu.coffee index f6266f06b..a12c47576 100644 --- a/app/assets/javascripts/main_menu.coffee +++ b/app/assets/javascripts/main_menu.coffee @@ -1,4 +1,4 @@ -$(document).on 'turbolinks:load', -> +$ -> link = [] ptitleCont = "" diff --git a/app/assets/javascripts/nav_panels.coffee b/app/assets/javascripts/nav_panels.coffee index 829db5ad0..25f15f063 100644 --- a/app/assets/javascripts/nav_panels.coffee +++ b/app/assets/javascripts/nav_panels.coffee @@ -1,4 +1,4 @@ -$(document).on 'turbolinks:load', -> +$ -> $('#menu_top [data-panel="toggle"]').on 'click', (e) -> e.preventDefault() $(this).siblings().removeClass 'active' diff --git a/app/assets/javascripts/routing_constraint_zones.coffee b/app/assets/javascripts/routing_constraint_zones.coffee index fc032f074..ceb9fb218 100644 --- a/app/assets/javascripts/routing_constraint_zones.coffee +++ b/app/assets/javascripts/routing_constraint_zones.coffee @@ -1,28 +1,62 @@ -fill_stop_points_options = -> - stop_point_select = $('#routing_constraint_zone_stop_point_ids') - stop_point_select.empty() - referential_id = document.location.pathname.match(/\d+/g)[0] - line_id = document.location.pathname.match(/\d+/g)[1] - route_id = $('#routing_constraint_zone_route_id').val() - - if errors_on_form() - stop_point_ids = eval($('#stop_point_ids').val()) - - $.ajax - url: "/referentials/#{referential_id}/lines/#{line_id}/routes/#{route_id}/stop_points" - dataType: 'json' - success: (data, textStatus, jqXHR) -> - for stop_point in data - selected = $.inArray(stop_point.id, stop_point_ids) != -1 - stop_point_select.append "<option value='#{stop_point.id}'" + "#{if selected then ' selected' else ''}" + ">#{stop_point.name}</option>" - error: (jqXHR, textStatus, errorThrown) -> - console.log textStatus - console.log errorThrown - -errors_on_form = -> - document.location.pathname.endsWith('routing_constraint_zones') && $('#new_routing_constraint_zone').length - -$(document).on 'turbolinks:load', -> - if document.location.pathname.endsWith('routing_constraint_zones/new') || errors_on_form() - fill_stop_points_options() - $('#routing_constraint_zone_route_id').change(fill_stop_points_options) +@ITL_stoppoints = -> + routeID = $('#routing_constraint_zone_route_id').val() + + if (routeID) + origin = window.location.origin + path = window.location.pathname.split('/', 5).join('/') + reqURL = origin + path + '/routes/' + routeID + '/stop_points.json' + + $.ajax + url: reqURL + dataType: 'json' + error: (jqXHR, textStatus, errorThrown) -> + console.log(errorThrown) + success: (collection, textStatus, jqXHR) -> + html = '' + stopAreaBaseURL = origin + window.location.pathname.split('/', 3).join('/') + '/stop_areas/' + + collection.forEach (item, index) -> + html += "<div class='nested-fields'><div class='wrapper'> + <div><a href='" + stopAreaBaseURL + item.stop_area_id + "' class='navlink' title='Voir l'arrêt'><span>" + item.name + "</span></a></div> + <div><span>" + item.city_name + " (" + item.zip_code + ")</span></div> + <div> + <span class='has_radio'> + <input type='checkbox' name='routing_constraint_zone[stop_point_ids][" + index + "]' value='" + item.id + "'> + <span class='radio-label'></span> + </span> + </div> + </div></div>" + + $('#ITL_stoppoints').find('.nested-fields').remove() + $('#ITL_stoppoints').find('.nested-head').after(html) + + # VALIDATION + selection = [] + $('#ITL_stoppoints').on 'click', "input[type='checkbox']", (e) -> + v = $(e.target).val() + + if ( $.inArray(v, selection) != -1 ) + selection.splice(selection.indexOf(v), 1) + else + selection.push(v) + + alertMsg = "<div class='alert alert-danger' style='margin-bottom:15px;'> + <p class='small'>Un ITL doit comporter au moins deux arrêts</p> + </div>" + + $(document).on 'click', "input[type='submit']", (e)-> + inputName = $('#routing_constraint_zone_name').val() + + $('.alert-danger').remove() + + if ( selection.length < 2 && inputName != "") + e.preventDefault() + $('#routing_constraint_zone_name').closest('.form-group').removeClass('has-error').find('.help-block').remove() + $('#ITL_stoppoints').prepend(alertMsg) + +$ -> + ITL_stoppoints() + + $('#routing_constraint_zone_route_id').on 'change', -> + $('.alert-danger').remove() + ITL_stoppoints() diff --git a/app/assets/javascripts/select2.coffee b/app/assets/javascripts/select2.coffee index af3dc6d75..d54ddb811 100644 --- a/app/assets/javascripts/select2.coffee +++ b/app/assets/javascripts/select2.coffee @@ -20,6 +20,7 @@ bind_select2_ajax = (el, cfg = {}) -> delay: 125, processResults: (data, params) -> results: data minimumInputLength: 3 + placeholder: target.data('select2ed-placeholder') templateResult: (item) -> item.text templateSelection: (item) -> @@ -39,4 +40,5 @@ bind_select2_ajax = (el, cfg = {}) -> -$(document).on 'turbolinks:load', select_2 +$ -> + select_2() diff --git a/app/assets/javascripts/selectable_table.coffee b/app/assets/javascripts/selectable_table.coffee index 4086bf6c2..681c8f9e6 100644 --- a/app/assets/javascripts/selectable_table.coffee +++ b/app/assets/javascripts/selectable_table.coffee @@ -53,4 +53,5 @@ .addClass 'noselect' .children('.info-msg').children('span').text(selection.length) -$(document).on 'turbolinks:load', selectTable +$ -> + selectTable() diff --git a/app/assets/javascripts/time_table_combinations.coffee b/app/assets/javascripts/time_table_combinations.coffee index 8923af958..6104da7ff 100644 --- a/app/assets/javascripts/time_table_combinations.coffee +++ b/app/assets/javascripts/time_table_combinations.coffee @@ -3,4 +3,5 @@ $(this).closest('.has_switch').siblings('.form-group').each -> $(this).toggleClass('hidden') -$(document).on 'turbolinks:load', combinedTypeToggle +$ -> + combinedTypeToggle() diff --git a/app/assets/stylesheets/components/_tables.sass b/app/assets/stylesheets/components/_tables.sass index 0cd91a1a8..e3a33e131 100644 --- a/app/assets/stylesheets/components/_tables.sass +++ b/app/assets/stylesheets/components/_tables.sass @@ -82,6 +82,66 @@ border-top: 2px solid $darkgrey margin-top: 15px + // Specific for tables displaying stop points + &.has-stoppoints + tbody + > tr > td:first-child + position: relative + padding-left: 25px + + &:before + content: '' + display: block + width: 10px + height: 10px + background-color: #fff + border: 2px solid $blue + border-radius: 50% + position: absolute + z-index: 5 + left: 5px + top: 50% + margin-top: -5px + + &:after + content: '' + display: block + width: 4px + margin: 0 3px + background-color: rgba($grey, 0.5) + position: absolute + z-index: 3 + top: 0 + left: 5px + bottom: 0 + + > tr:first-child > td:first-child + &:after + content: '' + top: 50% + + > tr:last-child > td:first-child + &:after + content: '' + bottom: 50% + + > tr:first-child > td:first-child, > tr:last-child > td:first-child + &:before + content: '•' + color: $blue + text-align: center + font-size: 28px + letter-spacing: 0 + text-indent: -0.01em + line-height: 12px + width: 15px + height: 15px + left: 2px + top: 50% + margin-top: -8px + + + // select_toolbox .select_toolbox padding: 10px diff --git a/app/controllers/autocomplete_calendars_controller.rb b/app/controllers/autocomplete_calendars_controller.rb index f85f529fc..2b85fcff3 100644 --- a/app/controllers/autocomplete_calendars_controller.rb +++ b/app/controllers/autocomplete_calendars_controller.rb @@ -2,6 +2,6 @@ class AutocompleteCalendarsController < ApplicationController respond_to :json, :only => [:autocomplete] def autocomplete - @calendars = current_organisation.referentials.search(params[:q]).result.paginate(page: params[:page]) + @calendars = current_organisation.calendars.search(params[:q]).result.paginate(page: params[:page]) end end diff --git a/app/controllers/calendars_controller.rb b/app/controllers/calendars_controller.rb index 5370d9cbb..86d567882 100644 --- a/app/controllers/calendars_controller.rb +++ b/app/controllers/calendars_controller.rb @@ -33,10 +33,8 @@ class CalendarsController < BreadcrumbController def collection return @calendars if @calendars - scope = Calendar.where('organisation_id = ?', current_organisation.id) - + scope = Calendar.where('organisation_id = ? OR shared = ?', current_organisation.id, true) scope = shared_scope(scope) - @q = scope.ransack(params[:q]) calendars = @q.result diff --git a/app/controllers/clean_ups_controller.rb b/app/controllers/clean_ups_controller.rb index 1e4835775..b9ff225b3 100644 --- a/app/controllers/clean_ups_controller.rb +++ b/app/controllers/clean_ups_controller.rb @@ -14,6 +14,6 @@ class CleanUpsController < ChouetteController end def clean_up_params - params.require(:clean_up).permit(:keep_lines, :keep_stops, :keep_companies, :keep_networks, :keep_group_of_lines, :begin_date, :end_date) + params.require(:clean_up).permit(:date_type, :begin_date, :end_date) end end diff --git a/app/controllers/devise/cas_sessions_controller.rb b/app/controllers/devise/cas_sessions_controller.rb index ecc7e9f7e..0a9d9ecb2 100644 --- a/app/controllers/devise/cas_sessions_controller.rb +++ b/app/controllers/devise/cas_sessions_controller.rb @@ -16,7 +16,12 @@ class Devise::CasSessionsController < Devise::SessionsController end def service - redirect_to after_sign_in_path_for(warden.authenticate!(:scope => resource_name)) + warden.authenticate!(:scope => resource_name) + if LoginPolicy.new(current_user).boiv? + redirect_to after_sign_in_path_for(current_user) + else + redirect_to root_path, flash: {alert: t('devise.sessions.new.unauthorized')} + end end def unregistered diff --git a/app/controllers/referentials_controller.rb b/app/controllers/referentials_controller.rb index 437444f29..aa5b359da 100644 --- a/app/controllers/referentials_controller.rb +++ b/app/controllers/referentials_controller.rb @@ -74,7 +74,6 @@ class ReferentialsController < BreadcrumbController alias_method :current_referential, :referential helper_method :current_referential - def resource @referential ||= current_organisation.find_referential(params[:id]) end @@ -130,13 +129,7 @@ class ReferentialsController < BreadcrumbController params.require(:referential).permit( :id, :name, - :slug, - :prefix, - :time_zone, - :upper_corner, - :lower_corner, :organisation_id, - :projection_type, :data_format, :archived_at, :created_from_id, diff --git a/app/controllers/route_stop_points_controller.rb b/app/controllers/route_stop_points_controller.rb index e12acb33b..730bd08a9 100644 --- a/app/controllers/route_stop_points_controller.rb +++ b/app/controllers/route_stop_points_controller.rb @@ -11,8 +11,7 @@ class RouteStopPointsController < ChouetteController def index respond_to do |format| - format.json { render json: referential.lines.find(params[:line_id]).routes.find(params[:route_id]).stop_points.map { |sp| { id: sp.id, name: sp.name } } } + format.json { render json: referential.lines.find(params[:line_id]).routes.find(params[:route_id]).stop_points.map { |sp| { id: sp.id, stop_area_id: sp.stop_area.id, name: sp.name, zip_code: sp.stop_area.zip_code, city_name: sp.stop_area.city_name } } } end end end - diff --git a/app/controllers/routing_constraint_zones_controller.rb b/app/controllers/routing_constraint_zones_controller.rb index f2f74e801..7707427b0 100644 --- a/app/controllers/routing_constraint_zones_controller.rb +++ b/app/controllers/routing_constraint_zones_controller.rb @@ -1,22 +1,71 @@ class RoutingConstraintZonesController < ChouetteController - defaults resource_class: Chouette::RoutingConstraintZone + include PolicyChecker + defaults resource_class: Chouette::RoutingConstraintZone respond_to :html, :xml, :json - before_action :remove_empty_stop_point, only: [:create, :update] + before_action :check_stoppoint_param, only: [:create, :update] belongs_to :referential do belongs_to :line, parent_class: Chouette::Line end - include PolicyChecker + def index + @routing_constraint_zones = collection + end + + def show + @routing_constraint_zone = collection.find(params[:id]) + end + + protected + + def collection + @q = resource.routing_constraint_zones.search(params[:q]) + + if sort_column && sort_direction + @routing_constraint_zones ||= @q.result(distinct: true).order(sort_column + ' ' + sort_direction) + else + @routing_constraint_zones ||= @q.result(distinct: true).order(:name) + end + @routing_constraint_zones = @routing_constraint_zones.paginate(page: params[:page], per_page: 10) + end private + def sort_column + (Chouette::RoutingConstraintZone.column_names).include?(params[:sort]) ? params[:sort] : 'name' + end + def sort_direction + %w[asc desc].include?(params[:direction]) ? params[:direction] : 'asc' + end + + def resource + @referential = Referential.find params[:referential_id] + @line = @referential.lines.find params[:line_id] + end + def routing_constraint_zone_params - params.require(:routing_constraint_zone).permit(:name, { stop_point_ids: [] }, :line_id, :route_id, :objectid, :object_version, :creator_id) + params.require(:routing_constraint_zone).permit( + :name, + { stop_point_ids: [] }, + :line_id, + :route_id, + :objectid, + :object_version, + :creator_id + ) end - def remove_empty_stop_point - params.require(:routing_constraint_zone)[:stop_point_ids].delete('') + def check_stoppoint_param + spArr = [] + if params.require(:routing_constraint_zone)[:stop_point_ids] and params.require(:routing_constraint_zone)[:stop_point_ids].length >= 2 + params.require(:routing_constraint_zone)[:stop_point_ids].each do |k,v| + spArr << v + end + params.require(:routing_constraint_zone)[:stop_point_ids] = spArr + else + Rails.logger.error("Error: An ITL must have at least two stop points") + end end + end diff --git a/app/controllers/users/login_controller.rb b/app/controllers/users/login_controller.rb new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/app/controllers/users/login_controller.rb diff --git a/app/controllers/vehicle_journeys_controller.rb b/app/controllers/vehicle_journeys_controller.rb index 71787ba78..fe2e2137f 100644 --- a/app/controllers/vehicle_journeys_controller.rb +++ b/app/controllers/vehicle_journeys_controller.rb @@ -64,7 +64,13 @@ class VehicleJourneysController < ChouetteController } end - @jp_origin = Chouette::JourneyPattern.find_by(objectid: params[:jp]) + @transport_mode = route.line['transport_mode'] + @transport_submode = route.line['transport_submode'] + + if params[:jp] + @jp_origin = Chouette::JourneyPattern.find_by(objectid: params[:jp]) + @jp_origin_stop_points = @jp_origin.stop_points + end index! do if collection.out_of_bounds? @@ -111,12 +117,18 @@ class VehicleJourneysController < ChouetteController end def maybe_filter_out_journeys_with_time_tables(scope) - if params[:q] && - params[:q][:vehicle_journey_without_time_table] == 'true' - return scope - .without_time_tables + if params[:q] && params[:q][:vehicle_journey_without_time_table] == 'false' + return scope.without_time_tables end + # if params[:q] + # if params[:q][:vehicle_journey_without_time_table] == 'true' + # return scope.without_time_tables + # end + # else + # return scope.without_time_tables + # end + scope end @@ -129,7 +141,7 @@ class VehicleJourneysController < ChouetteController def adapted_params params.tap do |adapted_params| - adapted_params.merge!( :route => parent) + adapted_params.merge!(:route => parent) hour_entry = "vehicle_journey_at_stops_departure_time_gt(4i)".to_sym if params[:q] && params[:q][ hour_entry] adapted_params[:q].merge! hour_entry => (params[:q][ hour_entry].to_i - utc_offset) diff --git a/app/helpers/newapplication_helper.rb b/app/helpers/newapplication_helper.rb index e189199a2..3d43e9fc7 100644 --- a/app/helpers/newapplication_helper.rb +++ b/app/helpers/newapplication_helper.rb @@ -16,7 +16,7 @@ module NewapplicationHelper end columns.map do |k, v| - if ["ID Codif", "Oid", "OiD", "ID Reflex", "Arrêt de départ", "Arrêt d'arrivée", "Période de validité englobante", "Période englobante", "Nombre de courses associées", "Journées d'application"].include? k + if ["ID Codif", "Oid", "OiD", "ID Reflex", "Arrêt de départ", "Arrêt d'arrivée", "Période de validité englobante", "Période englobante", "Nombre de courses associées", "Journées d'application", "Arrêts de l'itinéraire", "Arrêts inclus dans l'ITL"].include? k hcont << content_tag(:th, k) else hcont << content_tag(:th, sortable_columns(collection, k)) @@ -160,7 +160,7 @@ module NewapplicationHelper end.join.html_safe end - # content_tag :div, trigger + menu, class: 'btn-group' + content_tag :div, trigger + menu, class: 'btn-group' end diff --git a/app/models/chouette/route.rb b/app/models/chouette/route.rb index 9f6344055..76905bf2b 100644 --- a/app/models/chouette/route.rb +++ b/app/models/chouette/route.rb @@ -63,8 +63,10 @@ class Chouette::Route < Chouette::TridentActiveRecord end accepts_nested_attributes_for :stop_points, :allow_destroy => :true - # validates_presence_of :name + validates_presence_of :name + validates_presence_of :published_name validates_presence_of :line + # validates_presence_of :direction # validates_presence_of :wayback diff --git a/app/models/chouette/time_table.rb b/app/models/chouette/time_table.rb index 42879c6d5..4186af6d2 100644 --- a/app/models/chouette/time_table.rb +++ b/app/models/chouette/time_table.rb @@ -135,10 +135,17 @@ class Chouette::TimeTable < Chouette::TridentActiveRecord [Chouette::TimeTable.maximum(:end_date)].compact.max end + def add_exclude_date(in_out, date) + self.dates.create!({in_out: in_out, date: date}) + end + def actualize self.dates.clear self.periods.clear - self.merge! self.calendar.convert_to_time_table + from = self.calendar.convert_to_time_table + self.dates = from.dates + self.periods = from.periods + self.save end def month_inspect(date) @@ -470,18 +477,17 @@ class Chouette::TimeTable < Chouette::TridentActiveRecord # merge effective days from another timetable def merge!(another_tt) transaction do + # merge dates + self.dates ||= [] + another_tt.included_days.each do |d| + add_included_day d + end + # if one tt has no period, just merge lists if self.periods.empty? || another_tt.periods.empty? if !another_tt.periods.empty? # copy periods self.periods = another_tt.clone_periods - # set valid_days - self.int_day_types = another_tt.int_day_types - end - # merge dates - self.dates ||= [] - another_tt.included_days.each do |d| - add_included_day d end else # check if periods can be kept @@ -540,7 +546,6 @@ class Chouette::TimeTable < Chouette::TridentActiveRecord self.dates.clear days.each {|d| self.dates << Chouette::TimeTableDate.new( :date =>d, :in_out => true)} self.periods.clear - self.int_day_types = 0 self.dates.to_a.sort! { |a,b| a.date <=> b.date} self.save! end @@ -555,8 +560,6 @@ class Chouette::TimeTable < Chouette::TridentActiveRecord days = self.effective_days - days_to_exclude self.dates.clear self.periods.clear - self.int_day_types = 0 - days.each {|d| self.dates << Chouette::TimeTableDate.new( :date =>d, :in_out => true)} self.dates.to_a.sort! { |a,b| a.date <=> b.date} diff --git a/app/models/chouette/time_table_date.rb b/app/models/chouette/time_table_date.rb index 4624ae88e..b881c9a5d 100644 --- a/app/models/chouette/time_table_date.rb +++ b/app/models/chouette/time_table_date.rb @@ -6,6 +6,8 @@ class Chouette::TimeTableDate < Chouette::ActiveRecord validates_presence_of :date validates_uniqueness_of :date, :scope => :time_table_id + scope :in_dates, -> { where(in_out: true) } + def self.model_name ActiveModel::Name.new Chouette::TimeTableDate, Chouette, "TimeTableDate" end diff --git a/app/models/chouette/vehicle_journey.rb b/app/models/chouette/vehicle_journey.rb index 72d554e96..44dd85864 100644 --- a/app/models/chouette/vehicle_journey.rb +++ b/app/models/chouette/vehicle_journey.rb @@ -28,17 +28,37 @@ module Chouette has_and_belongs_to_many :time_tables, :class_name => 'Chouette::TimeTable', :foreign_key => "vehicle_journey_id", :association_foreign_key => "time_table_id" has_many :stop_points, -> { order("stop_points.position") }, :through => :vehicle_journey_at_stops - validates :vehicle_journey_at_stops, + validates :vehicle_journey_at_stops, :vjas_departure_time_must_be_before_next_stop_arrival_time, vehicle_journey_at_stops_are_in_increasing_time_order: true validates_presence_of :number - before_validation :set_default_values + before_validation :set_default_values, + :calculate_vehicle_journey_at_stop_day_offset + + def vjas_departure_time_must_be_before_next_stop_arrival_time + notice = 'departure time must be before next stop arrival time' + vehicle_journey_at_stops.each_with_index do |current_stop, index| + next_stop = vehicle_journey_at_stops[index + 1] + + next unless next_stop && (next_stop[:arrival_time] < current_stop[:departure_time]) + + current_stop.errors.add(:departure_time, notice) + self.errors.add(:vehicle_journey_at_stops, notice) + end + end + def set_default_values if number.nil? self.number = 0 end end + def calculate_vehicle_journey_at_stop_day_offset + Chouette::VehicleJourneyAtStopsDayOffset.new( + vehicle_journey_at_stops + ).update + end + scope :without_any_time_table, -> { joins('LEFT JOIN "time_tables_vehicle_journeys" ON "time_tables_vehicle_journeys"."vehicle_journey_id" = "vehicle_journeys"."id" LEFT JOIN "time_tables" ON "time_tables"."id" = "time_tables_vehicle_journeys"."time_table_id"').where(:time_tables => { :id => nil}) } scope :without_any_passing_time, -> { joins('LEFT JOIN "vehicle_journey_at_stops" ON "vehicle_journey_at_stops"."vehicle_journey_id" = "vehicle_journeys"."id"').where(vehicle_journey_at_stops: { id: nil }) } @@ -50,9 +70,12 @@ module Chouette def vehicle_journey_at_stops_matrix at_stops = self.vehicle_journey_at_stops.to_a.dup + active_stop_point_ids = journey_pattern.stop_points.map(&:id) + (route.stop_points.map(&:id) - at_stops.map(&:stop_point_id)).each do |id| - # Set stop_point id for fake vjas with no departure time yep. - at_stops.insert(route.stop_points.map(&:id).index(id), Chouette::VehicleJourneyAtStop.new(stop_point_id: id)) + vjas = Chouette::VehicleJourneyAtStop.new(stop_point_id: id) + vjas.dummy = !active_stop_point_ids.include?(id) + at_stops.insert(route.stop_points.map(&:id).index(id), vjas) end at_stops end diff --git a/app/models/chouette/vehicle_journey_at_stop.rb b/app/models/chouette/vehicle_journey_at_stop.rb index 553531422..5dfec8352 100644 --- a/app/models/chouette/vehicle_journey_at_stop.rb +++ b/app/models/chouette/vehicle_journey_at_stop.rb @@ -9,7 +9,7 @@ module Chouette belongs_to :stop_point belongs_to :vehicle_journey - attr_accessor :_destroy + attr_accessor :_destroy, :dummy validate :arrival_must_be_before_departure def arrival_must_be_before_departure @@ -27,6 +27,7 @@ module Chouette after_initialize :set_virtual_attributes def set_virtual_attributes @_destroy = false + @dummy = false end end diff --git a/app/models/chouette/vehicle_journey_at_stops_day_offset.rb b/app/models/chouette/vehicle_journey_at_stops_day_offset.rb new file mode 100644 index 000000000..9f577dded --- /dev/null +++ b/app/models/chouette/vehicle_journey_at_stops_day_offset.rb @@ -0,0 +1,42 @@ +module Chouette + class VehicleJourneyAtStopsDayOffset + def initialize(at_stops) + @at_stops = at_stops + end + + def calculate! + arrival_offset = 0 + departure_offset = 0 + + @at_stops.inject(nil) do |prior_stop, stop| + next stop if prior_stop.nil? + + if stop.arrival_time < prior_stop.departure_time || + stop.arrival_time < prior_stop.arrival_time + arrival_offset += 1 + end + + if stop.departure_time < stop.arrival_time || + stop.departure_time < prior_stop.departure_time + departure_offset += 1 + end + + stop.arrival_day_offset = arrival_offset + stop.departure_day_offset = departure_offset + + stop + end + end + + def save + @at_stops.each do |at_stop| + at_stop.save + end + end + + def update + calculate! + save + end + end +end diff --git a/app/models/clean_up.rb b/app/models/clean_up.rb index a44bb46a4..a791065aa 100644 --- a/app/models/clean_up.rb +++ b/app/models/clean_up.rb @@ -1,9 +1,13 @@ class CleanUp < ActiveRecord::Base + extend Enumerize include AASM belongs_to :referential has_one :clean_up_result + enumerize :date_type, in: %i(between before after) + validates :begin_date, presence: true + validates :date_type, presence: true after_commit :perform_cleanup, :on => :create def perform_cleanup @@ -11,25 +15,96 @@ class CleanUp < ActiveRecord::Base end def clean - result = {} - result['time_table_count'] = self.clean_time_tables - result['vehicle_journey_count'] = self.clean_vehicle_journeys - result['journey_pattern_count'] = self.clean_journey_patterns - result + {}.tap do |result| + result['time_table'] = send("destroy_time_tables_#{self.date_type}").try(:count) + result['time_table_date'] = send("destroy_time_tables_dates_#{self.date_type}").try(:count) + result['time_table_period'] = send("destroy_time_tables_periods_#{self.date_type}").try(:count) + self.overlapping_periods.each do |period| + exclude_dates_in_overlapping_period(period) + end + end + end + + def destroy_time_tables_between + time_tables = Chouette::TimeTable.where('end_date <= ? AND start_date >= ?', self.end_date, self.begin_date) + self.destroy_time_tables(time_tables) + end + + def destroy_time_tables_before + time_tables = Chouette::TimeTable.where('end_date < ?', self.begin_date) + self.destroy_time_tables(time_tables) + end + + def destroy_time_tables_after + time_tables = Chouette::TimeTable.where('start_date > ?', self.begin_date) + self.destroy_time_tables(time_tables) + end + + def destroy_time_tables_dates_before + Chouette::TimeTableDate.in_dates.where('date < ?', self.begin_date).destroy_all + end + + def destroy_time_tables_dates_after + Chouette::TimeTableDate.in_dates.where('date > ?', self.begin_date).destroy_all + end + + def destroy_time_tables_dates_between + Chouette::TimeTableDate.in_dates.where('date >= ? AND date <= ?', self.begin_date, self.end_date).destroy_all end - def clean_time_tables - Chouette::TimeTable.validity_out_between?(begin_date, end_date).delete_all + def destroy_time_tables_periods_before + Chouette::TimeTablePeriod.where('period_end < ?', self.begin_date).destroy_all end - def clean_vehicle_journeys - ids = Chouette::VehicleJourney.includes(:time_tables).where(:time_tables => {id: nil}).pluck(:id) - Chouette::VehicleJourney.where(id: ids).delete_all + def destroy_time_tables_periods_after + Chouette::TimeTablePeriod.where('period_start > ?', self.begin_date).destroy_all end - def clean_journey_patterns - ids = Chouette::JourneyPattern.includes(:vehicle_journeys).where(:vehicle_journeys => {id: nil}).pluck(:id) - Chouette::JourneyPattern.where(id: ids).delete_all + def destroy_time_tables_periods_between + Chouette::TimeTablePeriod.where('period_start >= ? AND period_end <= ?', self.begin_date, self.end_date).destroy_all + end + + def overlapping_periods + self.end_date = self.begin_date if self.date_type != 'between' + Chouette::TimeTablePeriod.where('(period_start, period_end) OVERLAPS (?, ?)', self.begin_date, self.end_date) + end + + def exclude_dates_in_overlapping_period(period) + days_in_period = period.period_start..period.period_end + day_out = period.time_table.dates.where(in_out: false).map(&:date) + # check if day is greater or less then cleanup date + if date_type != 'between' + operator = date_type == 'after' ? '>' : '<' + to_exclude_days = days_in_period.map do |day| + day if day.public_send(operator, self.begin_date) + end + else + days_in_cleanup_periode = (self.begin_date..self.end_date) + to_exclude_days = days_in_period & days_in_cleanup_periode + end + + to_exclude_days.to_a.compact.each do |day| + # we ensure day is not already an exclude date + if !day_out.include?(day) + self.add_exclude_date(period.time_table, day) + end + end + end + + def add_exclude_date(time_table, day) + day_in = time_table.dates.where(in_out: true).map(&:date) + unless day_in.include?(day) + time_table.add_exclude_date(false, day) + else + time_table.dates.where(date: day).take.update_attribute(:in_out, false) + end + end + + def destroy_time_tables(time_tables) + time_tables.each do |time_table| + time_table.vehicle_journeys.map(&:destroy) + end + time_tables.destroy_all end aasm column: :status do diff --git a/app/models/referential.rb b/app/models/referential.rb index 83d507320..0ce325bd6 100644 --- a/app/models/referential.rb +++ b/app/models/referential.rb @@ -1,13 +1,13 @@ -# -*- coding: utf-8 -*- class Referential < ActiveRecord::Base include DataFormatEnumerations validates_presence_of :name validates_presence_of :slug validates_presence_of :prefix - validates_presence_of :time_zone - validates_presence_of :upper_corner - validates_presence_of :lower_corner + # Fixme #3657 + # validates_presence_of :time_zone + # validates_presence_of :upper_corner + # validates_presence_of :lower_corner validates_uniqueness_of :slug validates_uniqueness_of :name @@ -127,7 +127,7 @@ class Referential < ActiveRecord::Base end def self.new_from(from, organisation:) - Referential.new({ + Referential.new( name: I18n.t("activerecord.copy", :name => from.name), slug: "#{from.slug}_clone", prefix: from.prefix, @@ -139,7 +139,7 @@ class Referential < ActiveRecord::Base workbench: from.workbench, created_from: from, metadatas: from.metadatas.map { |m| ReferentialMetadata.new_from(m) } - }) + ) end def self.available_srids @@ -180,7 +180,9 @@ class Referential < ActiveRecord::Base before_validation :assign_line_and_stop_area_referential, :on => :create, if: :workbench, unless: :created_from before_validation :clone_associations, :on => :create, if: :created_from - before_create :create_schema + before_validation :assign_slug, :on => :create + before_validation :assign_prefix, :on => :create + before_create :create_schema, unless: :created_from after_create :clone_schema, if: :created_from @@ -282,13 +284,21 @@ class Referential < ActiveRecord::Base end def clone_schema - ReferentialCloning.create(source_referential: self.created_from, target_referential: self) + ReferentialCloning.create(source_referential: created_from, target_referential: self) end def create_schema Apartment::Tenant.create slug end + def assign_slug + self.slug ||= "#{self.name.parameterize.gsub('-', '_')}_#{Time.now.to_i}" + end + + def assign_prefix + self.prefix = self.organisation.name.parameterize.gsub('-', '_') + end + def assign_line_and_stop_area_referential self.line_referential = workbench.line_referential self.stop_area_referential = workbench.stop_area_referential diff --git a/app/models/referential_cloning.rb b/app/models/referential_cloning.rb index 2f34093e2..5bf283814 100644 --- a/app/models/referential_cloning.rb +++ b/app/models/referential_cloning.rb @@ -6,7 +6,8 @@ class ReferentialCloning < ActiveRecord::Base private def perform_clone - ReferentialCloningWorker.perform_async(self.id) + ReferentialCloningWorker.perform_async(id) + # ReferentialCloningWorker.new.perform(id) end aasm column: :status do diff --git a/app/models/user.rb b/app/models/user.rb index 14dbeb4d7..1a06746da 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -43,6 +43,8 @@ class User < ActiveRecord::Base self.name = extra[:full_name] self.email = extra[:email] self.organisation = Organisation.sync_update extra[:organisation_code], extra[:organisation_name], extra[:functional_scope] + # TODO: Discuss the following behavior in the light of how the portal's permissions will evolve + # boiv:edit-offer does not imply boiv:read-offer, which needs to be provided specifically for any connection rights self.permissions = extra[:permissions].include?('boiv:edit-offer') ? @@edit_offer_permissions : [] end @@ -71,10 +73,10 @@ class User < ActiveRecord::Base user.locked_at = el['locked_at'] user.organisation = Organisation.sync_update el['organization_code'], el['organization_name'], el['functional_scope'] user.synced_at = Time.now + # TODO: Discuss the following behavior in the light of how the portal's permissions will evolve + # boiv:edit-offer does not imply boiv:read-offer, which needs to be provided specifically for any connection rights user.permissions = el['permissions'].include?('boiv:edit-offer') ? @@edit_offer_permissions : [] - user.permissions += el['permissions'].grep( %r{^\Aboiv:read-offer\z} ) user.save - puts "✓ user #{user.username} has been updated" unless Rails.env.test? end end diff --git a/app/policies/boiv_policy.rb b/app/policies/boiv_policy.rb index 4270dc686..444006aa4 100644 --- a/app/policies/boiv_policy.rb +++ b/app/policies/boiv_policy.rb @@ -1,6 +1,6 @@ -require_relative 'chain' class BoivPolicy < ApplicationPolicy + def boiv_read_offer? organisation_match? && user.has_permission?('boiv:read-offer') end diff --git a/app/policies/chain.rb b/app/policies/chain.rb deleted file mode 100644 index 4bf96bd84..000000000 --- a/app/policies/chain.rb +++ /dev/null @@ -1,57 +0,0 @@ -module Policies - # Implements the `chain_policies` macro as follows - # - # chain_policies <method_chain>, policies: - # - # e.g. - # - # chain_policies [:archived?, :!], policies: %i{create? edit?} - # - # which would establish a precondition `not archived` for the `create?` and `edit?` - # method, it is semantically identical to instrumenting both methods - # as follows: - # - # def create? # or edit? - # archived?.! && <original code of method> - # end - module Chain - - # A local chain store implemented to avoid any possible side effect on client policies. - defined_chains = {} - - # Using `define_method` in order to close over `defined_chains` - # We need to store the chains because the methods they will apply to - # are not defined yet. - define_method :chain_policies do |*conditions, policies:| - policies.each do | meth_name | - # self represents the client Policy - defined_chains[[self, meth_name]] = conditions - end - end - # Intercept method definition and check if a policy_chain has been registered for it‥. - define_method :method_added do |meth_name, *args, &blk| - # Delete potentially registered criteria conditions to‥. - # (i) protect against endless recursion via (:merthod_added → :define_method → :method_added → ‥. - # (ii) get the condition - conditions = defined_chains.delete([self, meth_name]) - return unless conditions - - instrument_method(meth_name, conditions) - end - - private - - # Access to the closure is not necessary anymore, normal metaprogramming can take place :) - def instrument_method(meth_name, conditions) - orig_method = instance_method(meth_name) - # In case of warnings remove original method here, depends on Ruby Version, ok in 2.3.1 - define_method meth_name do |*a, &b| - # Method chain describing the chained policy precondition. - conditions.inject(self) do | result, msg | - result.send msg - end && - orig_method.bind(self).(*a, &b) - end - end - end -end diff --git a/app/policies/journey_pattern_policy.rb b/app/policies/journey_pattern_policy.rb index 9d13624c5..01ce2cbbb 100644 --- a/app/policies/journey_pattern_policy.rb +++ b/app/policies/journey_pattern_policy.rb @@ -1,4 +1,5 @@ class JourneyPatternPolicy < BoivPolicy + class Scope < Scope def resolve scope diff --git a/app/policies/line_policy.rb b/app/policies/line_policy.rb index c3e0051c8..b829040af 100644 --- a/app/policies/line_policy.rb +++ b/app/policies/line_policy.rb @@ -1,8 +1,4 @@ -require_relative 'chain' class LinePolicy < BoivPolicy - extend Policies::Chain - - chain_policies :archived?, :!, policies: %i{create_footnote? destroy_footnote? edit_footnote?} class Scope < Scope def resolve @@ -19,15 +15,15 @@ class LinePolicy < BoivPolicy def destroy? ; create? end def create_footnote? - user.has_permission?('footnotes.create') + !archived? && user.has_permission?('footnotes.create') end def edit_footnote? - user.has_permission?('footnotes.edit') + !archived? && user.has_permission?('footnotes.edit') end def destroy_footnote? - user.has_permission?('footnotes.destroy') + !archived? && user.has_permission?('footnotes.destroy') end def update_footnote? ; edit_footnote? end diff --git a/app/policies/login_policy.rb b/app/policies/login_policy.rb new file mode 100644 index 000000000..3364c37ac --- /dev/null +++ b/app/policies/login_policy.rb @@ -0,0 +1,13 @@ +# Headless as described here https://github.com/elabs/pundit#headless-policies +class LoginPolicy + + attr_reader :user + def initialize user + @user = user + end + + def boiv? + !(user.permissions || []).grep(%r{\Aboiv:.}).empty? + end + +end diff --git a/app/policies/referential_policy.rb b/app/policies/referential_policy.rb index 4a5e85ead..e531c6c19 100644 --- a/app/policies/referential_policy.rb +++ b/app/policies/referential_policy.rb @@ -25,6 +25,11 @@ class ReferentialPolicy < BoivPolicy organisation_match? && create? end + def common_lines? + # TODO: Replace with correct BL ASA available, c.f. https://projects.af83.io/issues/2692 + true + end + def unarchive? ; archive? end def update? ; edit? end def new? ; create? end diff --git a/app/policies/route_policy.rb b/app/policies/route_policy.rb index dba3a27da..ca9b02164 100644 --- a/app/policies/route_policy.rb +++ b/app/policies/route_policy.rb @@ -1,23 +1,20 @@ class RoutePolicy < BoivPolicy - extend Policies::Chain class Scope < Scope def resolve scope end end - chain_policies :archived?, :!, policies: %i{create? destroy? edit?} - def create? - user.has_permission?('routes.create') # organisation match via referential is checked in the view + !archived? && user.has_permission?('routes.create') # organisation match via referential is checked in the view end def edit? - organisation_match? && user.has_permission?('routes.edit') + !archived? && organisation_match? && user.has_permission?('routes.edit') end def destroy? - organisation_match? && user.has_permission?('routes.destroy') + !archived? && organisation_match? && user.has_permission?('routes.destroy') end def update? ; edit? end diff --git a/app/policies/routing_constraint_zone_policy.rb b/app/policies/routing_constraint_zone_policy.rb index abba5639c..da311bc03 100644 --- a/app/policies/routing_constraint_zone_policy.rb +++ b/app/policies/routing_constraint_zone_policy.rb @@ -1,23 +1,20 @@ class RoutingConstraintZonePolicy < BoivPolicy - extend Policies::Chain class Scope < Scope def resolve scope end end - chain_policies :archived?, :!, policies: %i{create? destroy? edit?} - def create? - user.has_permission?('routing_constraint_zones.create') # organisation match via referential is checked in the view + !archived? && user.has_permission?('routing_constraint_zones.create') # organisation match via referential is checked in the view end def edit? - organisation_match? && user.has_permission?('routing_constraint_zones.edit') + !archived? && organisation_match? && user.has_permission?('routing_constraint_zones.edit') end def destroy? - organisation_match? && user.has_permission?('routing_constraint_zones.destroy') + !archived? && organisation_match? && user.has_permission?('routing_constraint_zones.destroy') end def update? ; edit? end diff --git a/app/policies/time_table_policy.rb b/app/policies/time_table_policy.rb index efab6ac00..e915ede6a 100644 --- a/app/policies/time_table_policy.rb +++ b/app/policies/time_table_policy.rb @@ -1,5 +1,4 @@ class TimeTablePolicy < BoivPolicy - extend Policies::Chain class Scope < Scope def resolve @@ -7,22 +6,20 @@ class TimeTablePolicy < BoivPolicy end end - chain_policies :archived?, :!, policies: %i{create? destroy? duplicate? edit?} - def create? - user.has_permission?('time_tables.create') # organisation match via referential is checked in the view + !archived? && user.has_permission?('time_tables.create') # organisation match via referential is checked in the view end def edit? - organisation_match? && user.has_permission?('time_tables.edit') + !archived? && organisation_match? && user.has_permission?('time_tables.edit') end def destroy? - organisation_match? && user.has_permission?('time_tables.destroy') + !archived? && organisation_match? && user.has_permission?('time_tables.destroy') end def duplicate? - organisation_match? && create? + !archived? && organisation_match? && create? end def update? ; edit? end diff --git a/app/views/calendars/_filters.html.slim b/app/views/calendars/_filters.html.slim index 4f625e4f0..6bcf25f99 100644 --- a/app/views/calendars/_filters.html.slim +++ b/app/views/calendars/_filters.html.slim @@ -1,7 +1,7 @@ = search_form_for @q, url: calendars_path, builder: SimpleForm::FormBuilder, html: { method: :get, class: 'form form-filter' } do |f| .ffg-row .input-group.search_bar - = f.search_field :short_name_cont, class: 'form-control', placeholder: 'Indiquez un nom de calendrier...' + = f.search_field :name_or_short_name_cont, class: 'form-control', placeholder: 'Indiquez un nom/nom court de calendrier...' span.input-group-btn button.btn.btn-default#search_btn type='submit' span.fa.fa-search diff --git a/app/views/calendars/_form.html.slim b/app/views/calendars/_form.html.slim index 9aed9f7d8..3c152c61d 100644 --- a/app/views/calendars/_form.html.slim +++ b/app/views/calendars/_form.html.slim @@ -6,8 +6,8 @@ - if policy(@calendar).share? .form-group.has_switch - = f.label :shared, class: 'col-sm-4 control-label' - = f.input :shared, as: :boolean, checked_value: true, unchecked_value: false, label: content_tag(:span, t("#{@calendar.shared}"), class: 'switch-label', data: {checkedValue: t('true'), uncheckedValue: t('false')}), wrapper_html: { class: 'col-sm-8'} + = f.label :shared, class: 'col-sm-4 col-xs-5 control-label' + = f.input :shared, as: :boolean, checked_value: true, unchecked_value: false, label: content_tag(:span, t("#{@calendar.shared}"), class: 'switch-label', data: {checkedValue: t('true'), uncheckedValue: t('false')}), wrapper_html: { class: 'col-sm-8 col-xs-7'} .separator diff --git a/app/views/layouts/application.html.slim b/app/views/layouts/application.html.slim index dc1ec9766..6eab2a761 100644 --- a/app/views/layouts/application.html.slim +++ b/app/views/layouts/application.html.slim @@ -8,10 +8,10 @@ html lang=I18n.locale title STIF BOIV - = stylesheet_link_tag 'base', media: 'all', 'data-turbolinks-track': true - = stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': true + = stylesheet_link_tag 'base' + = stylesheet_link_tag 'application' - = javascript_include_tag 'application', 'data-turbolinks-track': true + = javascript_include_tag 'application' body diff --git a/app/views/referential_lines/index.js.slim b/app/views/referential_lines/index.js.slim deleted file mode 100644 index 0165fb077..000000000 --- a/app/views/referential_lines/index.js.slim +++ /dev/null @@ -1 +0,0 @@ -| $('#lines').html("#{escape_javascript(render('lines'))}");
\ No newline at end of file diff --git a/app/views/referential_lines/show.html.slim b/app/views/referential_lines/show.html.slim index 1e24f0f15..db99381d3 100644 --- a/app/views/referential_lines/show.html.slim +++ b/app/views/referential_lines/show.html.slim @@ -1,26 +1,23 @@ / PageHeader = pageheader 'map-marker', @line.name, - 'Lorem ipsum dolor sit amet', + '', t('last_update', time: l(@line.updated_at, format: :short)) do / Below is secundary actions & optional contents .row .col-lg-12.text-right.mb-sm - / = link_to t('lines.actions.show_network'), [@referential, @line.network], class: 'btn btn-primary' - / = link_to t('lines.actions.show_company'), [@referential, @line.company], class: 'btn btn-primary' - = link_to @line.human_attribute_name(:footnotes), referential_line_footnotes_path(@referential, @line), class: 'btn btn-primary' - = link_to Chouette::RoutingConstraintZone.model_name.human.pluralize(:fr), referential_line_routing_constraint_zones_path(@referential, @line), class: 'btn btn-primary disabled' + = link_to t('routing_constraint_zones.index.title'), referential_line_routing_constraint_zones_path(@referential, @line), class: 'btn btn-primary' - if policy(Chouette::Line).create? && @referential.organisation == current_organisation - = link_to t('lines.actions.new'), new_referential_line_path(@referential), class: 'btn btn-primary' + = link_to t('actions.new'), new_referential_line_path(@referential), class: 'btn btn-primary' - if policy(@line).update? - = link_to t('lines.actions.edit'), edit_referential_line_path(@referential, @line), class: 'btn btn-primary' + = link_to t('actions.edit'), edit_referential_line_path(@referential, @line), class: 'btn btn-primary' - if policy(@line).destroy? = link_to referential_line_path(@referential, @line), method: :delete, data: {confirm: t('lines.actions.destroy_confirm')}, class: 'btn btn-primary' do span.fa.fa-trash - span = t('lines.actions.destroy') + span = t('actions.destroy') - if !@line.hub_restricted? || (@line.hub_restricted? && @line.routes.size < 2) - if policy(Chouette::Route).create? && @referential.organisation == current_organisation diff --git a/app/views/referentials/_form.html.slim b/app/views/referentials/_form.html.slim index f209b278e..31d71bcdc 100644 --- a/app/views/referentials/_form.html.slim +++ b/app/views/referentials/_form.html.slim @@ -3,34 +3,16 @@ .row .col-lg-12 = form.input :name - - - if @referential.new_record? - = form.input :slug, input_html: { title: t('formtastic.titles.referential.slug') } - - else - = form.input :slug, disabled: true, input_html: { title: t('formtastic.titles.referential.slug') } - - if @referential.new_record? - if @referential.created_from = form.input :created_from, disabled: true, input_html: { value: Referential.find(@referential.created_from_id).name } .hidden = form.input :created_from_id, as: :hidden - - = form.input :prefix, input_html: { title: t("formtastic.titles.referential.prefix") } - - else - if @referential.created_from = form.input :created_from, disabled: true, input_html: { value: Referential.find(@referential.created_from_id).name } - = form.input :prefix, input_html: { title: t("formtastic.titles.referential.prefix") } - - = form.input :projection_type, as: :select, collection: Referential.available_srids - = form.input :time_zone - - - = form.input :upper_corner, input_html: {title: t("formtastic.titles.referential.upper_corner")} - = form.input :lower_corner, input_html: {title: t("formtastic.titles.referential.lower_corner")} - .separator - + = form.simple_fields_for :metadatas do |subform| .row .col-lg-12 @@ -40,7 +22,7 @@ .alert.alert-danger - @referential.errors[:metadatas].each do |msg| p.small = "- #{msg}" - + .subform .nested-head .wrapper @@ -55,10 +37,10 @@ = t('simple_form.labels.referential.metadatas.periods.end') abbr title='requis' * div - + = subform.simple_fields_for :periods do |period_form| = render 'period_fields', f: period_form - + .links.nested-linker = link_to_add_association 'Ajouter une période', subform, :periods, class: 'btn btn-outline-primary' diff --git a/app/views/referentials/show.html.slim b/app/views/referentials/show.html.slim index befa851ab..3c1e36302 100644 --- a/app/views/referentials/show.html.slim +++ b/app/views/referentials/show.html.slim @@ -75,9 +75,12 @@ .container-fluid .row .col-lg-8.col-ld-offset-2.col-md-8.col-md-offset-2.col-sm-8.col-sm-offset-2.col-xs-12 + = f.input :date_type, as: :radio_buttons, label: false = f.input :begin_date, as: :date, label: t('titles.clean_up.begin_date'),:wrapper_html => { class: 'date', title: t('titles.clean_up.begin_date') } - = f.input :end_date, as: :date, label: t('titles.clean_up.end_date'), :wrapper_html => { class: 'date', title: t('titles.clean_up.end_date') } + + = f.input :end_date, as: :date, label: t('titles.clean_up.end_date'), :wrapper_html => { class: 'date cleanup_end_date_wrapper hidden', title: t('titles.clean_up.end_date') } .modal-footer button.btn.btn-link type='button' data-dismiss='modal' Annuler - = f.button :submit, t('actions.clean_up') , class: 'btn btn-primary' + - unless policy(@referential).archived? + = f.button :submit, t('actions.clean_up') , class: 'btn btn-primary' diff --git a/app/views/routes/_form.html.slim b/app/views/routes/_form.html.slim index ad389c482..bee86f91e 100644 --- a/app/views/routes/_form.html.slim +++ b/app/views/routes/_form.html.slim @@ -6,8 +6,8 @@ = f.input :published_name .form-group.has_switch - = f.label :wayback, class: 'col-sm-4 control-label' - = f.input :wayback, as: :boolean, checked_value: :straight_forward, unchecked_value: :backward, label: content_tag(:span, @route.wayback_text, class: 'switch-label', data: {checkedValue: t('enumerize.route.direction.straight_forward'), uncheckedValue: t('enumerize.route.direction.backward')}), wrapper_html: { class: 'col-sm-8'} + = f.label :wayback, class: 'col-sm-4 col-xs-5 control-label' + = f.input :wayback, as: :boolean, checked_value: :straight_forward, unchecked_value: :backward, label: content_tag(:span, @route.wayback_text, class: 'switch-label', data: {checkedValue: t('enumerize.route.direction.straight_forward'), uncheckedValue: t('enumerize.route.direction.backward')}), wrapper_html: { class: 'col-sm-8 col-xs-7'} = f.input :opposite_route_id, collection: @forward, disabled: @route.wayback.straight_forward?, wrapper_html: {class: input_opposite_route_id_css(@route, 'straight_forward')} diff --git a/app/views/routes/new.html.slim b/app/views/routes/new.html.slim index 6ac95f023..102467677 100644 --- a/app/views/routes/new.html.slim +++ b/app/views/routes/new.html.slim @@ -1,7 +1,7 @@ / PageHeader = pageheader 'map-marker', t('routes.new.title'), - 'Lorem ipsum dolor sit amet' + '' / PageContent .page_content diff --git a/app/views/routes/show.html.slim b/app/views/routes/show.html.slim index d994e3861..6d2d4e90d 100644 --- a/app/views/routes/show.html.slim +++ b/app/views/routes/show.html.slim @@ -3,15 +3,15 @@ @route.name, 'Lorem ipsum dolor sit amet', t('last_update', time: l(@route.updated_at, format: :short)), - (policy(@route).edit? ? link_to(t('actions.edit'), edit_referential_line_route_path(@referential, @line, @route), data: {turbolinks: false}, class: 'btn btn-default') : '') do + (policy(@route).edit? ? link_to(t('actions.edit'), edit_referential_line_route_path(@referential, @line, @route), class: 'btn btn-default') : '') do / Below is secundary actions & optional contents (filters, ...) .row.mb-sm .col-lg-12.text-right - if @route_sp.any? - = link_to t('journey_patterns.index.title'), [@referential, @line, @route, :journey_patterns_collection], data: {turbolinks: false}, class: 'btn btn-primary' + = link_to t('journey_patterns.index.title'), [@referential, @line, @route, :journey_patterns_collection], class: 'btn btn-primary' - if @route.journey_patterns.present? - = link_to t('vehicle_journeys.actions.index'), [@referential, @line, @route, :vehicle_journeys], data: {turbolinks: false}, class: 'btn btn-primary' + = link_to t('vehicle_journeys.actions.index'), [@referential, @line, @route, :vehicle_journeys], class: 'btn btn-primary' = link_to t('vehicle_journey_exports.new.title'), referential_line_route_vehicle_journey_exports_path(@referential, @line, @route, format: :zip), class: 'btn btn-primary' diff --git a/app/views/routing_constraint_zones/_filters.html.slim b/app/views/routing_constraint_zones/_filters.html.slim new file mode 100644 index 000000000..18ef40d61 --- /dev/null +++ b/app/views/routing_constraint_zones/_filters.html.slim @@ -0,0 +1,16 @@ += search_form_for @q, url: referential_line_routing_constraint_zones_path(@referential, @line), class: 'form form-filter' do |f| + .ffg-row + .input-group.search_bar + = f.search_field :name_or_objectid_cont, class: 'form-control', placeholder: "Indiquez un nom d'ITL ou un OiD..." + span.input-group-btn + button.btn.btn-default#search-btn type='submit' + span.fa.fa-search + + .ffg-row + .form-group + = f.label 'Itinéraire associé', required: false, class: 'control-label' + = f.input :route_id, as: :select, collection: @line.routing_constraint_zones.pluck(:route_id), label: false, label_method: lambda {|r| @line.routing_constraint_zones.find_by(route_id: r).route_name}, input_html: { 'data-select2ed': 'true', 'data-select2ed-placeholder': 'Indiquez un itinéraire...' }, wrapper_html: { class: 'select2ed'} + + .actions + = link_to 'Effacer', referential_line_routing_constraint_zones_path(@referential, @line), class: 'btn btn-link' + = f.submit 'Filtrer', class: 'btn btn-default' diff --git a/app/views/routing_constraint_zones/_form.html.slim b/app/views/routing_constraint_zones/_form.html.slim index b1c77a44b..3d4764ef7 100644 --- a/app/views/routing_constraint_zones/_form.html.slim +++ b/app/views/routing_constraint_zones/_form.html.slim @@ -1,19 +1,25 @@ -= simple_form_for [@referential, @line, @routing_constraint_zone] do |f| - .row - .col-lg-6.col-sm-12 - = f.input :name - .row - .col-lg-6.col-sm-12 - = f.input :route_id, collection: @line.routes.select { |route| route.stop_points.count > 2 }, include_blank: false - .row - .col-lg-6.col-sm-12 - - stop_points_collection = @routing_constraint_zone.persisted? ? @routing_constraint_zone.route.stop_points : [] - = f.input :stop_point_ids, as: :select, collection: stop_points_collection, selected: @routing_constraint_zone.stop_point_ids, label: Chouette::StopPoint.model_name.human.pluralize.capitalize, label_method: :name, input_html: { 'data-select2ed': 'true', 'data-select2ed-placeholder': 'Sélection des arrêts sur séquence d\'arrêts', 'multiple': 'multiple', style: 'width: 100%' } += simple_form_for [@referential, @line, @routing_constraint_zone], html: {class: 'form-horizontal', id: 'itl_form'}, wrapper: :horizontal_form do |form| .row - .col-lg-12.text-right - = link_to 'Annuler', :back, class: 'btn btn-link' - = f.button :submit, class: 'btn btn-danger' + .col-lg-12 + = form.input :name + = form.input :route_id, collection: @line.routes.select { |route| route.stop_points.count > 2 }, include_blank: false + + .separator + + #ITL_stoppoints + .subform + .nested-head + .wrapper + div + .form-group + label.control-label Arrêt + div + .form-group + label.control-label Commune + div + + = hidden_field_tag 'stop_point_ids', @routing_constraint_zone.stop_point_ids.to_s, id: 'stop_point_ids' -= hidden_field_tag 'stop_point_ids', @routing_constraint_zone.stop_point_ids.to_s, id: 'stop_point_ids' + = form.button :submit, t('actions.submit'), class: 'btn btn-default formSubmitr', form: 'itl_form' diff --git a/app/views/routing_constraint_zones/edit.html.slim b/app/views/routing_constraint_zones/edit.html.slim index fcb0d08a8..d81a347e0 100644 --- a/app/views/routing_constraint_zones/edit.html.slim +++ b/app/views/routing_constraint_zones/edit.html.slim @@ -1,5 +1,12 @@ -= title_tag t('.title', routing_constraint_zone: @routing_constraint_zone.name) +/ PageHeader += pageheader 'map-marker', + t('.title'), + '', + t('last_update', time: l(@routing_constraint_zone.updated_at, format: :short)) -.row - .col-lg-8.col-lg-offset-2.col-md-8.col-md-offset-2.col-sm-8.col-sm-offset-2 - == render 'form' +/ PageContent +.page_content + .container-fluid + .row + .col-lg-8.col-lg-offset-2.col-md-8.col-md-offset-2.col-sm-10.col-sm-offset-1 + == render 'form' diff --git a/app/views/routing_constraint_zones/index.html.slim b/app/views/routing_constraint_zones/index.html.slim index 620b0f7db..248efedb4 100644 --- a/app/views/routing_constraint_zones/index.html.slim +++ b/app/views/routing_constraint_zones/index.html.slim @@ -1,11 +1,30 @@ -= title_tag Chouette::RoutingConstraintZone.model_name.human.pluralize(:fr) +/ PageHeader += pageheader 'map-marker', + t('routing_constraint_zones.index.title'), + '', + '', + ((policy(Chouette::RoutingConstraintZone).create? && @referential.organisation == current_organisation) ? link_to(t('actions.new'), new_referential_line_routing_constraint_zone_path(@referential, @line), class: 'btn btn-primary') : '') -- if policy(Chouette::RoutingConstraintZone).create? && @referential.organisation == current_organisation - = link_to t('routing_constraint_zones.actions.new'), new_referential_line_routing_constraint_zone_path(@referential, @line) +/ PageContent +.page_content + .container-fluid + - if params[:q].present? or @routing_constraint_zones.any? + .row + .col-lg-12 + = render 'filters' -- if @routing_constraint_zones.any? - = table_builder @routing_constraint_zones, - { objectid: 'objectid', name: 'name', route: 'route_name', stop_points_count: 'stop_points_count' }, - [:show, :edit, :delete], - [], - 'table table-bordered' + - if @routing_constraint_zones.any? + .row + .col-lg-12 + = table_builder @routing_constraint_zones, + { 'Oid' => Proc.new { |n| n.try(:objectid).try(:local_id) }, + :name => 'name', :stop_points_count => 'stop_points_count', + :route => 'route_name' }, + [:show, :edit, :delete], + [], + 'table has-filter has-search' + + - unless @routing_constraint_zones.any? + .row.mt-xs + .col-lg-12 + = replacement_msg t('routing_constraint_zones.search_no_results') diff --git a/app/views/routing_constraint_zones/new.html.slim b/app/views/routing_constraint_zones/new.html.slim index fcb0d08a8..f6ae62825 100644 --- a/app/views/routing_constraint_zones/new.html.slim +++ b/app/views/routing_constraint_zones/new.html.slim @@ -1,5 +1,10 @@ -= title_tag t('.title', routing_constraint_zone: @routing_constraint_zone.name) +/ PageHeader += pageheader 'map-marker', + t('.title') -.row - .col-lg-8.col-lg-offset-2.col-md-8.col-md-offset-2.col-sm-8.col-sm-offset-2 - == render 'form' +/ PageContent +.page_content + .container-fluid + .row + .col-lg-8.col-lg-offset-2.col-md-8.col-md-offset-2.col-sm-10.col-sm-offset-1 + == render 'form' diff --git a/app/views/routing_constraint_zones/show.html.slim b/app/views/routing_constraint_zones/show.html.slim index 351784ecc..f0c244387 100644 --- a/app/views/routing_constraint_zones/show.html.slim +++ b/app/views/routing_constraint_zones/show.html.slim @@ -1,17 +1,35 @@ -= title_tag @routing_constraint_zone.name +/ PageHeader += pageheader 'map-marker', + @routing_constraint_zone.name, + '', + t('last_update', time: l(@routing_constraint_zone.updated_at, format: :short)) do -p - label => "#{@routing_constraint_zone.human_attribute_name(:name)} : " - = @routing_constraint_zone.name + / Below is secundary actions & optional contents + .row + .col-lg-12.text-right.mb-sm + - if policy(@routing_constraint_zone).update? + = link_to t('actions.edit'), edit_referential_line_routing_constraint_zone_path(@referential, @line, @routing_constraint_zone), class: 'btn btn-primary' -p - label => "#{Chouette::Route.model_name.human.capitalize} : " - = link_to @routing_constraint_zone.route.name, referential_line_route_path(@referential, @line, @routing_constraint_zone.route) + - if policy(@routing_constraint_zone).destroy? + = link_to referential_line_routing_constraint_zone_path(@referential, @line, @routing_constraint_zone), method: :delete, data: {confirm: t('routing_constraint_zones.actions.destroy_confirm')}, class: 'btn btn-primary' do + span.fa.fa-trash + span = t('actions.destroy') -p - label => "#{Chouette::StopPoint.model_name.human.pluralize.capitalize} : " - br - - @routing_constraint_zone.stop_points.each do |stop_point| - = link_to stop_point.name, referential_stop_area_path(@referential, stop_point.stop_area) - br +/ PageContent +.page_content + .container-fluid + .row + .col-lg-6.col-md-6.col-sm-12.col-xs-12 + = definition_list t('metadatas'), + { @routing_constraint_zone.human_attribute_name(:name) => @routing_constraint_zone.try(:name), + @routing_constraint_zone.human_attribute_name(:route) => link_to(@routing_constraint_zone.try(:route_name), [@referential, @line, @routing_constraint_zone.route]), + @routing_constraint_zone.human_attribute_name(:line) => link_to(@line.name, [@referential, @line])} + .row + .col-lg-12 + = table_builder @routing_constraint_zone.route.stop_points, + { "Arrêts de l'itinéraire" => 'name', + "Arrêts inclus dans l'ITL" => Proc.new{ |rsp| (@routing_constraint_zone.stop_points.collect{|c| c.name}.include? rsp.name) ? 'Oui' : 'Non' } }, + [], + [], + 'table has-stoppoints' diff --git a/app/views/time_table_combinations/_form.html.slim b/app/views/time_table_combinations/_form.html.slim index d8bebf0c4..b4f818828 100644 --- a/app/views/time_table_combinations/_form.html.slim +++ b/app/views/time_table_combinations/_form.html.slim @@ -2,21 +2,21 @@ .row .col-lg-12 .form-group.has_switch - = f.label :combined_type, class: 'col-sm-4 control-label required' do + = f.label :combined_type, class: 'col-sm-4 col-xs-5 control-label required' do = 'Type de calendriers ' abbr title='Champ requis' * - = f.input :combined_type, as: :boolean, checked_value: 'time_table', unchecked_value: 'calendar', required: false, label: content_tag(:span, t("time_table_combinations.combined_type.#{@combination.combined_type}"), class: 'switch-label', data: { checkedValue: 'Calendriers', uncheckedValue: 'Modèles de calendriers' }), wrapper_html: { class: 'col-sm-8' } + = f.input :combined_type, as: :boolean, checked_value: 'time_table', unchecked_value: 'calendar', required: false, label: content_tag(:span, t("time_table_combinations.combined_type.#{@combination.combined_type}"), class: 'switch-label', data: { checkedValue: 'Calendriers', uncheckedValue: 'Modèles de calendriers' }), wrapper_html: { class: 'col-sm-8 col-xs-7' } - = f.input :time_table_id, as: :select, input_html: {class: 'tt_combination_target', style: "width: 100%", data: { 'select2-ajax': 'true', term: 'comment_or_objectid_cont', url: referential_autocomplete_time_tables_path(@referential, format: :json)}}, wrapper_html: {class: @combination.combined_type != 'time_table' ? 'hidden' : ''} + = f.input :time_table_id, as: :select, input_html: {class: 'tt_combination_target', style: "width: 100%", data: { 'select2-ajax': 'true', 'select2ed-placeholder': 'Indiquez un calendrier...', term: 'comment_or_objectid_cont', url: referential_autocomplete_time_tables_path(@referential, format: :json)}}, wrapper_html: {class: @combination.combined_type != 'time_table' ? 'hidden' : ''} - = f.input :calendar_id, as: :select, input_html: { class: 'tt_combination_target', style: "width: 100%", data: { 'select2-ajax': 'true', term: 'name_cont', url: autocomplete_calendars_path}}, wrapper_html: {class: @combination.combined_type != 'calendar' ? 'hidden' : ''} + = f.input :calendar_id, as: :select, input_html: { class: 'tt_combination_target', style: "width: 100%", data: { 'select2-ajax': 'true', 'select2ed-placeholder': 'Indiquez un modèle de calendrier...', term: 'name_cont', url: autocomplete_calendars_path}}, wrapper_html: {class: @combination.combined_type != 'calendar' ? 'hidden' : ''} .separator .row .col-lg-12 - = f.label :operation, class: 'col-sm-4 control-label' - = f.input :operation, as: :radio_buttons, label: false, collection: TimeTableCombination.operations, label_method: lambda{|o| t("time_table_combinations.operations.#{o}")}, wrapper_html: { class: 'col-sm-8' } + = f.label :operation, class: 'col-sm-4 col-xs-5 control-label' + = f.input :operation, as: :radio_buttons, label: false, collection: TimeTableCombination.operations, label_method: lambda{|o| t("time_table_combinations.operations.#{o}")}, wrapper_html: { class: 'col-sm-8 col-xs-7' } = f.button :submit, t('actions.submit'), class: 'btn btn-default formSubmitr', form: 'tt_combination_form' diff --git a/app/views/time_tables/_form.html.slim b/app/views/time_tables/_form.html.slim index d4e1d838e..d06fdf444 100644 --- a/app/views/time_tables/_form.html.slim +++ b/app/views/time_tables/_form.html.slim @@ -5,7 +5,7 @@ = form.input :comment, :input_html => { :title => t("formtastic.titles#{format_restriction_for_locales(@referential)}.time_table.comment")} - if @time_table.new_record? && !@time_table.created_from - = form.input :calendar_id, as: :select, collection: current_organisation.calendars + = form.input :calendar_id, as: :select, input_html: { class: 'tt_target', style: "width: 100%", data: { 'select2-ajax': 'true', 'select2ed-placeholder': 'Indiquez un modèle de calendrier...', term: 'name_cont', url: autocomplete_calendars_path}} - if @time_table.created_from = form.input :created_from, disabled: true, input_html: { value: @time_table.created_from.comment } diff --git a/app/views/time_tables/index.html.slim b/app/views/time_tables/index.html.slim index c17f96c85..402b09b98 100644 --- a/app/views/time_tables/index.html.slim +++ b/app/views/time_tables/index.html.slim @@ -20,7 +20,7 @@ "Nombre de courses associées" => Proc.new{ |tt| tt.vehicle_journeys.count }, "Journées d'application" => Proc.new{ |tt| (%w(monday tuesday wednesday thursday friday saturday sunday).collect{|d| tt.send(d) ? t("calendars.days.#{d}") : '' }).reject{|a| a.empty?}.join(', ').html_safe }, :calendar => Proc.new{ |tt| tt.calendar ? tt.calendar.try(:name) : '-' }, :updated_at => Proc.new {|tt| l(tt.updated_at, format: :short)} }, - [:show, :edit, :duplicate, :delete], + [:show, :edit, :duplicate, :actualize, :delete], [], 'table has-search' diff --git a/app/views/time_tables/show.html.slim b/app/views/time_tables/show.html.slim index 1c5984a7d..2e71ebb9e 100644 --- a/app/views/time_tables/show.html.slim +++ b/app/views/time_tables/show.html.slim @@ -4,6 +4,7 @@ = pageheader 'map-marker', @time_table.comment, '', + t('last_update', time: l(@time_table.updated_at, format: :short)), (policy(@time_table).edit? ? link_to(t('actions.edit'), edit_referential_time_table_path(@referential, @time_table), class: 'btn btn-default') : '') / Below is secundary actions & optional contents (filters, ...) diff --git a/app/views/vehicle_journeys/index.html.slim b/app/views/vehicle_journeys/index.html.slim index 38e07ad82..93f4e3221 100644 --- a/app/views/vehicle_journeys/index.html.slim +++ b/app/views/vehicle_journeys/index.html.slim @@ -16,6 +16,9 @@ | window.route_id = #{params[:route_id]}; | window.stopPoints = #{(@stop_points_list.to_json).html_safe}; | window.jpOrigin = #{(@jp_origin.to_json).html_safe}; + | window.jpOriginStopPoints = #{(@jp_origin_stop_points.to_json).html_safe}; + | window.transportMode = #{(@transport_mode.to_json).html_safe}; + | window.transportSubmode = #{(@transport_submode.to_json).html_safe}; | window.vehicleJourneysLength = #{@vehicle_journeys.total_entries()}; | window.vehicleJourneysPerPage = #{@ppage}; | window.line_footnotes = #{raw @footnotes}; diff --git a/app/views/vehicle_journeys/show.rabl b/app/views/vehicle_journeys/show.rabl index 1ef9bc6b5..fce16dfb3 100644 --- a/app/views/vehicle_journeys/show.rabl +++ b/app/views/vehicle_journeys/show.rabl @@ -31,18 +31,19 @@ end child(:vehicle_journey_at_stops_matrix, :object_root => false) do |vehicle_stops| node do |vehicle_stop| - node(:dummy) { !vehicle_stop.id? } + node(:dummy) { vehicle_stop.dummy } + node(:stop_area_object_id) do - vehicle_stop.stop_point ? vehicle_stop.stop_point.stop_area.objectid : nil + vehicle_stop.stop_point.stop_area.objectid end node(:stop_point_objectid) do - vehicle_stop.stop_point ? vehicle_stop.stop_point.objectid : nil + vehicle_stop.stop_point.objectid end node(:stop_area_name) do - vehicle_stop.stop_point ? vehicle_stop.stop_point.stop_area.name : nil + vehicle_stop.stop_point.stop_area.name end node(:stop_area_cityname) do - vehicle_stop.stop_point ? vehicle_stop.stop_point.stop_area.city_name : nil + vehicle_stop.stop_point.stop_area.city_name end [:id, :connecting_service_id, :boarding_alighting_possibility].map do |att| diff --git a/app/workers/referential_cloning_worker.rb b/app/workers/referential_cloning_worker.rb index ef3acd529..6592160ec 100644 --- a/app/workers/referential_cloning_worker.rb +++ b/app/workers/referential_cloning_worker.rb @@ -1,13 +1,12 @@ class ReferentialCloningWorker include Sidekiq::Worker - sidekiq_options queue: 'wip' # Replace default apartment created schema with clone schema from source referential def perform(id) ref_cloning = ReferentialCloning.find id source_schema = ref_cloning.source_referential.slug - target_schema = "#{source_schema}_tmp" + target_schema = ref_cloning.target_referential.slug clone_schema ref_cloning, source_schema, target_schema end @@ -17,9 +16,9 @@ class ReferentialCloningWorker def clone_schema ref_cloning, source_schema, target_schema ref_cloning.run! - StoredProcedures.invoke_stored_procedure(:clone_schema, source_schema, target_schema, true) - execute_sql "DROP SCHEMA #{source_schema} CASCADE;" - execute_sql "ALTER SCHEMA #{target_schema} RENAME TO #{source_schema};" + AF83::SchemaCloner + .new(source_schema, target_schema) + .clone_schema ref_cloning.successful! rescue Exception => e diff --git a/bin/spring b/bin/spring index 7b45d374f..fb2ec2ebb 100755 --- a/bin/spring +++ b/bin/spring @@ -4,12 +4,14 @@ # It gets overwritten when you run the `spring binstub` command. unless defined?(Spring) - require "rubygems" - require "bundler" + require 'rubygems' + require 'bundler' - if match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m) - Gem.paths = { "GEM_PATH" => [Bundler.bundle_path.to_s, *Gem.path].uniq } - gem "spring", match[1] - require "spring/binstub" + lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read) + spring = lockfile.specs.detect { |spec| spec.name == "spring" } + if spring + Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path + gem 'spring', spring.version + require 'spring/binstub' end end diff --git a/config/initializers/active_record.rb b/config/initializers/active_record.rb index bdf9e0b4b..0a7378532 100644 --- a/config/initializers/active_record.rb +++ b/config/initializers/active_record.rb @@ -1,5 +1 @@ -require_relative '../../lib/af83/stored_procedures' - ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::NATIVE_DATABASE_TYPES[:primary_key] = "bigserial primary key" - -StoredProcedures.create_stored_procedure(:clone_schema) diff --git a/config/initializers/simple_form_bootstrap.rb b/config/initializers/simple_form_bootstrap.rb index 67dfe1ceb..d90ea6398 100644 --- a/config/initializers/simple_form_bootstrap.rb +++ b/config/initializers/simple_form_bootstrap.rb @@ -58,9 +58,9 @@ SimpleForm.setup do |config| b.optional :pattern b.optional :min_max b.optional :readonly - b.use :label, class: 'col-sm-4 control-label' + b.use :label, class: 'col-sm-4 col-xs-5 control-label' - b.wrapper tag: 'div', class: 'col-sm-8' do |ba| + b.wrapper tag: 'div', class: 'col-sm-8 col-xs-7' do |ba| ba.use :input, class: 'form-control' ba.use :error, wrap_with: { tag: 'span', class: 'help-block small' } ba.use :hint, wrap_with: { tag: 'p', class: 'help-block small' } diff --git a/config/locales/actions.en.yml b/config/locales/actions.en.yml index 6e2dd3aa2..fe4d3d4e5 100644 --- a/config/locales/actions.en.yml +++ b/config/locales/actions.en.yml @@ -5,6 +5,7 @@ en: delete: "Delete" search: "Search" add: "Add new" + new: "Add new" show: "See" archive: "Archive" unarchive: "Unarchive" diff --git a/config/locales/actions.fr.yml b/config/locales/actions.fr.yml index 8f2fc90f8..ae0537ebb 100644 --- a/config/locales/actions.fr.yml +++ b/config/locales/actions.fr.yml @@ -6,6 +6,7 @@ fr: search: "Chercher" submit: "Valider" add: 'Ajouter' + new: 'Ajouter' show: 'Consulter' archive: 'Conserver' unarchive: 'Déconserver' diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml index af6feed2c..453b094fc 100644 --- a/config/locales/devise.en.yml +++ b/config/locales/devise.en.yml @@ -13,6 +13,7 @@ validating conformance of data wrt Neptune (French standard NFP 99 506)." introduction2: "The application is deployed in Saas mode and supports :" introduction_item1: "several data exchange formats (Neptune, GTFS, CSV ... Netex coming soon)" introduction_item2: "and also several base map backgrounds (Google, OSM, IGN)." + unauthorized: "Failed to connect due to missing IBOO connection rights" confirmations: new: title: Resend confirmation instructions diff --git a/config/locales/devise.fr.yml b/config/locales/devise.fr.yml index 54b2ff680..d3fd6d03e 100644 --- a/config/locales/devise.fr.yml +++ b/config/locales/devise.fr.yml @@ -14,6 +14,7 @@ norme Neptune (NFP 99 506)" introduction2: "Déployé en mode Saas, ce logiciel est ouvert et peut gérer" introduction_item1: "plusieurs formats (Neptune, GTFS, CSV, prochainement Netex)" introduction_item2: "plusieurs fonds cartographiques, notamment IGN, OSM et Google." + unauthorized: "Vous ne pouvez pas vous connecter car vous n'avez pas les permissions pour accéder à IBOO" confirmations: new: title: Renvoyer le mail de confirmation diff --git a/config/locales/enumerize.en.yml b/config/locales/enumerize.en.yml index 8857e593c..f76237288 100644 --- a/config/locales/enumerize.en.yml +++ b/config/locales/enumerize.en.yml @@ -105,6 +105,11 @@ en: zdlp: ZDLp zdlr: ZDLr lda: LDA + clean_up: + date_type: + between: Between two dates + before: Before date + after: After date line: transport_mode: interchange: Interchange @@ -193,7 +198,7 @@ en: regionalRail: 'Regional rail' interregionalRail: 'Interregional rail' longDistance: 'Long distance' - international: 'International' + intermational: 'International' sleeperRailService: 'Sleeper rail service' nightRail: 'Night rail' carTransportRailService: 'Car transport rail service' diff --git a/config/locales/enumerize.fr.yml b/config/locales/enumerize.fr.yml index 8b6c8092d..14aedd155 100644 --- a/config/locales/enumerize.fr.yml +++ b/config/locales/enumerize.fr.yml @@ -104,6 +104,11 @@ fr: zdlp: ZDLp zdlr: ZDLr lda: LDA + clean_up: + date_type: + between: Entre deux dates + before: Avant une date + after: Après une date line: transport_mode: interchange: Interconnection @@ -193,7 +198,7 @@ fr: regionalRail: 'Train régional' interregionalRail: 'Train interrégional' longDistance: 'Longue distance' - international: 'International' + intermational: Internationale sleeperRailService: 'Train à couchettes' nightRail: 'Train de nuit' carTransportRailService: 'Service ferroviaire de transport de voitures' diff --git a/config/locales/routing_constraint_zones.en.yml b/config/locales/routing_constraint_zones.en.yml index 2d4412dba..59d4484a8 100644 --- a/config/locales/routing_constraint_zones.en.yml +++ b/config/locales/routing_constraint_zones.en.yml @@ -11,7 +11,8 @@ en: updated_at: Updated at objectid: Object ID stop_points_count: Number of stop points - route: Route + route: Associated route + route_id: Associated route errors: models: routing_constraint_zone: @@ -21,6 +22,7 @@ en: stop_points_not_from_route: 'Stop point does not belong to the Route of this Routing constraint zone.' all_stop_points_selected: 'All stop points from route cannot be selected.' routing_constraint_zones: + search_no_results: "No ITL matches your query" actions: new: New routing constraint zone edit: Edit this routing constraint zone @@ -32,4 +34,5 @@ en: title: "Update routing constraint zone %{routing_constraint_zone}" show: title: "Routing constraint zone %{routing_constraint_zone}" - + index: + title: "Interdictions of local trafficous" diff --git a/config/locales/routing_constraint_zones.fr.yml b/config/locales/routing_constraint_zones.fr.yml index 36dcf9301..4faff6606 100644 --- a/config/locales/routing_constraint_zones.fr.yml +++ b/config/locales/routing_constraint_zones.fr.yml @@ -6,12 +6,13 @@ fr: routing_constraint_zone: name: Nom stop_areas: Arrêts - line: Ligne + line: Ligne associée created_at: "Créé le" updated_at: "Edité le" objectid: Object ID stop_points_count: Nombre d'arrêts - route: Itinéraire + route: Itinéraire associé + route_id: Itinéraire associé errors: models: routing_constraint_zone: @@ -21,14 +22,14 @@ fr: stop_points_not_from_route: "Arrêt sur séquence d'arrêts n'appartient pas à la Route de cette Zone de contrainte." all_stop_points_selected: "Une zone de contrainte ne peut pas couvrir tous les arrêts d'une ligne." routing_constraint_zones: + search_no_results: "Aucune ITL ne correspond à votre recherche" actions: - new: Ajouter une zone de contrainte - edit: Editer cette zone de contrainte - destroy: Supprimer cette zone de contrainte destroy_confirm: Etes vous sûr de supprimer cette zone de contrainte ? new: - title: Ajouter une zone de contrainte + title: Ajouter un ITL edit: title: "Editer la zone de contrainte %{routing_constraint_zone}" show: title: "Zone de contrainte %{routing_constraint_zone}" + index: + title: "Interdictions de traffic local" diff --git a/config/locales/time_tables.en.yml b/config/locales/time_tables.en.yml index 6c5aac012..dc6c6d9f4 100644 --- a/config/locales/time_tables.en.yml +++ b/config/locales/time_tables.en.yml @@ -87,6 +87,7 @@ en: calendars: "Calendar view" calendar_details: "Calendar details" calendar: "Calendar" + calendar_id: "Calendar" dates: "Peculiar dates" date: "On" excluded_dates: "Excluded dates" diff --git a/config/locales/time_tables.fr.yml b/config/locales/time_tables.fr.yml index 36a588eb4..886aaa263 100644 --- a/config/locales/time_tables.fr.yml +++ b/config/locales/time_tables.fr.yml @@ -68,7 +68,7 @@ fr: other: "calendriers" attributes: time_table: - comment: "Nom" + comment: "Nom du calendrier" color: "Couleur associée" bounding_dates: 'Période contenue dans le calendrier' version: "Abréviation" @@ -89,6 +89,7 @@ fr: calendars: "Calendrier" calendar_details: "Données du calendrier" calendar: Modèle de calendrier + calendar_id: Modèle de calendrier dates: "Dates particulières" date: "Le" excluded_dates: "Dates exclues" diff --git a/db/migrate/20170531154114_add_default_to_vehicle_journey_at_stops_arrival_and_departure_day_offset.rb b/db/migrate/20170531154114_add_default_to_vehicle_journey_at_stops_arrival_and_departure_day_offset.rb new file mode 100644 index 000000000..593d5822d --- /dev/null +++ b/db/migrate/20170531154114_add_default_to_vehicle_journey_at_stops_arrival_and_departure_day_offset.rb @@ -0,0 +1,6 @@ +class AddDefaultToVehicleJourneyAtStopsArrivalAndDepartureDayOffset < ActiveRecord::Migration + def change + change_column_default :vehicle_journey_at_stops, :arrival_day_offset, 0 + change_column_default :vehicle_journey_at_stops, :departure_day_offset, 0 + end +end diff --git a/db/migrate/20170605135126_add_date_type_to_clean_ups.rb b/db/migrate/20170605135126_add_date_type_to_clean_ups.rb new file mode 100644 index 000000000..693e1ccce --- /dev/null +++ b/db/migrate/20170605135126_add_date_type_to_clean_ups.rb @@ -0,0 +1,5 @@ +class AddDateTypeToCleanUps < ActiveRecord::Migration + def change + add_column :clean_ups, :date_type, :string + end +end diff --git a/db/migrate/20170607141031_change_begin_date_from_clean_ups.rb b/db/migrate/20170607141031_change_begin_date_from_clean_ups.rb new file mode 100644 index 000000000..850616b18 --- /dev/null +++ b/db/migrate/20170607141031_change_begin_date_from_clean_ups.rb @@ -0,0 +1,9 @@ +class ChangeBeginDateFromCleanUps < ActiveRecord::Migration + def up + change_column :clean_ups, :begin_date, :date + end + + def down + change_column :clean_ups, :begin_date, :datetime + end +end diff --git a/db/migrate/20170607141317_change_end_date_from_clean_ups.rb b/db/migrate/20170607141317_change_end_date_from_clean_ups.rb new file mode 100644 index 000000000..e47197ff1 --- /dev/null +++ b/db/migrate/20170607141317_change_end_date_from_clean_ups.rb @@ -0,0 +1,9 @@ +class ChangeEndDateFromCleanUps < ActiveRecord::Migration + def up + change_column :clean_ups, :end_date, :date + end + + def down + change_column :clean_ups, :end_date, :datetime + end +end diff --git a/db/schema.rb b/db/schema.rb index a45181687..e64e5c04a 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: 20170502130327) do +ActiveRecord::Schema.define(version: 20170607141317) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -108,10 +108,11 @@ ActiveRecord::Schema.define(version: 20170502130327) do t.datetime "started_at" t.datetime "ended_at" t.integer "referential_id", limit: 8 - t.datetime "begin_date" + t.date "begin_date" t.datetime "created_at" t.datetime "updated_at" - t.datetime "end_date" + t.date "end_date" + t.string "date_type" end add_index "clean_ups", ["referential_id"], name: "index_clean_ups_on_referential_id", using: :btree @@ -283,7 +284,7 @@ ActiveRecord::Schema.define(version: 20170502130327) do t.datetime "started_at" t.datetime "ended_at" t.string "token_download" - t.string "type" + t.string "type", limit: 255 end add_index "imports", ["referential_id"], name: "index_imports_on_referential_id", using: :btree @@ -601,7 +602,7 @@ ActiveRecord::Schema.define(version: 20170502130327) do create_table "stop_areas", id: :bigserial, force: :cascade do |t| t.integer "parent_id", limit: 8 - t.string "objectid", null: false + t.string "objectid", null: false t.integer "object_version", limit: 8 t.string "creator_id" t.string "name" @@ -610,8 +611,8 @@ ActiveRecord::Schema.define(version: 20170502130327) do t.string "registration_number" t.string "nearest_topic_name" t.integer "fare_code" - t.decimal "longitude", precision: 19, scale: 16 - t.decimal "latitude", precision: 19, scale: 16 + t.decimal "longitude", precision: 19, scale: 16 + t.decimal "latitude", precision: 19, scale: 16 t.string "long_lat_type" t.string "country_code" t.string "street_name" @@ -629,7 +630,7 @@ ActiveRecord::Schema.define(version: 20170502130327) do t.datetime "deleted_at" t.datetime "created_at" t.datetime "updated_at" - t.string "stif_type" + t.string "stif_type", limit: 255 end add_index "stop_areas", ["name"], name: "index_stop_areas_on_name", using: :btree @@ -696,18 +697,18 @@ ActiveRecord::Schema.define(version: 20170502130327) do add_index "time_table_periods", ["time_table_id"], name: "index_time_table_periods_on_time_table_id", using: :btree create_table "time_tables", id: :bigserial, force: :cascade do |t| - t.string "objectid", null: false - t.integer "object_version", limit: 8, default: 1 + t.string "objectid", null: false + t.integer "object_version", limit: 8, default: 1 t.string "creator_id" t.string "version" t.string "comment" - t.integer "int_day_types", default: 0 + t.integer "int_day_types", default: 0 t.date "start_date" t.date "end_date" t.integer "calendar_id", limit: 8 t.datetime "created_at" t.datetime "updated_at" - t.string "color" + t.string "color", limit: 255 t.integer "created_from_id" end @@ -783,8 +784,8 @@ ActiveRecord::Schema.define(version: 20170502130327) do t.time "departure_time" t.string "for_boarding" t.string "for_alighting" - t.integer "departure_day_offset" - t.integer "arrival_day_offset" + t.integer "departure_day_offset", default: 0 + t.integer "arrival_day_offset", default: 0 end add_index "vehicle_journey_at_stops", ["stop_point_id"], name: "index_vehicle_journey_at_stops_on_stop_pointid", using: :btree diff --git a/install/README b/install/README new file mode 100644 index 000000000..9825e6575 --- /dev/null +++ b/install/README @@ -0,0 +1,9 @@ +Script d'initialisation de l'environnement logiciel pour accueillir le BOIV +Prérequis : debian 8 (Jessie) netinst + +Paramètres : exporter les variables d'environnements +DATABASE_HOST permet de régler l'hôte qui héberge la base de données +valeur par défaut : localhost + +Procédure : en tant qu'utilisateur root, exécuter la commande depuis le répertoire courrant +./stif-boiv-setup.sh diff --git a/install/deploy-helper.sh b/install/deploy-helper.sh new file mode 100755 index 000000000..487c4539c --- /dev/null +++ b/install/deploy-helper.sh @@ -0,0 +1,102 @@ +#!/bin/bash -e + +export BASEDIR=$PREFIX/var/www/stif-boiv + +export RUN_USER=www-data +export RUN_GROUP=src + +export SUDO="" + +function setup() { + mkdir -p $BASEDIR + mkdir -p $BASEDIR/releases $BASEDIR/shared + + $SUDO mkdir -p $PREFIX/etc/stif-boiv + ln -fs $PREFIX/etc/stif-boiv $BASEDIR/shared/config + + mkdir -p $BASEDIR/shared/config/environments + + mkdir -p $BASEDIR/shared/public + mkdir -p $BASEDIR/shared/public/uploads + mkdir -p $BASEDIR/shared/public/assets + + mkdir -p $BASEDIR/shared/tmp/uploads + + $SUDO chown $RUN_USER:$RUN_GROUP $BASEDIR/shared/public/uploads $BASEDIR/shared/tmp/uploads + + default_config +} + +function default_config() { + DATABASE_PASSWORD=${DATABASE_PASSWORD:-FIXME} + DATABASE_HOST=${DATABASE_HOST:-"localhost"} + + cd $BASEDIR/shared/config + + if [ ! -f secrets.yml ]; then + cat > secrets.yml <<EOF +production: + secret_key_base: `echo $RANDOM | sha512sum | cut -f1 -d' '` +EOF + fi + + if [ ! -f database.yml ]; then + cat > database.yml <<EOF +production: + adapter: postgresql + encoding: unicode + pool: 5 + + host: $DATABASE_HOST + database: stif-boiv + + username: stif-boiv + password: $DATABASE_PASSWORD +EOF + fi +} + +function install() { + tar_file=$1 + + # stif-boiv-20160617154541.tar + release_name=`echo $tar_file | sed 's/.*-\([0-9]*\)\.tar/\1/g'` + + RELEASE_PATH=$BASEDIR/releases/$release_name + + if [ -d $RELEASE_PATH ]; then + echo "Release directory $RELEASE_PATH already exists" + return + fi + + mkdir -p $RELEASE_PATH + + tar -xf $tar_file -C $RELEASE_PATH + + cd $RELEASE_PATH + + mkdir -p tmp + + for directory in public/uploads tmp/uploads public/assets; do + local_directory=$BASEDIR/shared/$directory + release_directory=$directory + + rm -rf $release_directory + ln -s $local_directory $release_directory + done + + for file in secrets.yml database.yml environments/production.rb; do + local_file=$BASEDIR/shared/config/$file + release_file=config/$file + + rm $release_file && ln -fs $local_file $release_file + done + + echo "Release installed into $RELEASE_PATH" +} + +command=$1 +shift + +set -x +$command $@ diff --git a/install/sidekiq-stif-boiv.service b/install/sidekiq-stif-boiv.service new file mode 100644 index 000000000..90c6ed391 --- /dev/null +++ b/install/sidekiq-stif-boiv.service @@ -0,0 +1,52 @@ +# +# systemd unit file for CentOS 7, Ubuntu 15.04 +# +# Customize this file based on your bundler location, app directory, etc. +# Put this in /usr/lib/systemd/system (CentOS) or /lib/systemd/system (Ubuntu). +# Run: +# - systemctl enable sidekiq +# - systemctl {start,stop,restart} sidekiq +# +# This file corresponds to a single Sidekiq process. Add multiple copies +# to run multiple processes (sidekiq-1, sidekiq-2, etc). +# +# See Inspeqtor's Systemd wiki page for more detail about Systemd: +# https://github.com/mperham/inspeqtor/wiki/Systemd +# +[Unit] +Description=sidekiq stif-boiv +# start us only once the network and logging subsystems are available, +# consider adding redis-server.service if Redis is local and systemd-managed. +After=syslog.target network.target + +# See these pages for lots of options: +# http://0pointer.de/public/systemd-man/systemd.service.html +# http://0pointer.de/public/systemd-man/systemd.exec.html +[Service] +Type=simple +WorkingDirectory=/var/www/stif-boiv/current +PIDFile=/var/run/sidekiq-stif-boiv.pid + +PermissionsStartOnly=true +ExecStartPre=-/bin/touch /var/run/sidekiq-stif-boiv.pid +ExecStartPre=/bin/chown www-data:www-data /var/run/sidekiq-stif-boiv.pid + +ExecStart= /usr/local/bin/bundle exec sidekiq -e production -P /var/run/sidekiq-stif-boiv.pid + +User=www-data +Group=www-data +UMask=0002 + +# if we crash, restart +RestartSec=10 +Restart=on-failure + +# output goes to /var/log/syslog +StandardOutput=syslog +StandardError=syslog + +# This will default to "bundler" if we don't specify it +SyslogIdentifier=stif-boiv/sidekiq + +[Install] +WantedBy=multi-user.target diff --git a/install/stif-boiv-setup.sh b/install/stif-boiv-setup.sh new file mode 100755 index 000000000..a2b8bd4a6 --- /dev/null +++ b/install/stif-boiv-setup.sh @@ -0,0 +1,84 @@ +#!/bin/bash -e + +DATABASE_HOST=${DATABASE_HOST:-"localhost"} + +# mandatory packages and distribution upgrade +apt-get update && apt-get dist-upgrade +apt-get install -y wget sudo + +# ruby +echo "==== Installation de Ruby 2.3" +cat > /etc/apt/sources.list.d/bearstech.list <<EOF +deb http://deb.bearstech.com/debian jessie-bearstech main +EOF + +wget -q -O - http://deb.bearstech.com/bearstech-archive.gpg | apt-key add - +apt-get update +apt-get install -y ruby2.3 ruby2.3-dev +apt-get install -y libsqlite3-dev libproj-dev libpq-dev +gem2.3 install bundler + +# Apache / Passenger +echo "==== Installation de Apache 2.4 et Passenger" +apt-get install -y apache2 libapache2-mod-passenger + +cp stif-boiv.conf /etc/apache2/sites-available/ + +a2enmod expires +a2ensite stif-boiv + +# Redis + +echo "==== Installation de Redis" + +apt-get install -y redis-server + +# Sidekiq + +echo "==== Installation de Sidekiq comme service" +cp sidekiq-stif-boiv.service /etc/systemd/system/ +systemctl enable sidekiq-stif-boiv + + +echo "==== Installation de PostgreSQL" +if [ "x$DATABASE_HOST" = "xlocalhost" ]; then +apt-get install -y postgresql-9.4 postgresql-9.4-postgis-2.1 postgresql-contrib-9.4 +[ -d /usr/local/share/postgresql ] || mkdir -p /usr/local/share/postgresql/ +cp template-stif-boiv.sql /usr/local/share/postgresql/ +pushd . +cd /usr/local/share/postgresql +sudo -u postgres createdb --encoding UTF-8 template_stif_boiv < template-stif-boiv.sql +popd +echo "Saisissez le mot de passe de la base de données. Il vous sera redemandé ultérieurement" +sudo -u postgres createuser --pwprompt stif_boiv +sudo -u postgres createdb --owner stif_boiv --template template_stif_boiv stif_boiv +else +echo "W! Base de donnée externe : Pas d'installation" +fi + +# NodeJS + +echo "==== Installation de NodeJS 5.x" +apt-get install -y apt-transport-https + +cat > /etc/apt/sources.list.d/nodesource.list <<EOF +deb https://deb.nodesource.com/node_5.x jessie main +EOF + +wget -q -O - https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - +apt-get update + +apt-get install -y nodejs + +# Configuration de l'applicatif + +echo "==== Paramétrage de l'applicatif" +echo -n "Veuillez saisir à nouveau le mot de passe d'accès à la base de données :" +read -s DATABASE_PASSWORD + +export DATABASE_PASSWORD + +PGPASSWORD=$DATABASE_PASSWORD PGHOST=$DATABASE_HOST PGUSER=stif_boiv psql -q -c 'select 1' stif_boiv >/dev/null 2>&1 && echo "Mot de passe correct" +./deploy-helper.sh setup + +echo "!!! Configuration intiale terminée. Vous pouvez maintenant déployer l'applicatif" diff --git a/install/stif-boiv.conf b/install/stif-boiv.conf new file mode 100644 index 000000000..6b2abe4eb --- /dev/null +++ b/install/stif-boiv.conf @@ -0,0 +1,38 @@ +<VirtualHost *:80> + ServerName boiv.stif.info + + DocumentRoot /var/www/stif-boiv/current/public + + PassengerDefaultUser www-data + PassengerUserSwitching off + + PassengerRuby /usr/bin/ruby2.3 + RackEnv production + + ExpiresActive On + + ExpiresByType application/javascript "access plus 1 year" + ExpiresByType application/x-javascript "access plus 1 year" + ExpiresByType application/x-shockwave-flash "access plus 1 year" + ExpiresByType image/gif "access plus 1 year" + ExpiresByType image/ico "access plus 1 year" + ExpiresByType image/jpeg "access plus 1 year" + ExpiresByType image/jpg "access plus 1 year" + ExpiresByType image/png "access plus 1 year" + ExpiresByType image/vnd.microsoft.icon "access plus 1 year" + ExpiresByType image/x-icon "access plus 1 year" + ExpiresByType text/css "access plus 1 year" + ExpiresByType text/javascript "access plus 1 year" + ExpiresByType font/truetype "access plus 1 year" + ExpiresByType application/x-font-ttf "access plus 1 year" + + <Directory /var/www/stif-boiv/current/public> + AllowOverride None + Require all granted + </Directory> + + <Location /sidekiq> + # Replace with correct policy + Require all denied + </Location> +</virtualHost> diff --git a/install/template-stif-boiv.sql b/install/template-stif-boiv.sql new file mode 100644 index 000000000..4d4a19afe --- /dev/null +++ b/install/template-stif-boiv.sql @@ -0,0 +1,11 @@ +set ON_ERROR_STOP=on; + +CREATE SCHEMA shared_extensions; +GRANT ALL ON SCHEMA shared_extensions TO PUBLIC; +CREATE EXTENSION IF NOT EXISTS postgis WITH SCHEMA shared_extensions; +CREATE EXTENSION IF NOT EXISTS hstore WITH SCHEMA shared_extensions; + +UPDATE pg_database SET datistemplate = TRUE WHERE datname = 'template_stif_boiv'; + +VACUUM FULL FREEZE; + diff --git a/lib/af83/schema_cloner.rb b/lib/af83/schema_cloner.rb new file mode 100644 index 000000000..6ffb646f3 --- /dev/null +++ b/lib/af83/schema_cloner.rb @@ -0,0 +1,151 @@ +module AF83 + class SchemaCloner + + attr_reader :source_schema, :target_schema + + def clone_schema + assure_schema_preconditons + create_target_schema + end + + private + + def adjust_default table_name, column_name, default_val + changed_default = default_val.sub(%r{\Anextval\('#{source_schema}}, "nextval('#{target_schema}") + execute "ALTER TABLE #{target_schema}.#{table_name} ALTER COLUMN #{column_name} SET DEFAULT #{changed_default}" + end + + def adjust_defaults table_name + pairs = execute <<-EOSQL + SELECT column_name, column_default + FROM information_schema.columns + WHERE table_schema = '#{target_schema}' AND table_name = '#{table_name}' AND + column_default LIKE 'nextval(''#{source_schema}%::regclass)' + EOSQL + pairs.each do | pair | + adjust_default table_name, pair['column_name'], pair['column_default'] + end + end + + def alter_sequence sequence_name + seq_props = execute_get_ostruct( "SELECT * FROM #{source_schema}.#{sequence_name}" ) + cycle_on_off = seq_props.is_cycled == 't' ? '' : 'NO' + execute <<-EOSQL + ALTER SEQUENCE #{target_schema}.#{sequence_name} + INCREMENT BY #{seq_props.increment_by} + MINVALUE #{seq_props.min_value} + MAXVALUE #{seq_props.max_value} + START WITH #{seq_props.start_value} + RESTART WITH #{seq_props.last_value} + CACHE #{seq_props.cache_value} + #{cycle_on_off} CYCLE; + + -- TODO: What is this good for? + SELECT setval('#{target_schema}.#{sequence_name}', #{seq_props.last_value}, '#{seq_props.is_called}'); + EOSQL + end + + def assure_schema_preconditons + raise RuntimeError, "Source Schema #{source_schema} does not exist" unless source + log 'found', source + end + + def clone_foreign_key fk_desc + relname, conname, constraint_def = fk_desc.values_at(*%w[relname conname constraint_def]) + constraint_def = constraint_def.sub(" REFERENCES #{source_schema}.", " REFERENCES #{target_schema}.") + execute <<-EOSQL + ALTER TABLE #{target_schema}.#{relname} ADD CONSTRAINT #{conname} #{constraint_def} + EOSQL + end + def clone_foreign_keys + get_foreign_keys.each(&method(:clone_foreign_key)) + end + + def clone_sequence sequence_name + create_sequence sequence_name + alter_sequence sequence_name + end + def clone_sequences + source_sequence_names.each(&method(:clone_sequence)) + end + + def clone_table table_name + create_table table_name + end + + def clone_tables + table_names.each(&method(:clone_table)) + end + + def create_sequence sequence_name + execute "CREATE SEQUENCE #{target_schema}.#{sequence_name}" + end + def create_table table_name + execute "CREATE TABLE #{target_schema}.#{table_name} (LIKE #{source_schema}.#{table_name} INCLUDING ALL)" + execute "INSERT INTO #{target_schema}.#{table_name} SELECT * FROM #{source_schema}.#{table_name}" + adjust_defaults table_name + end + def create_target_schema + execute("CREATE SCHEMA IF NOT EXISTS #{target_schema}") + clone_sequences + clone_tables + clone_foreign_keys + end + def execute(str) + connection.execute(str).to_a + end + def execute_get_first(str) + execute(str).first + end + def execute_get_ostruct(str) + OpenStruct.new(execute_get_first(str)) + end + def execute_get_values(str) + execute(str).flat_map(&:values) + end + + def get_foreign_keys + execute <<-EOS + SELECT rn.relname, ct.conname, pg_get_constraintdef(ct.oid) AS constraint_def + FROM pg_constraint ct JOIN pg_class rn ON rn.oid = ct.conrelid + WHERE connamespace = #{source['oid']} AND rn.relkind = 'r' AND ct.contype = 'f' + EOS + end + def get_columns(table_name) + end + + def initialize(source_schema, target_schema) + @source_schema = source_schema + @target_schema = target_schema + end + + def log(*messages) + messages.each do | message | + Rails.logger.info "SchemaCloner: #{message.inspect}" + end + end + + # + # Memvars + # ------- + def connection + @__connection__ ||= ActiveRecord::Base.connection + end + + def source + @__source__ ||= execute("SELECT oid FROM pg_namespace WHERE nspname = '#{source_schema}' LIMIT 1").first; + end + def source_sequence_names + @__source_sequence_names__ ||= + execute_get_values \ + "SELECT sequence_name::text FROM information_schema.sequences WHERE sequence_schema = '#{source_schema}'" + end + def source_oid + @__source_oid__ ||= source["oid"].to_i; + end + def table_names + @__table_names__ ||= execute_get_values \ + "SELECT TABLE_NAME::text FROM information_schema.tables WHERE table_schema = '#{ source_schema }' AND table_type = 'BASE TABLE'" + end + end +end diff --git a/lib/af83/stored_procedures.rb b/lib/af83/stored_procedures.rb deleted file mode 100644 index 698f3861d..000000000 --- a/lib/af83/stored_procedures.rb +++ /dev/null @@ -1,44 +0,0 @@ -module StoredProcedures extend self - - def invoke_stored_procedure(name, *params) - name = name.to_s - raise ArgumentError, "no such stored procedure #{name.inspect}" unless stored_procedures[name] - invocation = "#{name}(#{quote_params(params)})" - ActiveRecord::Base.connection.execute "SELECT #{invocation}" - end - - def create_stored_procedure(name) - name = name.to_s - sql_file = File.expand_path("../../sql/#{name}.sql", __FILE__) - raise ArgumentError, "missing sql file #{sql_file.inspect}" unless File.readable? sql_file - - # We could store the file's content for reload without application restart if desired. - stored_procedures[name] = true - - ActiveRecord::Base.connection.execute File.read(sql_file) - end - - private - def quote_params(params) - params - .map(&method(:quote_param)) - .join(", ") - end - - def quote_param(param) - case param - when String - "'#{param}'" - when TrueClass - "TRUE" - when FalseClass - "FALSE" - else - param - end - end - - def stored_procedures - @__stored_procedures__ ||= {} - end -end diff --git a/lib/sql/clone_schema.sql b/lib/sql/clone_schema.sql deleted file mode 100644 index 9fb88466b..000000000 --- a/lib/sql/clone_schema.sql +++ /dev/null @@ -1,114 +0,0 @@ -CREATE OR REPLACE FUNCTION clone_schema( source_schema text, dest_schema text, include_recs boolean) RETURNS void AS -$BODY$ - DECLARE - src_oid oid; - tbl_oid oid; - func_oid oid; - object text; - buffer text; - srctbl text; - default_ text; - column_ text; - qry text; - dest_qry text; - v_def text; - seqval bigint; - sq_last_value bigint; - sq_max_value bigint; - sq_start_value bigint; - sq_increment_by bigint; - sq_min_value bigint; - sq_cache_value bigint; - sq_log_cnt bigint; - sq_is_called boolean; - sq_is_cycled boolean; - sq_cycled char(10); - BEGIN - -- Assure that source_schema exists - SELECT oid INTO src_oid FROM pg_namespace WHERE nspname = quote_ident(source_schema); - IF NOT FOUND THEN - RAISE NOTICE 'source schema % does not exist!', source_schema; - RETURN; - END IF; - - -- Refute that dest_schema exists and then create it - PERFORM nspname FROM pg_namespace WHERE nspname = quote_ident(dest_schema); - IF FOUND THEN - RAISE NOTICE 'dest schema % already exists!', dest_schema; - RETURN; - END IF; - EXECUTE 'CREATE SCHEMA ' || quote_ident(dest_schema) ; - - -- loop over sequences, creating the same one in the destination namespace, ... - FOR object IN - SELECT sequence_name::text FROM information_schema.sequences WHERE sequence_schema = quote_ident(source_schema) - LOOP - EXECUTE 'CREATE SEQUENCE ' || quote_ident(dest_schema) || '.' || quote_ident(object); - - -- ...storing the attributes of the sequence from the source namespace in local variables, ... - srctbl := quote_ident(source_schema) || '.' || quote_ident(object); - EXECUTE 'SELECT last_value, max_value, start_value, increment_by, min_value, cache_value, log_cnt, is_cycled, is_called FROM ' || srctbl || ';' INTO sq_last_value, sq_max_value, sq_start_value, sq_increment_by, sq_min_value, sq_cache_value, sq_log_cnt, sq_is_cycled, sq_is_called; - IF sq_is_cycled THEN - sq_cycled := 'CYCLE'; - ELSE - sq_cycled := 'NO CYCLE'; - END IF; - EXECUTE 'ALTER SEQUENCE ' || (dest_schema) || '.' || quote_ident(object) || ' INCREMENT BY ' || sq_increment_by || ' MINVALUE ' || sq_min_value || ' MAXVALUE ' || sq_max_value || ' START WITH ' || sq_start_value || ' RESTART ' || sq_min_value || ' CACHE ' || sq_cache_value || sq_cycled || ' ;' ; - - buffer := quote_ident(dest_schema) || '.' || quote_ident(object); - IF include_recs THEN - EXECUTE 'SELECT setval( ''' || buffer || ''', ' || sq_last_value || ', ' || sq_is_called || ');' ; - ELSE - EXECUTE 'SELECT setval( ''' || buffer || ''', ' || sq_start_value || ', ' || sq_is_called || ');' ; - END IF; - END LOOP; - - -- loop over tables in source schema,... - FOR object IN - SELECT TABLE_NAME::text FROM information_schema.tables WHERE table_schema = quote_ident(source_schema) AND table_type = 'BASE TABLE' - LOOP - -- ...creating the table in the destination schema, potentially including the records - buffer := dest_schema || '.' || quote_ident(object); - EXECUTE 'CREATE TABLE ' || buffer || '(LIKE ' || quote_ident(source_schema) || '.' || quote_ident(object) || ' INCLUDING ALL)'; - IF include_recs THEN - EXECUTE 'INSERT INTO ' || buffer || ' SELECT * FROM ' || quote_ident(source_schema) || '.' || quote_ident(object) || ';'; - END IF; - - -- alter table, assuring the destination schema's table has: - -- * the same defaults - FOR column_, default_ - IN SELECT column_name::text, REPLACE(column_default::text, source_schema, dest_schema) - FROM information_schema.COLUMNS - WHERE table_schema = dest_schema AND TABLE_NAME = object AND column_default LIKE 'nextval(%' || quote_ident(source_schema) || '%::regclass)' - LOOP - EXECUTE 'ALTER TABLE ' || buffer || ' ALTER COLUMN ' || column_ || ' SET DEFAULT ' || default_; - END LOOP; - END LOOP; - - /* SELECT 'ALTER TABLE ' || quote_ident(dest_schema) || '.' || quote_ident(rn.relname) || ' ADD CONSTRAINT ' || quote_ident(ct.conname) || ' ' || pg_get_constraintdef(ct.oid) || ';' FROM pg_constraint ct JOIN pg_class rn ON rn.oid = ct.conrelid WHERE connamespace = src_oid AND rn.relkind = 'r' AND ct.contype = 'f' LIMIT 2; */ - -- apply all constraints on tables in destination schema - FOR qry IN - SELECT 'ALTER TABLE ' || quote_ident(dest_schema) || '.' || quote_ident(rn.relname) || ' ADD CONSTRAINT ' || quote_ident(ct.conname) || ' ' || REPLACE(pg_get_constraintdef(ct.oid), source_schema || '.', dest_schema || '.') || ';' - FROM pg_constraint ct JOIN pg_class rn ON rn.oid = ct.conrelid WHERE connamespace = src_oid AND rn.relkind = 'r' AND ct.contype = 'f' - LOOP - EXECUTE qry; - END LOOP; - - -- create views from source schema in destination schema - FOR object IN - SELECT table_name::text, view_definition FROM information_schema.views WHERE table_schema = quote_ident(source_schema) - LOOP - buffer := dest_schema || '.' || quote_ident(object); - SELECT view_definition INTO v_def FROM information_schema.views WHERE table_schema = quote_ident(source_schema) AND table_name = quote_ident(object); - EXECUTE 'CREATE OR REPLACE VIEW ' || buffer || ' AS ' || v_def || ';' ; - END LOOP; - - FOR func_oid IN SELECT oid FROM pg_proc WHERE pronamespace = src_oid - LOOP - SELECT pg_get_functiondef(func_oid) INTO qry; - SELECT replace(qry, source_schema, dest_schema) INTO dest_qry; - EXECUTE dest_qry; - END LOOP; - RETURN; - END; -$BODY$ LANGUAGE plpgsql VOLATILE COST 100; diff --git a/lib/tasks/install.rake b/lib/tasks/install.rake new file mode 100644 index 000000000..f32f3f240 --- /dev/null +++ b/lib/tasks/install.rake @@ -0,0 +1,18 @@ +task :package do + release_name = Time.now.strftime('%Y%m%d%H%M%S') + + rm_rf "tmp/package" + mkdir_p "tmp/package" + + sh "git archive --format=tar --output=tmp/package/stif-boiv-release-#{release_name}.tar HEAD" + + sh "bundle package --all" + sh "tar -rf tmp/package/stif-boiv-release-#{release_name}.tar vendor/cache" + + %w{deploy-helper.sh README sidekiq-stif-boiv.service stif-boiv.conf stif-boiv-setup.sh template-stif-boiv.sql}.each do |f| + cp "install/#{f}", "tmp/package/#{f}" + end + + sh "tar -czf stif-boiv-#{release_name}.tar.gz -C tmp/package ." + sh "rm -rf tmp/package vendor/cache" +end diff --git a/lib/tasks/referential.rake b/lib/tasks/referential.rake index 76f1b4c00..ce1ded4fc 100644 --- a/lib/tasks/referential.rake +++ b/lib/tasks/referential.rake @@ -22,7 +22,9 @@ namespace :referential do stop_areas = workbench.stop_area_referential.stop_areas.last(10) 4.times do |i| - route_attrs = { line: line, name: "Route #{Faker::Name.unique.name}" } + name = Faker::Name.unique.name + route_attrs = { line: line, name: "Route #{name}", published_name: "Published #{name}" } + if i.even? route_attrs[:wayback] = :straight_forward route = Chouette::Route.create!(route_attrs) diff --git a/spec/controllers/devise/cas_sessions_controller_spec.rb b/spec/controllers/devise/cas_sessions_controller_spec.rb new file mode 100644 index 000000000..950d141fd --- /dev/null +++ b/spec/controllers/devise/cas_sessions_controller_spec.rb @@ -0,0 +1,27 @@ +RSpec.describe Devise::CasSessionsController, type: :controller do + + login_user + + context 'login is correctly redirected' do + it 'to #service' do + get :new + expect(response).to redirect_to(unauthenticated_root_path) + end + end + + context 'user does not have any boiv:.+ permission' do + xit 'cannot login and will be redirected to the login page, with a corresponding message' do + get :service + expect(controller).to set_flash[:alert].to(%r{IBOO}) + expect(response).to redirect_to("http://stif-portail-dev.af83.priv/sessions/login?service=http%3A%2F%2Ftest.host%2Fusers%2Fservice") + end + end + + context 'user does have a boiv:.+ permission' do + it 'can login and will be redirected to the referentials page' do + @user.update_attribute :permissions, (@user.permissions << 'boiv:UnameIt') + get :service + expect(response).to redirect_to(authenticated_root_path) + end + end +end diff --git a/spec/controllers/imports_controller_spec.rb b/spec/controllers/imports_controller_spec.rb index bffb89338..7b575ab61 100644 --- a/spec/controllers/imports_controller_spec.rb +++ b/spec/controllers/imports_controller_spec.rb @@ -1,5 +1,3 @@ -require 'rails_helper' - RSpec.describe ImportsController, :type => :controller do login_user diff --git a/spec/controllers/route_stop_points_controller_spec.rb b/spec/controllers/route_stop_points_controller_spec.rb index 2f5fa41c7..ac9e2f11b 100644 --- a/spec/controllers/route_stop_points_controller_spec.rb +++ b/spec/controllers/route_stop_points_controller_spec.rb @@ -15,9 +15,7 @@ RSpec.describe RouteStopPointsController, type: :controller do end it 'returns a JSON of stop areas' do - expect(response.body).to eq(route.stop_points.map { |sp| { id: sp.id, name: sp.name } }.to_json) + expect(response.body).to eq(route.stop_points.map { |sp| { id: sp.id, stop_area_id: sp.stop_area.id, name: sp.name, zip_code: sp.stop_area.zip_code, city_name: sp.stop_area.city_name } }.to_json) end end end - - diff --git a/spec/factories/chouette_time_table.rb b/spec/factories/chouette_time_table.rb index f462349cf..6480df79d 100644 --- a/spec/factories/chouette_time_table.rb +++ b/spec/factories/chouette_time_table.rb @@ -1,6 +1,7 @@ FactoryGirl.define do factory :time_table_date, :class => Chouette::TimeTableDate do + association :time_table, :factory => :time_table end factory :time_table_period, :class => Chouette::TimeTablePeriod do diff --git a/spec/factories/chouette_vehicle_journey.rb b/spec/factories/chouette_vehicle_journey.rb index 452909f23..e7ecb79ac 100644 --- a/spec/factories/chouette_vehicle_journey.rb +++ b/spec/factories/chouette_vehicle_journey.rb @@ -18,11 +18,16 @@ FactoryGirl.define do after(:create) do |vehicle_journey, evaluator| vehicle_journey.journey_pattern.stop_points.each_with_index do |stop_point, index| + prev_stop = vehicle_journey.vehicle_journey_at_stops[index - 1] + + arrival_time = prev_stop ? prev_stop[:departure_time] + 1.minute : evaluator.stop_arrival_time + departure_time = prev_stop ? arrival_time + 1.minute : evaluator.stop_departure_time + vehicle_journey.vehicle_journey_at_stops << create(:vehicle_journey_at_stop, :vehicle_journey => vehicle_journey, :stop_point => stop_point, - :arrival_time => "2000-01-01 #{evaluator.stop_arrival_time} UTC", - :departure_time => "2000-01-01 #{evaluator.stop_departure_time} UTC") + :arrival_time => "2000-01-01 #{arrival_time} UTC", + :departure_time => "2000-01-01 #{departure_time} UTC") end end diff --git a/spec/factories/clean_ups.rb b/spec/factories/clean_ups.rb index 41165ac16..d3746c3b2 100644 --- a/spec/factories/clean_ups.rb +++ b/spec/factories/clean_ups.rb @@ -1,15 +1,6 @@ FactoryGirl.define do factory :clean_up do - status "MyString" -started_at "2016-11-14 14:45:18" -ended_at "2016-11-14 14:45:18" -referential nil -keep_lines false -keep_stops false -keep_companies false -keep_networks false -keep_group_of_lines false -expected_date "2016-11-14 14:45:18" + begin_date { Date.today} + end_date { Date.today + 1.month } end - end diff --git a/spec/features/calendars_spec.rb b/spec/features/calendars_spec.rb index 2089939bb..e15624295 100644 --- a/spec/features/calendars_spec.rb +++ b/spec/features/calendars_spec.rb @@ -19,12 +19,20 @@ describe 'Calendars', type: :feature do context 'filtering' do it 'supports filtering by short name' do - fill_in 'q[short_name_cont]', with: calendars.first.short_name + fill_in 'q[name_or_short_name_cont]', with: calendars.first.short_name click_button 'search_btn' expect(page).to have_content(calendars.first.short_name) expect(page).not_to have_content(calendars.last.short_name) end + it 'supports filtering by name' do + fill_in 'q[name_or_short_name_cont]', with: calendars.first.name + click_button 'search_btn' + expect(page).to have_content(calendars.first.name) + expect(page).not_to have_content(calendars.last.name) + end + + it 'supports filtering by shared' do shared_calendar = create :calendar, organisation_id: 1, shared: true visit calendars_path diff --git a/spec/features/lines_spec.rb b/spec/features/lines_spec.rb index e7e1e601c..a55f30ebc 100644 --- a/spec/features/lines_spec.rb +++ b/spec/features/lines_spec.rb @@ -1,7 +1,4 @@ -# -*- coding: utf-8 -*- -require 'spec_helper' - -describe "Lines", :type => :feature do +describe "Lines", type: :feature do login_user let(:line_referential) { create :line_referential } diff --git a/spec/features/referentials_permissions_spec.rb b/spec/features/referentials_permissions_spec.rb new file mode 100644 index 000000000..0216eeeb0 --- /dev/null +++ b/spec/features/referentials_permissions_spec.rb @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- + +describe "Referentials", :type => :feature do + + login_user + let(:referential) { Referential.first } + + let( :edit_link_text ){ I18n.t('actions.edit') } + let( :destroy_link_text ){ I18n.t('actions.destroy') } + + + context 'permissions' do + before do + allow_any_instance_of(ReferentialPolicy).to receive(:organisation_match?).and_return organisation_match + visit path + end + + context 'on show view with common lines' do + let( :path ){ referential_path(referential) } + before do + allow_any_instance_of(ReferentialPolicy).to receive(:common_lines?).and_return common_lines + end + + context 'if organisations match →' do + let( :organisation_match ){ true } + let( :common_lines ){ false } + + it 'shows the edit button' do + expected_href = edit_referential_path(referential) + expect( page ).to have_link(edit_link_text, href: expected_href) + end + it 'shows the delete button' do + expected_href = referential_path(referential) + expect( page ).to have_css(%{a[href=#{expected_href.inspect}] span}, text: destroy_link_text) + end + end + + context 'if organisations do not match →' do + let( :organisation_match ){ false } + let( :common_lines ){ true } + + it 'does not show the delete button' do + expected_href = edit_referential_path(referential) + expect( page ).not_to have_link(edit_link_text, href: expected_href) + end + it 'does not show the delete button' do + expected_href = referential_path(referential) + expect( page ).not_to have_css(%{a[href=#{expected_href.inspect}] span}, text: destroy_link_text) + end + end + end + + end +end diff --git a/spec/features/referentials_spec.rb b/spec/features/referentials_spec.rb index 3c2258a3a..337271fea 100644 --- a/spec/features/referentials_spec.rb +++ b/spec/features/referentials_spec.rb @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -require 'spec_helper' - describe "Referentials", :type => :feature do login_user @@ -111,13 +109,9 @@ describe "Referentials", :type => :feature do end describe "create" do - it "should" do visit new_referential_path fill_in "Nom", :with => "Test" - fill_in "Code", :with => "test" - fill_in "Point haut/droite de l'emprise par défaut", :with => "0.0, 0.0" - fill_in "Point bas/gauche de l'emprise par défaut", :with => "1.0, 1.0" click_button "Valider" expect(Referential.where(:name => "Test")).not_to be_nil diff --git a/spec/features/routes_spec.rb b/spec/features/routes_spec.rb index 6d9ba990d..28015f011 100644 --- a/spec/features/routes_spec.rb +++ b/spec/features/routes_spec.rb @@ -36,10 +36,13 @@ describe "Routes", :type => :feature do visit referential_line_path(referential, line) click_link "Ajouter un itinéraire" fill_in "route_name", :with => "A to B" + fill_in "route_published_name", :with => "Published A to B" # select 'Aller', :from => "route_direction" check('route[wayback]') click_button("Valider") expect(page).to have_content("A to B") + expect(page).to have_content("Published A to B") + end end diff --git a/spec/features/routing_constraint_zones_spec.rb b/spec/features/routing_constraint_zones_spec.rb index 9e8c7dad4..b116b38bd 100644 --- a/spec/features/routing_constraint_zones_spec.rb +++ b/spec/features/routing_constraint_zones_spec.rb @@ -20,7 +20,7 @@ describe 'RoutingConstraintZones', type: :feature do context 'user has permission to create routing_constraint_zones' do it 'shows a create link for routing_constraint_zones' do - expect(page).to have_content(I18n.t('routing_constraint_zones.actions.new')) + expect(page).to have_content(I18n.t('actions.new')) end end @@ -28,7 +28,7 @@ describe 'RoutingConstraintZones', type: :feature do it 'does not show a create link for routing_constraint_zones' do @user.update_attribute(:permissions, []) visit referential_line_routing_constraint_zones_path(referential, line) - expect(page).not_to have_content(I18n.t('routing_constraint_zones.actions.new')) + expect(page).not_to have_content(I18n.t('actions.new')) end end diff --git a/spec/features/users/connection_spec.rb b/spec/features/users/connection_spec.rb new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/spec/features/users/connection_spec.rb diff --git a/spec/features/workbenches_spec.rb b/spec/features/workbenches_spec.rb index c11fbd03d..953eb2bf5 100644 --- a/spec/features/workbenches_spec.rb +++ b/spec/features/workbenches_spec.rb @@ -46,8 +46,6 @@ describe 'Workbenches', type: :feature do click_link I18n.t('actions.add') fill_in "referential[name]", with: "Referential to test creation" # Nom du JDD - fill_in "referential[slug]", with: "test" # Code - fill_in "referential[prefix]", with: "test" # Prefix Neptune select workbench.lines.first.id, from: 'referential[metadatas_attributes][0][lines][]' # Lignes click_button "Valider" diff --git a/spec/javascripts/time_table/reducers/pagination_spec.js b/spec/javascripts/time_table/reducers/pagination_spec.js index 740ded3ac..5da58427e 100644 --- a/spec/javascripts/time_table/reducers/pagination_spec.js +++ b/spec/javascripts/time_table/reducers/pagination_spec.js @@ -118,11 +118,4 @@ describe('pagination reducer', () => { }) ).toEqual(Object.assign({}, state, {stateChanged: true})) }) - it('should handle UPDATE_DAY_TYPES', () => { - expect( - paginationReducer(state, { - type: 'UPDATE_DAY_TYPES' - }) - ).toEqual(Object.assign({}, state, {stateChanged: true})) - }) }) diff --git a/spec/javascripts/vehicle_journeys/reducers/vehicle_journeys_spec.js b/spec/javascripts/vehicle_journeys/reducers/vehicle_journeys_spec.js index 662c3d82f..23ebc3d9f 100644 --- a/spec/javascripts/vehicle_journeys/reducers/vehicle_journeys_spec.js +++ b/spec/javascripts/vehicle_journeys/reducers/vehicle_journeys_spec.js @@ -111,7 +111,9 @@ describe('vehicleJourneys reducer', () => { time_tables: [], vehicle_journey_at_stops: pristineVjasList, selected: false, - deletable: false + deletable: false, + transport_mode: 'undefined', + transport_submode: 'undefined' }, ...state]) }) diff --git a/spec/lib/af83/cloning/clone_schema_spec.rb b/spec/lib/af83/cloning/clone_schema_spec.rb new file mode 100644 index 000000000..3d541f3e9 --- /dev/null +++ b/spec/lib/af83/cloning/clone_schema_spec.rb @@ -0,0 +1,113 @@ +RSpec.describe AF83::SchemaCloner, type: :pg_catalog do + let( :source_schema ){ "source_schema" } + let( :target_schema ){ "target_schema" } + let( :child_table ){ "children" } + let( :parent_table ){ "parents" } + + subject { described_class.new source_schema, target_schema } + + context "after cloning" do + before do + create_schema_with_tables + subject.clone_schema + end + + it "table information is correctly duplicated" do + expect(get_table_information(source_schema, child_table)) + .to eq([{"table_schema"=>"source_schema", + "table_name"=>"children", + "table_type"=>"BASE TABLE", + "self_referencing_column_name"=>nil, + "reference_generation"=>nil, + "user_defined_type_catalog"=>nil, + "user_defined_type_schema"=>nil, + "user_defined_type_name"=>nil, + "is_insertable_into"=>"YES", + "is_typed"=>"NO", + "commit_action"=>nil}]) + + expect( get_table_information(target_schema, child_table)) + .to eq([{"table_schema"=>"target_schema", + "table_name"=>"children", + "table_type"=>"BASE TABLE", + "self_referencing_column_name"=>nil, + "reference_generation"=>nil, + "user_defined_type_catalog"=>nil, + "user_defined_type_schema"=>nil, + "user_defined_type_name"=>nil, + "is_insertable_into"=>"YES", + "is_typed"=>"NO", + "commit_action"=>nil}]) + end + + it "table content is the same and sequences are synchronized" do + expect_same_content(parent_table) + expect_same_content(child_table) + + expect_same_sequence_params("#{parent_table}_id_seq") + expect_same_sequence_params("#{child_table}_id_seq") + end + + it "has correctly updated default values" do + child_table_pk_default = get_columns(target_schema, child_table) + .find{ |col| col["column_name"] == "id" }["column_default"] + expect( child_table_pk_default ).to eq("nextval('#{target_schema}.children_id_seq'::regclass)") + end + + it "has the correct foreign keys" do + expect( get_foreign_keys(target_schema, child_table) ) + .to eq([{ + "constraint_name" => "children_parents", + "constraint_def" => "FOREIGN KEY (parents_id) REFERENCES target_schema.parents(id)"}]) + end + + xit "it has the correct unique keys UNTESTABLE SO FAR" do + insert source_schema, child_table, "#{parent_table}_id" => 1, some_key: 400 + insert target_schema, child_table, "#{parent_table}_id" => 1, some_key: 400 + reinsert_sql = "INSERT INTO #{source_schema}.#{child_table} (#{parent_table}_id, some_key) VALUES (1, 400)" + expect{ execute(reinsert_sql) rescue nil}.not_to change{ execute("SELECT COUNT(*) FROM #{source_schema}.#{child_table}") } + + # expect{ insert(target_schema, child_table, "#{parent_table}_id" => 1, some_key: 400) }.to raise_error(ActiveRecord::RecordNotUnique) + end + + it "inserts are independent" do + insert source_schema, child_table, "#{parent_table}_id" => 1, some_key: 400 + insert target_schema, child_table, "#{parent_table}_id" => 1, some_key: 400 + last_source = get_content(source_schema, child_table).last + last_target = get_content(target_schema, child_table).last + + expect( last_source ).to eq("id"=>"3", "parents_id"=>"1", "some_key"=>"400", "is_orphan"=>"f") + expect( last_target ).to eq("id"=>"3", "parents_id"=>"1", "some_key"=>"400", "is_orphan"=>"f") + end + + end + + def create_schema_with_tables + execute <<-EOSQL + DROP SCHEMA IF EXISTS #{source_schema} CASCADE; + CREATE SCHEMA #{source_schema}; + + CREATE TABLE #{source_schema}.#{parent_table} ( + id bigserial PRIMARY KEY + ); + CREATE TABLE #{source_schema}.#{child_table} ( + id bigserial PRIMARY KEY, + #{parent_table}_id bigint, + some_key bigint NOT NULL, + is_orphan boolean DEFAULT false + ); + + CREATE UNIQUE INDEX #{child_table}_some_key_idx ON #{source_schema}.#{child_table} (some_key); + + ALTER TABLE #{source_schema}.#{child_table} + ADD CONSTRAINT #{child_table}_#{parent_table} + FOREIGN KEY( #{parent_table}_id ) REFERENCES #{source_schema}.#{parent_table}(id); + + INSERT INTO #{source_schema}.#{parent_table} VALUES (DEFAULT); + INSERT INTO #{source_schema}.#{parent_table} VALUES (DEFAULT); + EOSQL + insert source_schema, child_table, "#{parent_table}_id" => 1, some_key: 200 + insert source_schema, child_table, "#{parent_table}_id" => 2, some_key: 300, is_orphan: true + end + +end diff --git a/spec/lib/af83/stored_procedure_spec.rb b/spec/lib/af83/stored_procedure_spec.rb deleted file mode 100644 index 2530d7fc1..000000000 --- a/spec/lib/af83/stored_procedure_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -require 'rails_helper' - -RSpec.describe StoredProcedures do - - - before do - described_class.create_stored_procedure(:clone_schema) - end - - let( :source_schema_name ){ "parissudest201604" } - let( :dest_schema_name ){ "#{source_schema_name}_v1"} - - context "Error cases" do - it "raises an error if stored procedure does not exist" do - expect{ described_class.invoke_stored_procedure(:idonotexist) } - .to raise_error(ArgumentError, %r{no such stored procedure "idonotexist"}) - end - end - -end diff --git a/spec/lib/af83/stored_procedures/clone_schema_spec.rb b/spec/lib/af83/stored_procedures/clone_schema_spec.rb deleted file mode 100644 index c387ddc7d..000000000 --- a/spec/lib/af83/stored_procedures/clone_schema_spec.rb +++ /dev/null @@ -1,167 +0,0 @@ -require 'spec_helper' - -include Support::PGCatalog - -RSpec.describe StoredProcedures do - let( :source_schema ){ "source_schema" } - let( :target_schema ){ "target_schema" } - let( :child_table ){ "children" } - let( :parent_table ){ "parents" } - - before do - create_schema_with_tables - StoredProcedures.create_stored_procedure :clone_schema - end - - # :meta specs are not run, as the describe the testing methd and not the application - context "meta specs describe source schema's introspection", :meta do - it "table information is correctly read" do - expect(get_table_information(source_schema, child_table)) - .to eq([{"table_schema"=>"source_schema", - "table_name"=>"children", - "table_type"=>"BASE TABLE", - "self_referencing_column_name"=>nil, - "reference_generation"=>nil, - "user_defined_type_catalog"=>nil, - "user_defined_type_schema"=>nil, - "user_defined_type_name"=>nil, - "is_insertable_into"=>"YES", - "is_typed"=>"NO", - "commit_action"=>nil}]) - - expect( get_table_information(target_schema, child_table) ).to be_empty - end - - it "sequences are correctly read" do - expect(get_sequences(source_schema, child_table)) - .to eq([{"sequence_name"=>"#{child_table}_id_seq", - "last_value"=>"1", - "start_value"=>"1", - "increment_by"=>"1", - "max_value"=>"9223372036854775807", - "min_value"=>"1", - "cache_value"=>"1", - "log_cnt"=>"0", - "is_cycled"=>"f", - "is_called"=>"f"}]) - - expect(get_sequences(source_schema, parent_table)) - .to eq([{"sequence_name"=>"#{parent_table}_id_seq", - "last_value"=>"1", - "start_value"=>"1", - "increment_by"=>"1", - "max_value"=>"9223372036854775807", - "min_value"=>"1", - "cache_value"=>"1", - "log_cnt"=>"0", - "is_cycled"=>"f", - "is_called"=>"f"}]) - end - - it "shows foreign key constraints are correctly read" do - expect( get_foreign_keys(source_schema, child_table) ) - .to eq([{ - "constraint_name" => "children_parents", - "constraint_def" => "FOREIGN KEY (parents_id) REFERENCES source_schema.parents(id)"}]) - end - end - - context "before cloning" do - it "target schema does not exist" do - expect( get_schema_oid(target_schema) ).to be_nil - end - end - - context "after cloning" do - before do - described_class.invoke_stored_procedure(:clone_schema, source_schema, target_schema, false) - end - - it "target schema does exist" do - expect( get_schema_oid(target_schema) ).not_to be_nil - end - - it "table information is correctly read" do - expect(get_table_information(source_schema, child_table)) - .to eq([{"table_schema"=>"source_schema", - "table_name"=>"children", - "table_type"=>"BASE TABLE", - "self_referencing_column_name"=>nil, - "reference_generation"=>nil, - "user_defined_type_catalog"=>nil, - "user_defined_type_schema"=>nil, - "user_defined_type_name"=>nil, - "is_insertable_into"=>"YES", - "is_typed"=>"NO", - "commit_action"=>nil}]) - - expect( get_table_information(target_schema, child_table)) - .to eq([{"table_schema"=>"target_schema", - "table_name"=>"children", - "table_type"=>"BASE TABLE", - "self_referencing_column_name"=>nil, - "reference_generation"=>nil, - "user_defined_type_catalog"=>nil, - "user_defined_type_schema"=>nil, - "user_defined_type_name"=>nil, - "is_insertable_into"=>"YES", - "is_typed"=>"NO", - "commit_action"=>nil}]) - end - - it "has the correct sequences" do - expect(get_sequences(target_schema, child_table)) - .to eq([{"sequence_name"=>"#{child_table}_id_seq", - "last_value"=>"1", - "start_value"=>"1", - "increment_by"=>"1", - "max_value"=>"9223372036854775807", - "min_value"=>"1", - "cache_value"=>"1", - "log_cnt"=>"0", - "is_cycled"=>"f", - "is_called"=>"f"}]) - - expect(get_sequences(target_schema, parent_table)) - .to eq([{"sequence_name"=>"#{parent_table}_id_seq", - "last_value"=>"1", - "start_value"=>"1", - "increment_by"=>"1", - "max_value"=>"9223372036854775807", - "min_value"=>"1", - "cache_value"=>"1", - "log_cnt"=>"0", - "is_cycled"=>"f", - "is_called"=>"f"}]) - end - - it "has the correct foreign keys" do - expect( get_foreign_keys(target_schema, child_table) ) - .to eq([{ - "constraint_name" => "children_parents", - "constraint_def" => "FOREIGN KEY (parents_id) REFERENCES target_schema.parents(id)"}]) - end - - end - -end - -def create_schema_with_tables - execute("CREATE SCHEMA IF NOT EXISTS #{source_schema}") - execute <<-EOSQL - DROP SCHEMA IF EXISTS #{source_schema} CASCADE; - CREATE SCHEMA #{source_schema}; - - CREATE TABLE #{source_schema}.#{parent_table} ( - id bigserial PRIMARY KEY - ); - CREATE TABLE #{source_schema}.#{child_table} ( - id bigserial PRIMARY KEY, - #{parent_table}_id bigint - ); - ALTER TABLE #{source_schema}.#{child_table} - ADD CONSTRAINT #{child_table}_#{parent_table} - FOREIGN KEY( #{parent_table}_id ) REFERENCES #{source_schema}.#{parent_table}(id); - EOSQL -end - diff --git a/spec/lib/time_duration_spec.rb b/spec/lib/time_duration_spec.rb index 1cba1f6d5..bf23f7ba6 100644 --- a/spec/lib/time_duration_spec.rb +++ b/spec/lib/time_duration_spec.rb @@ -4,15 +4,17 @@ describe TimeDuration do describe ".exceeds_gap?" do context "when duration is 4.hours" do it "should return false if gap < 1.hour" do - t1 = Time.now - t2 = Time.now + 3.minutes - expect(TimeDuration.exceeds_gap?(4.hours, t1, t2)).to be_falsey + earlier = Time.now + later = Time.now + 3.minutes + + expect(TimeDuration.exceeds_gap?(4.hours, earlier, later)).to be false end it "should return true if gap > 4.hour" do - t1 = Time.now - t2 = Time.now + (4.hours + 1.minutes) - expect(TimeDuration.exceeds_gap?(4.hours, t1, t2)).to be_truthy + earlier = Time.now + later = Time.now + (4.hours + 1.minutes) + + expect(TimeDuration.exceeds_gap?(4.hours, earlier, later)).to be true end it "returns true when `earlier` is later than `later`" do diff --git a/spec/models/chouette/time_table_spec.rb b/spec/models/chouette/time_table_spec.rb index 3d45bd346..7a8863cb3 100644 --- a/spec/models/chouette/time_table_spec.rb +++ b/spec/models/chouette/time_table_spec.rb @@ -1,12 +1,47 @@ require 'spec_helper' describe Chouette::TimeTable, :type => :model do - subject { create(:time_table) } it { is_expected.to validate_presence_of :comment } it { is_expected.to validate_uniqueness_of :objectid } + context "merge with calendar" do + let(:calendar) { create(:calendar) } + + it 'should add calendar dates to time_table' do + subject.dates.clear + subject.merge!(calendar.convert_to_time_table) + expect(subject.dates.map(&:date)).to include(*calendar.dates) + end + end + + describe "actualize" do + let(:calendar) { create(:calendar) } + let(:int_day_types) { 508 } + + before do + subject.int_day_types = int_day_types + subject.calendar = calendar + subject.save + subject.actualize + end + + it 'should override dates' do + expect(subject.dates.map(&:date)).to match_array calendar.dates + end + + it 'should override periods' do + [:period_start, :period_end].each do |key| + expect(subject.periods.map(&key)).to match_array calendar.convert_to_time_table.periods.map(&key) + end + end + + it 'should not change int_day_types' do + expect(subject.int_day_types).to eq(int_day_types) + end + end + describe "Update state" do def time_table_to_state time_table time_table.slice('id', 'comment').tap do |item| @@ -119,7 +154,7 @@ describe Chouette::TimeTable, :type => :model do end it 'should create new include date' do - day = state['current_month'].first + day = state['current_month'].find{|d| !d['excluded_date'] && !d['include_date'] } date = Date.parse(day['date']) day['include_date'] = true expect(subject.included_days).not_to include(date) @@ -131,7 +166,7 @@ describe Chouette::TimeTable, :type => :model do end it 'should create new exclude date' do - day = state['current_month'].first + day = state['current_month'].find{|d| !d['excluded_date'] && !d['include_date']} date = Date.parse(day['date']) day['excluded_date'] = true expect(subject.excluded_days).not_to include(date) @@ -973,7 +1008,7 @@ end expect(subject.periods[2].period_start).to eq(Date.new(2014, 8, 1)) expect(subject.periods[2].period_end).to eq(Date.new(2014, 8, 12)) end - it "should have common day_types" do + it "should not modify day_types" do expect(subject.int_day_types).to eq(4|16|128) end it "should have dates for thursdays and fridays" do @@ -1007,9 +1042,6 @@ end it "should have no period" do expect(subject.periods.size).to eq(0) end - it "should have no day_types" do - expect(subject.int_day_types).to eq(0) - end it "should have date all common days" do expect(subject.dates.size).to eq(3) expect(subject.dates[0].date).to eq(Date.new(2014,7,16)) @@ -1038,7 +1070,7 @@ end it "should have 0 period" do expect(subject.periods.size).to eq(0) end - it "should have no day_types" do + it "should not modify day_types" do expect(subject.int_day_types).to eq(0) end it "should have date reduced for period" do @@ -1063,8 +1095,8 @@ end it "should have 0 result periods" do expect(subject.periods.size).to eq(0) end - it "should have no day_types" do - expect(subject.int_day_types).to eq(0) + it "should not modify day_types" do + expect(subject.int_day_types).to eq(4|8|16) end it "should have 1 date " do expect(subject.dates.size).to eq(1) @@ -1093,9 +1125,6 @@ end it "should have 0 periods" do expect(subject.periods.size).to eq(0) end - it "should have 0 day_types" do - expect(subject.int_day_types).to eq(0) - end it "should have only dates " do expect(subject.dates.size).to eq(11) expect(subject.dates[0].date).to eq(Date.new(2014,6,30)) @@ -1134,7 +1163,7 @@ end it "should have 0 period" do expect(subject.periods.size).to eq(0) end - it "should have no remained day_types" do + it "should not modify day_types" do expect(subject.int_day_types).to eq(0) end it "should have date reduced for period" do @@ -1163,9 +1192,6 @@ end it "should have 0 result periods" do expect(subject.periods.size).to eq(0) end - it "should have no remained day_types" do - expect(subject.int_day_types).to eq(0) - end it "should have dates for period reduced" do expect(subject.dates.size).to eq(4) expect(subject.dates[0].date).to eq(Date.new(2014,7,3)) @@ -1197,9 +1223,6 @@ end it "should have 0 result periods" do expect(subject.periods.size).to eq(0) end - it "should have no remained day_types" do - subject.int_day_types == 0 - end it "should have 3 dates left" do expect(subject.dates.size).to eq(3) expect(subject.dates[0].date).to eq(Date.new(2014,7,16)) @@ -1233,10 +1256,6 @@ end expect(subject.periods.size).to eq(0) end - it "should have no remained day_types" do - subject.int_day_types == 0 - end - it "should have 0 dates left" do expect(subject.dates.size).to eq(0) end @@ -1261,10 +1280,6 @@ end expect(subject.periods.size).to eq(0) end - it "should have 0 day_types" do - expect(subject.int_day_types).to eq(0) - end - it "should have 6 dates " do expect(subject.dates.size).to eq(6) expect(subject.dates[0].date).to eq(Date.new(2014,8,11)) @@ -1303,8 +1318,8 @@ end end end - it "should have 0 day_types" do - expect(subject.int_day_types).to eq(0) + it "should not modify day_types" do + expect(subject.int_day_types).to eq(4|8|16) end it "should have 1 dates " do @@ -1342,8 +1357,8 @@ end end end - it "should have 0 day_types" do - expect(subject.int_day_types).to eq(0) + it "should not modify day_types" do + expect(subject.int_day_types).to eq(4|8|16) end it "should have only 1 dates " do @@ -1373,9 +1388,6 @@ end it "should have same 0 result periods" do expect(subject.periods.size).to eq(0) end - it "should have 0 day_types" do - expect(subject.int_day_types).to eq(0) - end it "should have 0 dates " do expect(subject.dates.size).to eq(0) end diff --git a/spec/models/chouette/vehicle_journey_at_stops_day_offset_spec.rb b/spec/models/chouette/vehicle_journey_at_stops_day_offset_spec.rb new file mode 100644 index 000000000..69a2d5cb9 --- /dev/null +++ b/spec/models/chouette/vehicle_journey_at_stops_day_offset_spec.rb @@ -0,0 +1,90 @@ +require 'spec_helper' + +describe Chouette::VehicleJourneyAtStop do + describe "#calculate" do + it "increments day offset when departure & arrival are on different sides + of midnight" do + at_stops = [] + [ + ['22:30', '22:35'], + ['23:50', '00:05'], + ['00:30', '00:35'], + ].each do |arrival_time, departure_time| + at_stops << build_stubbed( + :vehicle_journey_at_stop, + arrival_time: arrival_time, + departure_time: departure_time + ) + end + + offsetter = Chouette::VehicleJourneyAtStopsDayOffset.new(at_stops) + + offsetter.calculate! + + expect(at_stops[0].arrival_day_offset).to eq(0) + expect(at_stops[0].departure_day_offset).to eq(0) + + expect(at_stops[1].arrival_day_offset).to eq(0) + expect(at_stops[1].departure_day_offset).to eq(1) + + expect(at_stops[2].arrival_day_offset).to eq(1) + expect(at_stops[2].departure_day_offset).to eq(1) + end + + it "increments day offset when an at_stop passes midnight the next day" do + at_stops = [] + [ + ['22:30', '22:35'], + ['01:02', '01:14'], + ].each do |arrival_time, departure_time| + at_stops << build_stubbed( + :vehicle_journey_at_stop, + arrival_time: arrival_time, + departure_time: departure_time + ) + end + + offsetter = Chouette::VehicleJourneyAtStopsDayOffset.new(at_stops) + + offsetter.calculate! + + expect(at_stops[0].arrival_day_offset).to eq(0) + expect(at_stops[0].departure_day_offset).to eq(0) + + expect(at_stops[1].arrival_day_offset).to eq(1) + expect(at_stops[1].departure_day_offset).to eq(1) + end + + it "increments day offset for multi-day offsets" do + at_stops = [] + [ + ['22:30', '22:35'], + ['01:02', '01:14'], + ['04:30', '04:35'], + ['00:00', '00:04'], + ].each do |arrival_time, departure_time| + at_stops << build_stubbed( + :vehicle_journey_at_stop, + arrival_time: arrival_time, + departure_time: departure_time + ) + end + + offsetter = Chouette::VehicleJourneyAtStopsDayOffset.new(at_stops) + + offsetter.calculate! + + expect(at_stops[0].arrival_day_offset).to eq(0) + expect(at_stops[0].departure_day_offset).to eq(0) + + expect(at_stops[1].arrival_day_offset).to eq(1) + expect(at_stops[1].departure_day_offset).to eq(1) + + expect(at_stops[2].arrival_day_offset).to eq(1) + expect(at_stops[2].departure_day_offset).to eq(1) + + expect(at_stops[3].arrival_day_offset).to eq(2) + expect(at_stops[3].departure_day_offset).to eq(2) + end + end +end diff --git a/spec/models/chouette/vehicle_journey_spec.rb b/spec/models/chouette/vehicle_journey_spec.rb index 4a108d7c0..8f9080b99 100644 --- a/spec/models/chouette/vehicle_journey_spec.rb +++ b/spec/models/chouette/vehicle_journey_spec.rb @@ -1,7 +1,38 @@ require 'spec_helper' + describe Chouette::VehicleJourney, :type => :model do - describe "state_update" do + describe "vjas_departure_time_must_be_before_next_stop_arrival_time" do + let(:vehicle_journey) { create :vehicle_journey } + let(:vjas) { vehicle_journey.vehicle_journey_at_stops } + + it 'should add errors a stop departure_time is greater then next stop arrival time' do + vjas[0][:departure_time] = vjas[1][:arrival_time] + 1.minute + vehicle_journey.validate + + expect(vjas[0].errors[:departure_time]).not_to be_blank + expect(vehicle_journey.errors[:vehicle_journey_at_stops].count).to eq(1) + expect(vehicle_journey).not_to be_valid + end + + it 'should consider valid to have departure_time equal to next stop arrival time' do + vjas[0][:departure_time] = vjas[1][:arrival_time] + vehicle_journey.validate + + expect(vjas[0].errors[:departure_time]).to be_blank + expect(vehicle_journey.errors[:vehicle_journey_at_stops]).to be_empty + expect(vehicle_journey).to be_valid + end + + it 'should not add errors when departure_time is less then next stop arrival time' do + vehicle_journey.validate + vjas.each do |stop| + expect(stop.errors).to be_empty + end + expect(vehicle_journey).to be_valid + end + end + describe "state_update" do def vehicle_journey_at_stop_to_state vjas at_stop = {'stop_area_object_id' => vjas.stop_point.stop_area.objectid } [:id, :connecting_service_id, :boarding_alighting_possibility].map do |att| @@ -136,16 +167,12 @@ describe Chouette::VehicleJourney, :type => :model do it 'should return errors when validation failed' do state['published_journey_name'] = 'edited_name' - # Exceeds_gap departure time validation failed - prev = state['vehicle_journey_at_stops'].last(2).first - last = state['vehicle_journey_at_stops'].last - prev['departure_time']['hour'] = '01' - last['departure_time']['hour'] = '23' + state['vehicle_journey_at_stops'].last['departure_time']['hour'] = '23' expect { Chouette::VehicleJourney.state_update(route, collection) }.not_to change(vehicle_journey, :published_journey_name) - expect(state['errors'][:vehicle_journey_at_stops].size).to eq 1 + expect(state['vehicle_journey_at_stops'].last['errors']).not_to be_empty end it 'should delete vj with deletable set to true from state' do @@ -190,8 +217,8 @@ describe Chouette::VehicleJourney, :type => :model do end end - describe '.vehicle_journey_at_stops_matrix' do - it 'should fill missing VehicleJourneyAtStop with dummy' do + describe '#vehicle_journey_at_stops_matrix' do + it 'should fill missing vjas with dummy vjas' do vehicle_journey.journey_pattern.stop_points.delete_all vehicle_journey.vehicle_journey_at_stops.delete_all @@ -201,14 +228,28 @@ describe Chouette::VehicleJourney, :type => :model do expect(at_stops.count).to eq route.stop_points.count end - it 'should fill VehicleJourneyAtStop with new vjas when vj has been save without departure time' do + it 'should set dummy to false for active stop_points vjas' do + # Destroy vjas but stop_points is still active + # it should fill a new vjas without dummy flag + vehicle_journey.vehicle_journey_at_stops[3].destroy + at_stops = vehicle_journey.reload.vehicle_journey_at_stops_matrix + expect(at_stops[3].dummy).to be false + end + + it 'should set dummy to true for deactivated stop_points vjas' do + vehicle_journey.journey_pattern.stop_points.delete(vehicle_journey.journey_pattern.stop_points.first) + at_stops = vehicle_journey.reload.vehicle_journey_at_stops_matrix + expect(at_stops.first.dummy).to be true + end + + it 'should fill vjas for active stop_points without vjas yet' do vehicle_journey.vehicle_journey_at_stops.destroy_all at_stops = vehicle_journey.reload.vehicle_journey_at_stops_matrix expect(at_stops.map(&:stop_point_id)).to eq vehicle_journey.journey_pattern.stop_points.map(&:id) end - it 'should keep index order of VehicleJourneyAtStop' do + it 'should keep index order of vjas' do vehicle_journey.vehicle_journey_at_stops[3].destroy at_stops = vehicle_journey.reload.vehicle_journey_at_stops_matrix diff --git a/spec/models/clean_up_spec.rb b/spec/models/clean_up_spec.rb index c495abdfe..4b1bf4da9 100644 --- a/spec/models/clean_up_spec.rb +++ b/spec/models/clean_up_spec.rb @@ -1,20 +1,227 @@ require 'rails_helper' RSpec.describe CleanUp, :type => :model do - let(:cleaner) { CleanUp.new } it { should validate_presence_of(:begin_date) } + it { should validate_presence_of(:date_type) } it { should belong_to(:referential) } - it 'should delete vehiclejourneys without timetables' do - create_list(:vehicle_journey, 2) - create_list(:vehicle_journey, 2, time_tables:[create(:time_table)]) - expect(cleaner.clean_vehicle_journeys).to eq 2 + context '#exclude_dates_in_overlapping_period with :before date_type' do + let(:time_table) { create(:time_table) } + let(:period) { time_table.periods[0] } + let(:cleaner) { create(:clean_up, date_type: :before) } + + it 'should add exclude date into period for overlapping period' do + days_in_period = (period.period_start..period.period_end).count + cleaner.begin_date = period.period_end + + expect { cleaner.exclude_dates_in_overlapping_period(period) }.to change { + time_table.dates.where(in_out: false).count + }.by(days_in_period - 1) + end + + it 'should not add exclude date if no overlapping found' do + cleaner.begin_date = period.period_start + expect { cleaner.exclude_dates_in_overlapping_period(period) }.to_not change { + time_table.dates.where(in_out: false).count + } + end + end + + context '#exclude_dates_in_overlapping_period with :after date_type' do + let(:time_table) { create(:time_table) } + let(:period) { time_table.periods[0] } + let(:cleaner) { create(:clean_up, date_type: :after) } + + it 'should add exclude date into period for overlapping period' do + days_in_period = (period.period_start..period.period_end).count + cleaner.begin_date = period.period_start + 1.day + expect { cleaner.exclude_dates_in_overlapping_period(period) }.to change { + time_table.dates.where(in_out: false).count + }.by(days_in_period - 2) + end + + it 'should not add exclude date if no overlapping found' do + cleaner.begin_date = period.period_end + expect { cleaner.exclude_dates_in_overlapping_period(period) }.to_not change { + time_table.dates.where(in_out: false).count + } + end + end + + context '#exclude_dates_in_overlapping_period with :between date_type' do + let(:time_table) { create(:time_table) } + let(:period) { time_table.periods[0] } + let(:cleaner) { create(:clean_up, date_type: :between, begin_date: period.period_start + 3.day, end_date: period.period_end) } + + it 'should add exclude date into period for overlapping period' do + expected_day_out = (cleaner.begin_date..cleaner.end_date).count + expect { cleaner.exclude_dates_in_overlapping_period(period) }.to change { + time_table.dates.where(in_out: false).count + }.by(expected_day_out) + end + + it 'should not add exclude date if no overlapping found' do + cleaner.begin_date = period.period_end + 1.day + cleaner.end_date = cleaner.begin_date + 1.day + + expect { cleaner.exclude_dates_in_overlapping_period(period) }.to_not change { + time_table.dates.where(in_out: false).count + } + end + end + + context '#overlapping_periods' do + let(:cleaner) { create(:clean_up, date_type: :before, end_date: nil) } + let(:time_table) { create(:time_table) } + + it 'should detect overlapping periods' do + cleaner.begin_date = time_table.periods[0].period_start + expect(cleaner.overlapping_periods).to include(time_table.periods[0]) + end + + it 'should not return none overlapping periods' do + cleaner.begin_date = time_table.periods[0].period_start - 1.day + expect(cleaner.overlapping_periods).to_not include(time_table.periods[0]) + end + end + + context '#clean' do + let(:cleaner) { create(:clean_up, date_type: :before) } + + it 'should call destroy_time_tables_before' do + cleaner.date_type = :before + expect(cleaner).to receive(:destroy_time_tables_before) + expect(cleaner).to receive(:destroy_time_tables_dates_before) + expect(cleaner).to receive(:destroy_time_tables_periods_before) + cleaner.clean + end + + it 'should call destroy_time_tables_after' do + cleaner.date_type = :after + expect(cleaner).to receive(:destroy_time_tables_after) + expect(cleaner).to receive(:destroy_time_tables_dates_after) + expect(cleaner).to receive(:destroy_time_tables_periods_after) + cleaner.clean + end + + it 'should call destroy_time_tables_between' do + cleaner.date_type = :between + expect(cleaner).to receive(:destroy_time_tables_between) + expect(cleaner).to receive(:destroy_time_tables_dates_between) + expect(cleaner).to receive(:destroy_time_tables_periods_between) + cleaner.clean + end + end + + context '#destroy_time_tables_dates_between' do + let!(:time_table) { create(:time_table) } + let(:cleaner) { create(:clean_up, date_type: :between) } + + before do + time_table.periods.clear + time_table.save + cleaner.begin_date = time_table.start_date + cleaner.end_date = time_table.end_date + end + + it 'should destroy record' do + expect{ cleaner.destroy_time_tables_dates_between }.to change { + Chouette::TimeTableDate.count + }.by(-time_table.dates.count) + end + + it 'should not destroy record not in range' do + cleaner.begin_date = time_table.end_date + 1.day + cleaner.end_date = cleaner.begin_date + 1.day + + expect{ cleaner.destroy_time_tables_dates_between }.to_not change { + Chouette::TimeTableDate.count + } + end + end + + context '#destroy_time_tables_dates_after' do + let!(:time_table_date) { create(:time_table_date, date: Date.yesterday, in_out: true) } + let(:cleaner) { create(:clean_up, date_type: :after, begin_date: time_table_date.date) } + + it 'should destroy record' do + count = Chouette::TimeTableDate.where('date > ?', cleaner.begin_date).count + expect{ cleaner.destroy_time_tables_dates_after }.to change { + Chouette::TimeTableDate.count + }.by(-count) + end + end + + context '#destroy_time_tables_between' do + let!(:time_table) { create(:time_table ) } + let(:cleaner) { create(:clean_up, date_type: :after, begin_date: time_table.start_date, end_date: time_table.end_date) } + + it 'should destroy time_tables with validity period in purge range' do + expect{ cleaner.destroy_time_tables_between }.to change { + Chouette::TimeTable.count + }.by(-1) + end + + it 'should not destroy time_tables if not totaly inside purge range' do + cleaner.begin_date = time_table.start_date + 1.day + expect{ cleaner.destroy_time_tables_between }.to_not change { + Chouette::TimeTable.count + } + end + end + + context '#destroy_time_tables_after' do + let!(:time_table) { create(:time_table ) } + let(:cleaner) { create(:clean_up, date_type: :after, begin_date: time_table.start_date - 1.day) } + + it 'should destroy time_tables with start_date > purge begin_date' do + expect{ cleaner.destroy_time_tables_after }.to change { + Chouette::TimeTable.count + }.by(-1) + end + + it 'should not destroy time_tables with start_date < purge begin date' do + cleaner.begin_date = time_table.end_date + expect{ cleaner.destroy_time_tables_after }.to_not change { + Chouette::TimeTable.count + } + end end - it 'should delete journeypatterns without vehicle journeys' do - create_list(:journey_pattern, 2) - create_list(:vehicle_journey, 2, journey_pattern: create(:journey_pattern)) - expect(cleaner.clean_journey_patterns).to eq 2 + context '#destroy_time_tables' do + let!(:time_table) { create(:time_table) } + let(:cleaner) { create(:clean_up, date_type: :before) } + + it 'should destroy all time_tables' do + expect{cleaner.destroy_time_tables(Chouette::TimeTable.all)}.to change { + Chouette::TimeTable.count + }.by(-1) + end + + it 'should destroy associated vehicle_journeys' do + create(:vehicle_journey, time_tables: [time_table]) + expect{cleaner.destroy_time_tables(Chouette::TimeTable.all)}.to change { + Chouette::VehicleJourney.count + }.by(-1) + end + end + + context '#destroy_time_tables_before' do + let!(:time_table) { create(:time_table ) } + let(:cleaner) { create(:clean_up, date_type: :before, begin_date: time_table.end_date + 1.day) } + + it 'should destroy time_tables with end_date < purge begin_date' do + expect{ cleaner.destroy_time_tables_before }.to change { + Chouette::TimeTable.count + }.by(-1) + end + + it 'should not destroy time_tables with end_date > purge begin date' do + cleaner.begin_date = Date.today + expect{ cleaner.destroy_time_tables_before }.to_not change { + Chouette::TimeTable.count + } + end end end diff --git a/spec/models/referential_cloning_spec.rb b/spec/models/referential_cloning_spec.rb index 30391b53e..5acd433ec 100644 --- a/spec/models/referential_cloning_spec.rb +++ b/spec/models/referential_cloning_spec.rb @@ -1,4 +1,4 @@ -require 'rails_helper' +require 'spec_helper' RSpec.describe ReferentialCloning, :type => :model do it 'should have a valid factory' do @@ -7,4 +7,12 @@ RSpec.describe ReferentialCloning, :type => :model do it { should belong_to :source_referential } it { should belong_to :target_referential } + + describe "ReferentialCloningWorker" do + let(:referential_cloning) { FactoryGirl.create(:referential_cloning) } + + it "should schedule a job in worker" do + expect{referential_cloning.run_callbacks(:commit)}.to change {ReferentialCloningWorker.jobs.count}.by(1) + end + end end diff --git a/spec/models/time_table_combination_spec.rb b/spec/models/time_table_combination_spec.rb index 0a8b3296a..3e60fa444 100644 --- a/spec/models/time_table_combination_spec.rb +++ b/spec/models/time_table_combination_spec.rb @@ -87,7 +87,7 @@ describe TimeTableCombination, :type => :model do end it "should intersect combined to source" do - expect(source.int_day_types).to eq(0) + expect(source.int_day_types).to eq(508) expect(source.periods.size).to eq(1) expect(source.dates.size).to eq(0) @@ -119,7 +119,7 @@ describe TimeTableCombination, :type => :model do end it "should disjoin combined to source" do - expect(source.int_day_types).to eq(0) + expect(source.int_day_types).to eq(508) expect(source.periods.size).to eq(1) expect(source.dates.size).to eq(0) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 5c7aa0b98..6f98e5ce7 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -118,7 +118,7 @@ describe User, :type => :model do context 'permissions' do it 'should give edit permissions to user if user has "edit offer" permission in portail' do User.portail_sync - expect(User.find_by(username: 'vlatka.pavisic').permissions).not_to be_empty + expect(User.find_by(username: 'vlatka.pavisic').permissions).to include_all(User.edit_offer_permissions) expect(User.find_by(username: 'pierre.vabre').permissions).to be_empty end end diff --git a/spec/policies/boiv_policy_spec.rb b/spec/policies/boiv_policy_spec.rb index bf09cdcd9..514534adc 100644 --- a/spec/policies/boiv_policy_spec.rb +++ b/spec/policies/boiv_policy_spec.rb @@ -11,5 +11,4 @@ RSpec.describe BoivPolicy, type: :policy do permissions :show? do it_behaves_like 'permitted policy and same organisation', 'boiv:read-offer' end - end diff --git a/spec/policies/login_policy_spec.rb b/spec/policies/login_policy_spec.rb new file mode 100644 index 000000000..132e57433 --- /dev/null +++ b/spec/policies/login_policy_spec.rb @@ -0,0 +1,15 @@ +RSpec.describe LoginPolicy, type: :policy do + permissions :boiv? do + it 'no permission starting with boiv:. → denies' do + expect( LoginPolicy.new(user_context.user) ).not_to be_boiv + end + + with_user_permission 'boiv:anything' do + it { expect( LoginPolicy.new(user_context.user) ).to be_boiv } + end + with_user_permission 'boiv:' do + it { expect( LoginPolicy.new(user_context.user) ).not_to be_boiv } + end + end + +end diff --git a/spec/support/bare_sql.rb b/spec/support/bare_sql.rb new file mode 100644 index 000000000..03a50ef77 --- /dev/null +++ b/spec/support/bare_sql.rb @@ -0,0 +1,58 @@ +module Support + module BareSQL + + def insert(schema, table, values) + execute "INSERT INTO #{schema}.#{table} (#{_keys(values)}) VALUES (#{_values values})" + end + + def execute(sql) + base_connection.execute(sql) + end + + def expect_same_content(table_name) + expected_content = get_content(source_schema, table_name) + actual_content = get_content(target_schema, table_name) + expect( actual_content ).to eq(expected_content) + end + + def expect_same_sequence_params(sequence_name) + expected_seq = Hash.without(get_sequences(source_schema, sequence_name).first, 'log_cnt') + actual_seq = Hash.without(get_sequences(target_schema, sequence_name).first, 'log_cnt') + expect( actual_seq ).to eq(expected_seq) + end + + def get_content(schema_name, table_name) + execute("SELECT * FROM #{schema_name}.#{table_name}").to_a + end + + private + + def base_connection + ActiveRecord::Base.connection + end + + def _keys(values) + values.keys.map(&:to_s).join(", ") + end + + def _values(values) + values + .values + .map(&method(:_format)) + .join(', ') + end + + def _format(val) + case val + when String + "'#{val}'" + when TrueClass + "'t'" + when FalseClass + "'f'" + else + val.to_s + end + end + end +end diff --git a/spec/support/custom_matchers.rb b/spec/support/custom_matchers.rb new file mode 100644 index 000000000..bdc3efaa0 --- /dev/null +++ b/spec/support/custom_matchers.rb @@ -0,0 +1,7 @@ +require 'rspec/expectations' + +RSpec::Matchers.define :include_all do |expected| + match do |actual| + ( expected - actual ).empty? + end +end diff --git a/spec/support/devise.rb b/spec/support/devise.rb index 14e316bea..d4a279a41 100644 --- a/spec/support/devise.rb +++ b/spec/support/devise.rb @@ -36,8 +36,8 @@ module DeviseRequestHelper end module DeviseControllerHelper - def login_user - before(:each) do + def setup_user + before do @request.env["devise.mapping"] = Devise.mappings[:user] organisation = Organisation.where(:code => "first").first_or_create(attributes_for(:organisation)) @user = create(:user, :organisation => organisation, @@ -47,6 +47,11 @@ module DeviseControllerHelper 'access_points.create', 'access_points.edit', 'access_points.destroy', 'access_links.create', 'access_links.edit', 'access_links.destroy', 'connection_links.create', 'connection_links.edit', 'connection_links.destroy', 'route_sections.create', 'route_sections.edit', 'route_sections.destroy', 'referentials.create', 'referentials.edit', 'referentials.destroy']) + end + end + def login_user() + setup_user + before do sign_in @user end end diff --git a/spec/support/hash.rb b/spec/support/hash.rb new file mode 100644 index 000000000..ec9a2f895 --- /dev/null +++ b/spec/support/hash.rb @@ -0,0 +1,6 @@ +class << Hash + def without(hash, *keys) + nk = hash.keys - keys + Hash[*nk.zip(hash.values_at(*nk)).flatten] + end +end diff --git a/spec/support/pg_catalog.rb b/spec/support/pg_catalog.rb index bb61adba5..ca02f2550 100644 --- a/spec/support/pg_catalog.rb +++ b/spec/support/pg_catalog.rb @@ -1,11 +1,12 @@ +require_relative 'bare_sql' module Support module PGCatalog - # TODO: Check what of the follwowing can be done with ActiveRecord. E.g. - # @connection.foreign_keys(table)... + include Support::BareSQL def get_columns(schema_name, table_name) - execute("SELECT * from information_schema.columns WHERE table_name = '#{table_name}' AND table_schema = '#{schema_name}'") + execute("SELECT column_name, column_default FROM information_schema.columns WHERE table_name = '#{table_name}' AND table_schema = '#{schema_name}'").to_a end + def get_foreign_keys(schema_oid, table_name) schema_oid = get_schema_oid(schema_oid) unless Integer === schema_oid return [] unless schema_oid @@ -20,11 +21,8 @@ module Support .first end - def get_sequences(schema_name, table_name) - sequences = execute <<-EOSQL - SELECT sequence_name FROM information_schema.sequences - WHERE sequence_schema = '#{schema_name}' AND sequence_name LIKE '#{table_name}%' - EOSQL + def get_sequences(schema_name, sequence_name) + sequences = execute(sequence_query(schema_name, sequence_name)) sequences.values.flatten.map do | sequence | execute "SELECT * from #{schema_name}.#{sequence}" end.flat_map(&:to_a) @@ -38,39 +36,20 @@ module Support private - def base_connection - ActiveRecord::Base.connection - end - - def execute(sql) - base_connection.execute(sql) - end def foreign_key_query(schema_oid, table_name) - key = [:foreign_key_query, schema_oid, table_name] - get_or_create_query(key){ <<-EOQ - SELECT ct.conname AS constraint_name, pg_get_constraintdef(ct.oid) AS constraint_def - FROM pg_constraint ct JOIN pg_class rn ON rn.oid = ct.conrelid - WHERE connamespace = #{schema_oid} AND rn.relname = '#{table_name}' AND rn.relkind = 'r' AND ct.contype = 'f' - EOQ - } - end - - def sequence_properties_query(schema_name, sequence_name) - key = [:sequence_properies_query, schema_name, sequence_name] - get_or_create_query(key){ <<-EOQ - Coming Soon - EOQ - } - - end - - def get_or_create_query(query_key, &query_value) - queries.fetch(query_key){ queries[query_key] = query_value.() } + <<-EOQ + SELECT ct.conname AS constraint_name, pg_get_constraintdef(ct.oid) AS constraint_def + FROM pg_constraint ct JOIN pg_class rn ON rn.oid = ct.conrelid + WHERE connamespace = #{schema_oid} AND rn.relname = '#{table_name}' AND rn.relkind = 'r' AND ct.contype = 'f' + EOQ end - def queries - @__queries__ ||= {} + def sequence_query(schema_name, sequence_name) + <<-EOQ + SELECT sequence_name FROM information_schema.sequences + WHERE sequence_schema = '#{schema_name}' AND sequence_name = '#{sequence_name}' + EOQ end def without_keys(*keys) @@ -82,3 +61,7 @@ module Support end end end + +RSpec.configure do | conf | + conf.include Support::PGCatalog, type: :pg_catalog +end diff --git a/spec/support/pundit/policies.rb b/spec/support/pundit/policies.rb index 637a2a528..e18309226 100644 --- a/spec/support/pundit/policies.rb +++ b/spec/support/pundit/policies.rb @@ -16,6 +16,7 @@ module Support for_user.permissions ||= [] for_user.permissions += permissions.flatten end + end module PoliciesMacros @@ -27,6 +28,12 @@ module Support let( :user ) { create :user } end end + def with_user_permission(permission, &blk) + it "with user permission #{permission.inspect}" do + add_permissions(permission, for_user: user) + blk.() + end + end end end end diff --git a/spec/views/referentials/new.html.erb_spec.rb b/spec/views/referentials/new.html.erb_spec.rb index 0673b4578..554e71d29 100644 --- a/spec/views/referentials/new.html.erb_spec.rb +++ b/spec/views/referentials/new.html.erb_spec.rb @@ -5,15 +5,9 @@ describe "referentials/new", :type => :view do before(:each) do assign(:referential, Referential.new) end - + it "should have a textfield for name" do render expect(rendered).to have_field("referential[name]") end - - it "should have a textfield for slug" do - render - expect(rendered).to have_field("referential[slug]") - end - end diff --git a/spec/workers/referential_cloning_worker_spec.rb b/spec/workers/referential_cloning_worker_spec.rb index 85d771742..52ed8913b 100644 --- a/spec/workers/referential_cloning_worker_spec.rb +++ b/spec/workers/referential_cloning_worker_spec.rb @@ -9,37 +9,29 @@ RSpec.describe ReferentialCloningWorker do let( :worker ){ described_class.new } + def make_referential(schema_name) + return OpenStruct.new( slug: schema_name ) + end let( :source_schema ){ "source_schema" } - let( :target_schema ){ "#{source_schema}_tmp" } - let( :referential_cloning ){ OpenStruct.new(source_referential: OpenStruct.new(slug: source_schema)) } + let( :target_schema ){ "target_schema" } + let( :referential_cloning ){ OpenStruct.new(source_referential: make_referential(source_schema), + target_referential: make_referential(target_schema)) } + let( :cloner ){ 'cloner' } + before do expect( ReferentialCloning ).to receive(:find).with(id).and_return(referential_cloning) - expect( StoredProcedures ) - .to receive(:invoke_stored_procedure) - .with(:clone_schema, source_schema, target_schema, true) - - expect( worker ).to receive(:execute_sql).with( "DROP SCHEMA #{source_schema} CASCADE;" ) + expect( AF83::SchemaCloner ).to receive(:new).with( source_schema, target_schema ).and_return(cloner) + expect( cloner ).to receive(:clone_schema) expect( referential_cloning ).to receive(:run!) end it "invokes the correct stored procedure, updates the database and the AASM" do - expect( worker ).to receive(:execute_sql).with( "ALTER SCHEMA #{target_schema} RENAME TO #{source_schema};" ) expect( referential_cloning ).to receive(:successful!) worker.perform(id) end - - it "handles failure correctly" do - expect( worker ) - .to receive(:execute_sql) - .with( "ALTER SCHEMA #{target_schema} RENAME TO #{source_schema};" ) - .and_raise(RuntimeError) - - expect( referential_cloning ).to receive(:failed!) - worker.perform(id) - end end - + end |
