diff options
210 files changed, 3546 insertions, 1021 deletions
@@ -163,6 +163,7 @@ group :test do gem 'simplecov', :require => false gem 'simplecov-rcov', :require => false gem 'htmlbeautifier' + gem 'timecop' end group :test, :development, :dev do diff --git a/Gemfile.lock b/Gemfile.lock index 2239cf853..d03a26d18 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -525,6 +525,7 @@ GEM thread (0.2.2) thread_safe (0.3.6) tilt (1.4.1) + timecop (0.9.1) transpec (3.3.0) activesupport (>= 3.0, < 6.0) astrolabe (~> 1.2) @@ -669,6 +670,7 @@ DEPENDENCIES squeel! teaspoon-jasmine therubyracer (~> 0.12) + timecop transpec uglifier (~> 2.7.2) webmock diff --git a/app/assets/javascripts/es6_browserified/journey_patterns/actions/index.js b/app/assets/javascripts/es6_browserified/journey_patterns/actions/index.js index 0ed961f44..3577df2b6 100644 --- a/app/assets/javascripts/es6_browserified/journey_patterns/actions/index.js +++ b/app/assets/javascripts/es6_browserified/journey_patterns/actions/index.js @@ -6,6 +6,12 @@ if (!window.Promise) { } const actions = { + enterEditMode: () => ({ + type: "ENTER_EDIT_MODE" + }), + exitEditMode: () => ({ + type: "EXIT_EDIT_MODE" + }), receiveJourneyPatterns : (json) => ({ type: "RECEIVE_JOURNEY_PATTERNS", json @@ -138,6 +144,7 @@ const actions = { dispatch(actions.updateTotalCount(window.currentItemsLength - json.length)) } window.currentItemsLength = json.length + dispatch(actions.exitEditMode()) dispatch(actions.receiveJourneyPatterns(json)) } } 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 b446e2b37..12871431a 100644 --- a/app/assets/javascripts/es6_browserified/journey_patterns/components/CreateModal.js +++ b/app/assets/javascripts/es6_browserified/journey_patterns/components/CreateModal.js @@ -17,7 +17,7 @@ class CreateModal extends Component { } render() { - if(this.props.status.isFetching == true || this.props.status.policy['journey_patterns.update'] == false) { + if(this.props.status.isFetching == true || this.props.status.policy['journey_patterns.create'] == false || this.props.editMode == false) { return false } if(this.props.status.fetchSuccess == true) { 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 286cfc454..377fd0612 100644 --- a/app/assets/javascripts/es6_browserified/journey_patterns/components/JourneyPattern.js +++ b/app/assets/javascripts/es6_browserified/journey_patterns/components/JourneyPattern.js @@ -34,7 +34,7 @@ class JourneyPattern extends Component{ type='checkbox' id={sp.id} checked={sp.checked} - disabled={(this.props.value.deletable || this.props.status.policy['journey_patterns.update'] == false) ? 'disabled' : ''} + disabled={(this.props.value.deletable || this.props.status.policy['journey_patterns.update'] == false || this.props.editMode == false) ? 'disabled' : ''} > </input> <span className='radio-label'></span> @@ -78,7 +78,7 @@ class JourneyPattern extends Component{ <span className='fa fa-cog'></span> </div> <ul className='dropdown-menu'> - <li className={(this.props.value.deletable || this.props.status.policy['journey_patterns.update'] == false) ? 'disabled' : ''}> + <li className={(this.props.status.policy['journey_patterns.update'] == false || this.props.editMode == false) ? 'disabled' : ''}> <button type='button' onClick={this.props.onOpenEditModal} @@ -91,10 +91,10 @@ class JourneyPattern extends Component{ <li className={this.props.value.object_id ? '' : 'disabled'}> {this.vehicleJourneyURL(this.props.value.object_id)} </li> - <li className={'delete-action' + ((this.props.status.policy['journey_patterns.update'] == false)? ' disabled' : '')}> + <li className={'delete-action' + ((this.props.status.policy['journey_patterns.destroy'] == false || this.props.editMode == false) ? ' disabled' : '')}> <button type='button' - disabled={(this.props.status.policy['journey_patterns.update'] == false)? 'disabled' : ''} + disabled={(this.props.status.policy['journey_patterns.destroy'] == false || this.props.editMode == false)? 'disabled' : ''} onClick={(e) => { e.preventDefault() this.props.onDeleteJourneyPattern(this.props.index)} diff --git a/app/assets/javascripts/es6_browserified/journey_patterns/components/JourneyPatterns.js b/app/assets/javascripts/es6_browserified/journey_patterns/components/JourneyPatterns.js index 23473ae52..6506b706c 100644 --- a/app/assets/javascripts/es6_browserified/journey_patterns/components/JourneyPatterns.js +++ b/app/assets/javascripts/es6_browserified/journey_patterns/components/JourneyPatterns.js @@ -96,7 +96,7 @@ class JourneyPatterns extends Component{ <div className="alert alert-danger mt-sm"> <strong>Erreur : </strong> {this.props.journeyPatterns.map((jp, index) => - jp.errors.map((err, i) => { + jp.errors && jp.errors.map((err, i) => { return ( <ul key={i}> <li>{err}</li> @@ -133,6 +133,7 @@ class JourneyPatterns extends Component{ onOpenEditModal= {() => this.props.onOpenEditModal(index, journeyPattern)} onDeleteJourneyPattern={() => this.props.onDeleteJourneyPattern(index)} status= {this.props.status} + editMode= {this.props.editMode} /> )} </div> diff --git a/app/assets/javascripts/es6_browserified/journey_patterns/components/SaveJourneyPattern.js b/app/assets/javascripts/es6_browserified/journey_patterns/components/SaveJourneyPattern.js index 871ba00e1..767dab088 100644 --- a/app/assets/javascripts/es6_browserified/journey_patterns/components/SaveJourneyPattern.js +++ b/app/assets/javascripts/es6_browserified/journey_patterns/components/SaveJourneyPattern.js @@ -21,10 +21,10 @@ class SaveJourneyPattern extends Component{ type='button' onClick={e => { e.preventDefault() - actions.submitJourneyPattern(this.props.dispatch, this.props.journeyPatterns) + this.props.editMode ? this.props.onSubmitJourneyPattern(this.props.dispatch, this.props.journeyPatterns) : this.props.onEnterEditMode() }} > - Valider + {this.props.editMode ? "Valider" : "Editer"} </button> </form> </div> diff --git a/app/assets/javascripts/es6_browserified/journey_patterns/containers/AddJourneyPattern.js b/app/assets/javascripts/es6_browserified/journey_patterns/containers/AddJourneyPattern.js index ee13819fd..7aa27754e 100644 --- a/app/assets/javascripts/es6_browserified/journey_patterns/containers/AddJourneyPattern.js +++ b/app/assets/javascripts/es6_browserified/journey_patterns/containers/AddJourneyPattern.js @@ -6,6 +6,7 @@ const mapStateToProps = (state) => { return { modal: state.modal, journeyPatterns: state.journeyPatterns, + editMode: state.editMode, status: state.status } } diff --git a/app/assets/javascripts/es6_browserified/journey_patterns/containers/JourneyPatternList.js b/app/assets/javascripts/es6_browserified/journey_patterns/containers/JourneyPatternList.js index bc2aaf95b..228df3ede 100644 --- a/app/assets/javascripts/es6_browserified/journey_patterns/containers/JourneyPatternList.js +++ b/app/assets/javascripts/es6_browserified/journey_patterns/containers/JourneyPatternList.js @@ -6,6 +6,7 @@ const mapStateToProps = (state) => { return { journeyPatterns: state.journeyPatterns, status: state.status, + editMode: state.editMode, stopPointsList: state.stopPointsList } } diff --git a/app/assets/javascripts/es6_browserified/journey_patterns/containers/SaveJourneyPattern.js b/app/assets/javascripts/es6_browserified/journey_patterns/containers/SaveJourneyPattern.js index 33442c5a0..434264fea 100644 --- a/app/assets/javascripts/es6_browserified/journey_patterns/containers/SaveJourneyPattern.js +++ b/app/assets/javascripts/es6_browserified/journey_patterns/containers/SaveJourneyPattern.js @@ -6,11 +6,23 @@ var SaveJourneyPatternComponent = require('../components/SaveJourneyPattern') const mapStateToProps = (state) => { return { journeyPatterns: state.journeyPatterns, + editMode: state.editMode, page: state.pagination.page, status: state.status } } -const SaveJourneyPattern = connect(mapStateToProps)(SaveJourneyPatternComponent) +const mapDispatchToProps = (dispatch) => { + return { + onEnterEditMode: () => { + dispatch(actions.enterEditMode()) + }, + onSubmitJourneyPattern: (next, state) => { + actions.submitJourneyPattern(dispatch, state, next) + } + } +} + +const SaveJourneyPattern = connect(mapStateToProps, mapDispatchToProps)(SaveJourneyPatternComponent) module.exports = SaveJourneyPattern diff --git a/app/assets/javascripts/es6_browserified/journey_patterns/index.js b/app/assets/javascripts/es6_browserified/journey_patterns/index.js index b06957e0f..ca9efd2d0 100644 --- a/app/assets/javascripts/es6_browserified/journey_patterns/index.js +++ b/app/assets/javascripts/es6_browserified/journey_patterns/index.js @@ -12,6 +12,7 @@ var App = require('./components/App') // var promise = require('redux-promise') var initialState = { + editMode: false, status: { policy: window.perms, fetchSuccess: true, @@ -35,7 +36,7 @@ var initialState = { let store = createStore( journeyPatternsApp, - initialState + initialState, // applyMiddleware(thunkMiddleware, promise, loggerMiddleware) ) diff --git a/app/assets/javascripts/es6_browserified/journey_patterns/reducers/editMode.js b/app/assets/javascripts/es6_browserified/journey_patterns/reducers/editMode.js new file mode 100644 index 000000000..2e8af1aa8 --- /dev/null +++ b/app/assets/javascripts/es6_browserified/journey_patterns/reducers/editMode.js @@ -0,0 +1,12 @@ +const editMode = (state = {}, action ) => { + switch (action.type) { + case "ENTER_EDIT_MODE": + return true + case "EXIT_EDIT_MODE": + return false + default: + return state + } +} + +module.exports = editMode diff --git a/app/assets/javascripts/es6_browserified/journey_patterns/reducers/index.js b/app/assets/javascripts/es6_browserified/journey_patterns/reducers/index.js index aa35adf0e..a9c28b83e 100644 --- a/app/assets/javascripts/es6_browserified/journey_patterns/reducers/index.js +++ b/app/assets/javascripts/es6_browserified/journey_patterns/reducers/index.js @@ -1,4 +1,5 @@ var combineReducers = require('redux').combineReducers +var editMode = require('./editMode') var status = require('./status') var journeyPatterns = require('./journeyPatterns') var pagination = require('./pagination') @@ -6,6 +7,7 @@ var modal = require('./modal') var stopPointsList = require('./stopPointsList') const journeyPatternsApp = combineReducers({ + editMode, status, journeyPatterns, pagination, diff --git a/app/assets/javascripts/es6_browserified/journey_patterns/reducers/status.js b/app/assets/javascripts/es6_browserified/journey_patterns/reducers/status.js index d7ef12d0b..07bbdc249 100644 --- a/app/assets/javascripts/es6_browserified/journey_patterns/reducers/status.js +++ b/app/assets/javascripts/es6_browserified/journey_patterns/reducers/status.js @@ -11,6 +11,10 @@ const status = (state = {}, action) => { return _.assign({}, state, {fetchSuccess: true, isFetching: false}) case 'RECEIVE_ERRORS': return _.assign({}, state, {isFetching: false}) + case 'ENTER_EDIT_MODE': + return _.assign({}, state, {editMode: true}) + case 'EXIT_EDIT_MODE': + return _.assign({}, state, {editMode: false}) default: return state } 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 951664129..61667f5ab 100644 --- a/app/assets/javascripts/es6_browserified/time_tables/actions/index.js +++ b/app/assets/javascripts/es6_browserified/time_tables/actions/index.js @@ -96,34 +96,41 @@ const actions = { closePeriodForm: () => ({ type: 'CLOSE_PERIOD_FORM' }), + resetModalErrors: () => ({ + type: 'RESET_MODAL_ERRORS' + }), updatePeriodForm: (val, group, selectType) => ({ type: 'UPDATE_PERIOD_FORM', val, group, selectType }), - validatePeriodForm: (modalProps, timeTablePeriods, metas) => ({ + validatePeriodForm: (modalProps, timeTablePeriods, metas, timetableInDates) => ({ type: 'VALIDATE_PERIOD_FORM', modalProps, timeTablePeriods, - metas + metas, + timetableInDates }), - includeDateInPeriod: (index, dayTypes) => ({ + includeDateInPeriod: (index, dayTypes, date) => ({ type: 'INCLUDE_DATE_IN_PERIOD', index, - dayTypes + dayTypes, + date }), - excludeDateFromPeriod: (index, dayTypes) => ({ + excludeDateFromPeriod: (index, dayTypes, date) => ({ type: 'EXCLUDE_DATE_FROM_PERIOD', index, - dayTypes + dayTypes, + date }), openConfirmModal : (callback) => ({ type : 'OPEN_CONFIRM_MODAL', callback }), - showErrorModal: () => ({ - type: 'OPEN_ERROR_MODAL' + showErrorModal: (error) => ({ + type: 'OPEN_ERROR_MODAL', + error }), closeModal : () => ({ type : 'CLOSE_MODAL' @@ -161,16 +168,15 @@ const actions = { // We compare periods & currentDate, to determine if it is included or not let testDate = false periods.map((p, i) => { - if(p.deleted){ - return false - } + if (p.deleted) return false + let begin = new Date(p.period_start) let end = new Date(p.period_end) if(testDate === false){ if(currentDate >= begin && currentDate <= end) { testDate = true - p.include_date = false + // p.include_date = false } } }) @@ -187,11 +193,11 @@ const actions = { }) return improvedCM }, - checkConfirmModal: (event, callback, stateChanged, dispatch, metas, timetable) => { - if(stateChanged === true){ - if(timetable.time_table_periods.length == 0 && _.some(metas.day_types)){ - return actions.showErrorModal() + if(stateChanged){ + const error = actions.errorModalKey(timetable.time_table_periods, metas.day_types) + if(error){ + return actions.showErrorModal(error) }else{ return actions.openConfirmModal(callback) } @@ -203,7 +209,7 @@ const actions = { formatDate: (props) => { return props.year + '-' + props.month + '-' + props.day }, - checkErrorsInPeriods: (start, end, index, periods) => { + checkErrorsInPeriods: (start, end, index, periods, days) => { let error = '' start = new Date(start) end = new Date(end) @@ -215,6 +221,18 @@ const actions = { }) return error }, + checkErrorsInDates: (start, end, in_days) => { + let error = '' + start = new Date(start) + end = new Date(end) + + _.each(in_days, ({date}) => { + if (start <= new Date(date) && end >= new Date(date)) { + error = 'Une période ne peut chevaucher une date dans un calendrier' + } + }) + return error + }, fetchTimeTables: (dispatch, nextPage) => { let urlJSON = window.location.pathname.split('/', 5).join('/') // console.log(nextPage) @@ -275,6 +293,31 @@ const actions = { } } }) + }, + errorModalKey: (periods, dayTypes) => { + const withoutPeriodsWithDaysTypes = _.reject(periods, 'deleted').length == 0 && _.some(dayTypes) && "withoutPeriodsWithDaysTypes" + const withPeriodsWithoutDayTypes = _.reject(periods, 'deleted').length > 0 && _.every(dayTypes, dt => dt == false) && "withPeriodsWithoutDayTypes" + + return (withoutPeriodsWithDaysTypes || withPeriodsWithoutDayTypes) && (withoutPeriodsWithDaysTypes ? "withoutPeriodsWithDaysTypes" : "withPeriodsWithoutDayTypes") + + }, + errorModalMessage: (errorKey) => { + switch (errorKey) { + case "withoutPeriodsWithDaysTypes": + return window.I18n.fr.time_tables.edit.error_modal.withoutPeriodsWithDaysTypes + case "withPeriodsWithoutDayTypes": + return window.I18n.fr.time_tables.edit.error_modal.withPeriodsWithoutDayTypes + default: + return errorKey + + } + }, + checkIfTTHasDate: (dates, date) => { + if (_.some(dates, date)) { + return _.reject(dates, ['date', date.date]) + } else { + return dates.concat(date) + } } } diff --git a/app/assets/javascripts/es6_browserified/time_tables/components/ErrorModal.js b/app/assets/javascripts/es6_browserified/time_tables/components/ErrorModal.js index 31ed256ea..4e8f7e363 100644 --- a/app/assets/javascripts/es6_browserified/time_tables/components/ErrorModal.js +++ b/app/assets/javascripts/es6_browserified/time_tables/components/ErrorModal.js @@ -1,18 +1,19 @@ var React = require('react') var Component = require('react').Component var PropTypes = require('react').PropTypes +var errorModalMessage = require('../actions').errorModalMessage -const ErrorModal = ({dispatch, modal, onModalClose}) => ( +const ErrorModal = ({dispatch, modal, I18n, 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> + <h4 className='modal-title'>{window.I18n.fr.time_tables.edit.error_modal.title}</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> + <p>{errorModalMessage(modal.modalProps.error)}</p> </div> </div> <div className='modal-footer'> diff --git a/app/assets/javascripts/es6_browserified/time_tables/components/ExceptionsInDay.js b/app/assets/javascripts/es6_browserified/time_tables/components/ExceptionsInDay.js index 10b558373..4879e537f 100644 --- a/app/assets/javascripts/es6_browserified/time_tables/components/ExceptionsInDay.js +++ b/app/assets/javascripts/es6_browserified/time_tables/components/ExceptionsInDay.js @@ -20,7 +20,7 @@ class ExceptionsInDay extends Component { data-actiontype='remove' onClick={(e) => { $(e.currentTarget).toggleClass('active') - this.props.onExcludeDateFromPeriod(this.props.index, this.props.metas.day_types) + this.props.onExcludeDateFromPeriod(this.props.index, this.props.metas.day_types, this.props.currentDate) }} > <span className='fa fa-times'></span> @@ -36,7 +36,7 @@ class ExceptionsInDay extends Component { data-actiontype='add' onClick={(e) => { $(e.currentTarget).toggleClass('active') - this.props.onIncludeDateInPeriod(this.props.index, this.props.metas.day_types) + this.props.onIncludeDateInPeriod(this.props.index, this.props.metas.day_types, this.props.currentDate) }} > <span className='fa fa-plus'></span> diff --git a/app/assets/javascripts/es6_browserified/time_tables/components/PeriodForm.js b/app/assets/javascripts/es6_browserified/time_tables/components/PeriodForm.js index 028974fc8..3234a3fd7 100644 --- a/app/assets/javascripts/es6_browserified/time_tables/components/PeriodForm.js +++ b/app/assets/javascripts/es6_browserified/time_tables/components/PeriodForm.js @@ -1,5 +1,6 @@ var React = require('react') var PropTypes = require('react').PropTypes +var _ = require('lodash') let monthsArray = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'] const formatNumber = (val) => { @@ -107,7 +108,7 @@ const PeriodForm = ({modal, timetable, metas, onOpenAddPeriodForm, onClosePeriod <button type='button' className='btn btn-outline-primary mr-sm' - onClick={() => onValidatePeriodForm(modal.modalProps, timetable.time_table_periods, metas)} + onClick={() => onValidatePeriodForm(modal.modalProps, timetable.time_table_periods, metas, _.filter(timetable.time_table_dates, ['in_out', true]))} > Valider </button> 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 e8c0aa3ba..779fd8e25 100644 --- a/app/assets/javascripts/es6_browserified/time_tables/components/SaveTimetable.js +++ b/app/assets/javascripts/es6_browserified/time_tables/components/SaveTimetable.js @@ -10,6 +10,8 @@ class SaveTimetable extends Component{ } render() { + const error = actions.errorModalKey(this.props.timetable.time_table_periods, this.props.metas.day_types) + return ( <div className='row mt-md'> <div className='col-lg-12 text-right'> @@ -19,9 +21,9 @@ class SaveTimetable extends Component{ type='button' onClick={e => { e.preventDefault() - if(this.props.timetable.time_table_periods.length == 0 && _.some(this.props.metas.day_types)){ - this.props.onShowErrorModal() - }else{ + if (error) { + this.props.onShowErrorModal(error) + } else { actions.submitTimetable(this.props.getDispatch(), this.props.timetable, this.props.metas) } }} diff --git a/app/assets/javascripts/es6_browserified/time_tables/components/Timetable.js b/app/assets/javascripts/es6_browserified/time_tables/components/Timetable.js index d562655b9..3af1a11a4 100644 --- a/app/assets/javascripts/es6_browserified/time_tables/components/Timetable.js +++ b/app/assets/javascripts/es6_browserified/time_tables/components/Timetable.js @@ -79,6 +79,7 @@ class Timetable extends Component{ <ExceptionsInDay index={i} value={this.props.timetable} + currentDate={d.date} metas={this.props.metas} blueDaytype={this.props.metas.day_types[d.wday]} onExcludeDateFromPeriod={this.props.onExcludeDateFromPeriod} diff --git a/app/assets/javascripts/es6_browserified/time_tables/containers/ErrorModal.js b/app/assets/javascripts/es6_browserified/time_tables/containers/ErrorModal.js index 16a7d45dd..e0b2c1240 100644 --- a/app/assets/javascripts/es6_browserified/time_tables/containers/ErrorModal.js +++ b/app/assets/javascripts/es6_browserified/time_tables/containers/ErrorModal.js @@ -12,6 +12,7 @@ const mapDispatchToProps = (dispatch) => { return { onModalClose: () =>{ dispatch(actions.closeModal()) + dispatch(actions.resetModalErrors()) } } } diff --git a/app/assets/javascripts/es6_browserified/time_tables/containers/PeriodForm.js b/app/assets/javascripts/es6_browserified/time_tables/containers/PeriodForm.js index 7f2db785a..723a4a7fb 100644 --- a/app/assets/javascripts/es6_browserified/time_tables/containers/PeriodForm.js +++ b/app/assets/javascripts/es6_browserified/time_tables/containers/PeriodForm.js @@ -7,7 +7,7 @@ const mapStateToProps = (state) => { return { modal: state.modal, timetable: state.timetable, - metas: state.metas + metas: state.metas, } } @@ -27,8 +27,8 @@ const mapDispatchToProps = (dispatch) => { val = (val < 10) ? '0' + String(val) : String(val) dispatch(actions.updatePeriodForm(val, group, 'day')) }, - onValidatePeriodForm: (modalProps, timeTablePeriods, metas) => { - dispatch(actions.validatePeriodForm(modalProps, timeTablePeriods, metas)) + onValidatePeriodForm: (modalProps, timeTablePeriods, metas, timetableInDates) => { + dispatch(actions.validatePeriodForm(modalProps, timeTablePeriods, metas, timetableInDates)) } } } 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 b5539e7d8..6287da15b 100644 --- a/app/assets/javascripts/es6_browserified/time_tables/containers/SaveTimetable.js +++ b/app/assets/javascripts/es6_browserified/time_tables/containers/SaveTimetable.js @@ -13,8 +13,8 @@ const mapStateToProps = (state) => { const mapDispatchToProps = (dispatch) => { return { - onShowErrorModal: () => { - dispatch(actions.showErrorModal()) + onShowErrorModal: (errorKey) => { + dispatch(actions.showErrorModal(errorKey)) }, getDispatch: () => { return dispatch diff --git a/app/assets/javascripts/es6_browserified/time_tables/containers/Timetable.js b/app/assets/javascripts/es6_browserified/time_tables/containers/Timetable.js index c6b5fcc6b..639a1e2ab 100644 --- a/app/assets/javascripts/es6_browserified/time_tables/containers/Timetable.js +++ b/app/assets/javascripts/es6_browserified/time_tables/containers/Timetable.js @@ -15,11 +15,11 @@ const mapDispatchToProps = (dispatch) => { onDeletePeriod: (index, dayTypes) =>{ dispatch(actions.deletePeriod(index, dayTypes)) }, - onExcludeDateFromPeriod: (index, dayTypes) => { - dispatch(actions.excludeDateFromPeriod(index, dayTypes)) + onExcludeDateFromPeriod: (index, dayTypes, date) => { + dispatch(actions.excludeDateFromPeriod(index, dayTypes, date)) }, - onIncludeDateInPeriod: (index, dayTypes) => { - dispatch(actions.includeDateInPeriod(index, dayTypes)) + onIncludeDateInPeriod: (index, dayTypes, date) => { + dispatch(actions.includeDateInPeriod(index, dayTypes, date)) }, onOpenEditPeriodForm: (period, index) => { dispatch(actions.openEditPeriodForm(period, index)) diff --git a/app/assets/javascripts/es6_browserified/time_tables/index.js b/app/assets/javascripts/es6_browserified/time_tables/index.js index 01f8c428e..a91747991 100644 --- a/app/assets/javascripts/es6_browserified/time_tables/index.js +++ b/app/assets/javascripts/es6_browserified/time_tables/index.js @@ -22,7 +22,8 @@ var initialState = { current_month: [], current_periode_range: '', periode_range: [], - time_table_periods: [] + time_table_periods: [], + time_table_dates: [] }, metas: { comment: '', @@ -61,7 +62,7 @@ var initialState = { let store = createStore( timeTablesApp, - initialState + initialState, // applyMiddleware(thunkMiddleware, promise, loggerMiddleware) ) 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 69f7b206e..3fe4e43a2 100644 --- a/app/assets/javascripts/es6_browserified/time_tables/reducers/modal.js +++ b/app/assets/javascripts/es6_browserified/time_tables/reducers/modal.js @@ -21,7 +21,11 @@ const modal = (state = {}, action) => { }) case 'OPEN_ERROR_MODAL': $('#ErrorModal').modal('show') - return _.assign({}, state, {type: 'error'}) + newModalProps = _.assign({}, state.modalProps, {error: action.error}) + return _.assign({}, state, {type: 'error'}, {modalProps: newModalProps}) + case 'RESET_MODAL_ERRORS': + newModalProps = _.assign({}, state.modalProps, {error: ''}) + return _.assign({}, state, {type: ''}, {modalProps: newModalProps}) case 'CLOSE_PERIOD_FORM': newModalProps = _.assign({}, state.modalProps, {active: false}) return _.assign({}, state, {modalProps: newModalProps}) @@ -60,7 +64,9 @@ const modal = (state = {}, action) => { } let newPeriods = JSON.parse(JSON.stringify(action.timeTablePeriods)) + let newDays = JSON.parse(JSON.stringify(action.timetableInDates)) let error = actions.checkErrorsInPeriods(period_start, period_end, action.modalProps.index, newPeriods) + if (error == '') error = actions.checkErrorsInDates(period_start, period_end, newDays) newModalProps.error = error newModalProps.active = (error == '') ? false : true 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 3d96fb7b7..45fec6b5f 100644 --- a/app/assets/javascripts/es6_browserified/time_tables/reducers/pagination.js +++ b/app/assets/javascripts/es6_browserified/time_tables/reducers/pagination.js @@ -26,6 +26,8 @@ const pagination = (state = {}, action) => { case 'VALIDATE_PERIOD_FORM': case 'UPDATE_COMMENT': case 'UPDATE_COLOR': + case 'UPDATE_DAY_TYPES': + case 'UPDATE_CURRENT_MONTH_FROM_DAYTYPES': toggleOnConfirmModal('modal') return _.assign({}, state, {stateChanged: true}) default: diff --git a/app/assets/javascripts/es6_browserified/time_tables/reducers/timetable.js b/app/assets/javascripts/es6_browserified/time_tables/reducers/timetable.js index 65cd9231a..64db1ccc1 100644 --- a/app/assets/javascripts/es6_browserified/time_tables/reducers/timetable.js +++ b/app/assets/javascripts/es6_browserified/time_tables/reducers/timetable.js @@ -1,6 +1,7 @@ const _ = require('lodash') var actions = require('../actions') let newState = {} +let newDates = [] const timetable = (state = {}, action) => { switch (action.type) { @@ -9,7 +10,8 @@ const timetable = (state = {}, action) => { current_month: action.json.current_month, current_periode_range: action.json.current_periode_range, periode_range: action.json.periode_range, - time_table_periods: action.json.time_table_periods + time_table_periods: action.json.time_table_periods, + time_table_dates: action.json.time_table_dates }) return _.assign({}, fetchedState, {current_month: actions.updateSynthesis(fetchedState, actions.strToArrayDayTypes(action.json.day_types))}) case 'RECEIVE_MONTH': @@ -38,22 +40,24 @@ const timetable = (state = {}, action) => { newState = _.assign({}, state, {time_table_periods : ttperiods}) return _.assign({}, newState, {current_month: actions.updateSynthesis(newState, action.dayTypes)}) case 'INCLUDE_DATE_IN_PERIOD': + newDates = actions.checkIfTTHasDate(state.time_table_dates, {date: action.date, in_out: true}) let newCMi = state.current_month.map((d, i) => { if(i == action.index){ d.include_date = !d.include_date } return d }) - newState = _.assign({}, state, {current_month: newCMi}) + newState = _.assign({}, state, {current_month: newCMi, time_table_dates: newDates}) return _.assign({}, newState, {current_month: actions.updateSynthesis(newState, action.dayTypes)}) case 'EXCLUDE_DATE_FROM_PERIOD': + newDates = actions.checkIfTTHasDate(state.time_table_dates, {date: action.date, in_out: false}) let newCMe = state.current_month.map((d, i) => { if(i == action.index){ d.excluded_date = !d.excluded_date } return d }) - newState = _.assign({}, state, {current_month: newCMe}) + newState = _.assign({}, state, {current_month: newCMe, time_table_dates: newDates}) return _.assign({}, newState, {current_month: actions.updateSynthesis(newState, action.dayTypes)}) case 'UPDATE_CURRENT_MONTH_FROM_DAYTYPES': return _.assign({}, state, {current_month: actions.updateSynthesis(state, action.dayTypes)}) @@ -64,7 +68,10 @@ const timetable = (state = {}, action) => { return state } let newPeriods = JSON.parse(JSON.stringify(action.timeTablePeriods)) + let newDays = JSON.parse(JSON.stringify(action.timetableInDates)) let error = actions.checkErrorsInPeriods(period_start, period_end, action.modalProps.index, newPeriods) + if (error == '') error = actions.checkErrorsInDates(period_start, period_end, newDays) + if(error != ''){ return state } diff --git a/app/assets/javascripts/es6_browserified/vehicle_journeys/actions/index.js b/app/assets/javascripts/es6_browserified/vehicle_journeys/actions/index.js index c30f460d8..2fde0d76f 100644 --- a/app/assets/javascripts/es6_browserified/vehicle_journeys/actions/index.js +++ b/app/assets/javascripts/es6_browserified/vehicle_journeys/actions/index.js @@ -7,6 +7,12 @@ if (!window.Promise) { var batchActions = require('../batch').batchActions const actions = { + enterEditMode: () => ({ + type: "ENTER_EDIT_MODE" + }), + exitEditMode: () => ({ + type: "EXIT_EDIT_MODE" + }), receiveVehicleJourneys : (json) => ({ type: "RECEIVE_VEHICLE_JOURNEYS", json @@ -121,6 +127,9 @@ const actions = { objectid: selectedCompany.objectid } }), + unselect2Company: () => ({ + type: 'UNSELECT_CP_EDIT_MODAL', + }), editVehicleJourney : (data, selectedCompany) => ({ type: 'EDIT_VEHICLEJOURNEY', data, @@ -130,13 +139,14 @@ const actions = { type: 'EDIT_VEHICLEJOURNEY_NOTES', footnotes }), - shiftVehicleJourney : (data) => ({ + shiftVehicleJourney : (addtionalTime) => ({ type: 'SHIFT_VEHICLEJOURNEY', - data + addtionalTime }), - duplicateVehicleJourney : (data, departureDelta) => ({ + duplicateVehicleJourney : (addtionalTime, duplicateNumber, departureDelta) => ({ type: 'DUPLICATE_VEHICLEJOURNEY', - data, + addtionalTime, + duplicateNumber, departureDelta }), deleteVehicleJourneys : () => ({ @@ -371,6 +381,7 @@ const actions = { dispatch(actions.updateTotalCount(window.currentItemsLength - json.length)) } window.currentItemsLength = json.length + dispatch(actions.exitEditMode()) dispatch(actions.receiveVehicleJourneys(json)) } } @@ -384,7 +395,7 @@ const actions = { }, simplePad: (d) => { if(d.toString().length == 1){ - return (d < 10) ? '0' + d.toString() : d.toString(); + return '0' + d.toString() }else{ return d.toString() } @@ -418,8 +429,8 @@ const actions = { return vjas }, getDuplicateDelta: (original, newDeparture) => { - if (original.departure_time.hour != '' && original.departure_time.minute != '' && newDeparture.departure_time.hour != '' && newDeparture.departure_time.minute != ''){ - return (parseInt(newDeparture.departure_time.hour) - parseInt(original.departure_time.hour)) * 60 + (parseInt(newDeparture.departure_time.minute) - parseInt(original.departure_time.minute)) + if (original.departure_time.hour != '' && original.departure_time.minute != '' && newDeparture.departure_time.hour != undefined && newDeparture.departure_time.minute != undefined){ + return (newDeparture.departure_time.hour - parseInt(original.departure_time.hour)) * 60 + (newDeparture.departure_time.minute - parseInt(original.departure_time.minute)) } return 0 }, @@ -431,69 +442,25 @@ const actions = { vjas.delta = delta return vjas }, - checkSchedules: (schedule) => { - let hours = 0 - let minutes = 0 - if (parseInt(schedule.departure_time.minute) > 59){ - hours = Math.floor(parseInt(schedule.departure_time.minute) / 60) - minutes = parseInt(schedule.departure_time.minute) % 60 - schedule.departure_time.minute = actions.simplePad(minutes, 'minute') - schedule.departure_time.hour = parseInt(schedule.departure_time.hour) + hours - } - if (parseInt(schedule.arrival_time.minute) > 59){ - hours = Math.floor(parseInt(schedule.arrival_time.minute) / 60) - minutes = parseInt(schedule.arrival_time.minute) % 60 - schedule.arrival_time.minute = actions.simplePad(minutes, 'minute') - schedule.arrival_time.hour = parseInt(schedule.arrival_time.hour) + hours - } - if (parseInt(schedule.departure_time.minute) < 0){ - hours = Math.floor(parseInt(schedule.departure_time.minute) / 60) - minutes = (parseInt(schedule.departure_time.minute) % 60) + 60 - if(minutes == 60){ - minutes = 0 - } - schedule.departure_time.minute = actions.simplePad(minutes, 'minute') - schedule.departure_time.hour = parseInt(schedule.departure_time.hour) + hours - } - if (parseInt(schedule.arrival_time.minute) < 0){ - hours = Math.floor(parseInt(schedule.arrival_time.minute) / 60) - minutes = (parseInt(schedule.arrival_time.minute) % 60) + 60 - if(minutes == 60){ - minutes = 0 - } - schedule.arrival_time.minute = actions.simplePad(minutes, 'minute') - schedule.arrival_time.hour = parseInt(schedule.arrival_time.hour) + hours - } + getShiftedSchedule: ({departure_time, arrival_time}, additional_time) => { + // We create dummy dates objects to manipulate time more easily + let departureDT = new Date (Date.UTC(2017, 2, 1, parseInt(departure_time.hour), parseInt(departure_time.minute))) + let arrivalDT = new Date (Date.UTC(2017, 2, 1, parseInt(arrival_time.hour), parseInt(arrival_time.minute))) - if(parseInt(schedule.departure_time.hour) > 23){ - schedule.departure_time.hour = '23' - schedule.departure_time.minute = '59' - } - if(parseInt(schedule.arrival_time.hour) > 23){ - schedule.arrival_time.hour = '23' - schedule.arrival_time.minute = '59' - } + let newDepartureDT = new Date (departureDT.getTime() + additional_time * 60000) + let newArrivalDT = new Date (arrivalDT.getTime() + additional_time * 60000) - if(parseInt(schedule.departure_time.hour) < 0){ - schedule.departure_time.hour = '00' - schedule.departure_time.minute = '00' - } - if(parseInt(schedule.arrival_time.hour) < 0){ - schedule.arrival_time.hour = '00' - schedule.arrival_time.minute = '00' + return { + departure_time: { + hour: actions.simplePad(newDepartureDT.getUTCHours()), + minute: actions.simplePad(newDepartureDT.getUTCMinutes()) + }, + arrival_time: { + hour: actions.simplePad(newArrivalDT.getUTCHours()), + minute: actions.simplePad(newArrivalDT.getUTCMinutes()) + } } - - schedule.departure_time.hour = actions.simplePad(parseInt(schedule.departure_time.hour), 'hour') - schedule.arrival_time.hour = actions.simplePad(parseInt(schedule.arrival_time.hour), 'hour') - // if (parseInt(schedule.departure_time.hour) > 23){ - // schedule.departure_time.hour = parseInt(schedule.departure_time.hour) - 24 - // } - // if (parseInt(schedule.arrival_time.hour) > 23){ - // schedule.arrival_time.hour = parseInt(schedule.arrival_time.hour) - 24 - // } - // schedule.departure_time.hour = actions.pad(schedule.departure_time.hour, 'hour') - // schedule.arrival_time.hour = actions.pad(schedule.arrival_time.hour, 'hour') - } + }, } module.exports = actions diff --git a/app/assets/javascripts/es6_browserified/vehicle_journeys/components/SaveVehicleJourneys.js b/app/assets/javascripts/es6_browserified/vehicle_journeys/components/SaveVehicleJourneys.js index 05dda976d..3c45e5758 100644 --- a/app/assets/javascripts/es6_browserified/vehicle_journeys/components/SaveVehicleJourneys.js +++ b/app/assets/javascripts/es6_browserified/vehicle_journeys/components/SaveVehicleJourneys.js @@ -9,7 +9,7 @@ class SaveVehicleJourneys extends Component{ } render() { - if(this.props.filters.policy['vehicle_journeys.update'] == false) { + if (this.props.filters.policy['vehicle_journeys.update'] == false) { return false }else{ return ( @@ -21,10 +21,10 @@ class SaveVehicleJourneys extends Component{ type='button' onClick={e => { e.preventDefault() - actions.submitVehicleJourneys(this.props.dispatch, this.props.vehicleJourneys) + this.props.editMode ? this.props.onSubmitVehicleJourneys(this.props.dispatch, this.props.vehicleJourneys) : this.props.onEnterEditMode() }} > - Valider + {this.props.editMode ? "Valider" : "Editer"} </button> </form> </div> @@ -38,7 +38,9 @@ SaveVehicleJourneys.propTypes = { vehicleJourneys: PropTypes.array.isRequired, page: PropTypes.number.isRequired, status: PropTypes.object.isRequired, - filters: PropTypes.object.isRequired + filters: PropTypes.object.isRequired, + onEnterEditMode: PropTypes.func.isRequired, + onSubmitVehicleJourneys: PropTypes.func.isRequired } module.exports = SaveVehicleJourneys diff --git a/app/assets/javascripts/es6_browserified/vehicle_journeys/components/Tools.js b/app/assets/javascripts/es6_browserified/vehicle_journeys/components/Tools.js index b417828db..4948e6b1a 100644 --- a/app/assets/javascripts/es6_browserified/vehicle_journeys/components/Tools.js +++ b/app/assets/javascripts/es6_browserified/vehicle_journeys/components/Tools.js @@ -9,28 +9,34 @@ var NotesEditVehicleJourney = require('../containers/tools/NotesEditVehicleJourn var TimetablesEditVehicleJourney = require('../containers/tools/TimetablesEditVehicleJourney') var actions = require('../actions') -const Tools = ({vehicleJourneys, onCancelSelection}) => { +const Tools = ({vehicleJourneys, onCancelSelection, filters: {policy}, editMode}) => { return ( - <div className='select_toolbox'> - <ul> - <AddVehicleJourney /> - <DuplicateVehicleJourney /> - <ShiftVehicleJourney /> - <EditVehicleJourney /> - <TimetablesEditVehicleJourney /> - <NotesEditVehicleJourney /> - <DeleteVehicleJourneys /> - </ul> + <div> + { + (policy['vehicle_journeys.create'] && policy['vehicle_journeys.update'] && policy['vehicle_journeys.destroy'] && editMode) && + <div className='select_toolbox'> + <ul> + <AddVehicleJourney /> + <DuplicateVehicleJourney /> + <ShiftVehicleJourney /> + <EditVehicleJourney /> + <TimetablesEditVehicleJourney /> + <NotesEditVehicleJourney /> + <DeleteVehicleJourneys /> + </ul> - <span className='info-msg'>{actions.getSelected(vehicleJourneys).length} course(s) sélectionnée(s)</span> - <button className='btn btn-xs btn-link pull-right' onClick={onCancelSelection}>Annuler la sélection</button> + <span className='info-msg'>{actions.getSelected(vehicleJourneys).length} course(s) sélectionnée(s)</span> + <button className='btn btn-xs btn-link pull-right' onClick={onCancelSelection}>Annuler la sélection</button> + </div> + } </div> ) } Tools.propTypes = { vehicleJourneys : PropTypes.array.isRequired, - onCancelSelection: PropTypes.func.isRequired + onCancelSelection: PropTypes.func.isRequired, + filters: PropTypes.object.isRequired } module.exports = Tools diff --git a/app/assets/javascripts/es6_browserified/vehicle_journeys/components/VehicleJourney.js b/app/assets/javascripts/es6_browserified/vehicle_journeys/components/VehicleJourney.js index f2bd0c3cd..ca6694f61 100644 --- a/app/assets/javascripts/es6_browserified/vehicle_journeys/components/VehicleJourney.js +++ b/app/assets/javascripts/es6_browserified/vehicle_journeys/components/VehicleJourney.js @@ -58,7 +58,7 @@ class VehicleJourney extends Component { )} </div> - {(this.props.filters.policy['vehicle_journeys.update'] == true) && + {(this.props.filters.policy['vehicle_journeys.update'] == true && this.props.editMode) && <div className={(this.props.value.deletable ? 'disabled ' : '') + 'checkbox'}> <input id={this.props.index} @@ -79,13 +79,13 @@ class VehicleJourney extends Component { <div className={'cellwrap' + (this.cityNameChecker(vj) ? ' headlined' : '')}> {this.props.filters.toggleArrivals && <div data-headline='Arrivée à '> - <span className={((this.isDisabled(this.props.value.deletable, vj.dummy) || this.props.filters.policy['vehicle_journeys.update'] == false) ? 'disabled ' : '') + 'input-group time'}> + <span className={((this.isDisabled(this.props.value.deletable, vj.dummy) || this.props.filters.policy['vehicle_journeys.update'] == false || this.props.editMode == false) ? 'disabled ' : '') + 'input-group time'}> <input type='number' min='00' max='23' className='form-control' - disabled={(this.isDisabled(this.props.value.deletable, vj.dummy) || this.props.filters.policy['vehicle_journeys.update'] == false)} + disabled={(this.isDisabled(this.props.value.deletable, vj.dummy) || this.props.filters.policy['vehicle_journeys.update'] == false || this.props.editMode == false)} onChange={(e) => {this.props.onUpdateTime(e, i, this.props.index, 'hour', false, false)}} value={vj.arrival_time['hour']} /> @@ -95,7 +95,7 @@ class VehicleJourney extends Component { min='00' max='59' className='form-control' - disabled={((this.isDisabled(this.props.value.deletable), vj.dummy) || this.props.filters.policy['vehicle_journeys.update'] == false)} + disabled={((this.isDisabled(this.props.value.deletable), vj.dummy) || this.props.filters.policy['vehicle_journeys.update'] == false || this.props.editMode == false)} onChange={(e) => {this.props.onUpdateTime(e, i, this.props.index, 'minute', false, false)}} value={vj.arrival_time['minute']} /> @@ -108,13 +108,13 @@ class VehicleJourney extends Component { } </div> <div data-headline='Départ à '> - <span className={((this.isDisabled(this.props.value.deletable, vj.dummy) || this.props.filters.policy['vehicle_journeys.update'] == false) ? 'disabled ' : '') + 'input-group time'}> + <span className={((this.isDisabled(this.props.value.deletable, vj.dummy) || this.props.filters.policy['vehicle_journeys.update'] == false || this.props.editMode == false) ? 'disabled ' : '') + 'input-group time'}> <input type='number' min='00' max='23' className='form-control' - disabled={(this.isDisabled(this.props.value.deletable, vj.dummy) || this.props.filters.policy['vehicle_journeys.update'] == false)} + disabled={(this.isDisabled(this.props.value.deletable, vj.dummy) || this.props.filters.policy['vehicle_journeys.update'] == false || this.props.editMode == false)} onChange={(e) => {this.props.onUpdateTime(e, i, this.props.index, 'hour', true, this.props.filters.toggleArrivals)}} value={vj.departure_time['hour']} /> @@ -124,7 +124,7 @@ class VehicleJourney extends Component { min='00' max='59' className='form-control' - disabled={(this.isDisabled(this.props.value.deletable, vj.dummy) || this.props.filters.policy['vehicle_journeys.update'] == false)} + disabled={(this.isDisabled(this.props.value.deletable, vj.dummy) || this.props.filters.policy['vehicle_journeys.update'] == false || this.props.editMode == false)} onChange={(e) => {this.props.onUpdateTime(e, i, this.props.index, "minute", true, this.props.filters.toggleArrivals)}} value={vj.departure_time['minute']} /> diff --git a/app/assets/javascripts/es6_browserified/vehicle_journeys/components/VehicleJourneys.js b/app/assets/javascripts/es6_browserified/vehicle_journeys/components/VehicleJourneys.js index e8673a25a..8f3f91b25 100644 --- a/app/assets/javascripts/es6_browserified/vehicle_journeys/components/VehicleJourneys.js +++ b/app/assets/javascripts/es6_browserified/vehicle_journeys/components/VehicleJourneys.js @@ -97,7 +97,7 @@ class VehicleJourneys extends Component{ <div className="alert alert-danger mt-sm"> <strong>Erreur : </strong> {this.props.vehicleJourneys.map((vj, index) => - vj.errors.map((err, i) => { + vj.errors && vj.errors.map((err, i) => { return ( <ul key={i}> <li>{err}</li> @@ -131,6 +131,7 @@ class VehicleJourneys extends Component{ value={vj} key={index} index={index} + editMode={this.props.editMode} filters={this.props.filters} onUpdateTime={this.props.onUpdateTime} onSelectVehicleJourney={this.props.onSelectVehicleJourney} 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 34463600a..780b10916 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 @@ -7,29 +7,54 @@ var _ = require('lodash') class DuplicateVehicleJourney extends Component { constructor(props) { super(props) + this.state = {} + this.onFormChange = this.onFormChange.bind(this) + } + + componentWillReceiveProps() { + if (actions.getSelected(this.props.vehicleJourneys).length > 0) { + this.setState((state, props) => { + return { + duplicate_time_hh: parseInt(this.getDefaultValue('hour')), + duplicate_time_mm: parseInt(this.getDefaultValue('minute')), + additional_time: 0, + duplicate_number: 1 + } + }) + } } handleSubmit() { if(actions.validateFields(this.refs) == true) { let newDeparture = { departure_time : { - hour: this.refs.duplicate_time_hh.value, - minute: this.refs.duplicate_time_mm.value + hour: this.state.duplicate_time_hh, + minute: this.state.duplicate_time_mm } } let val = actions.getDuplicateDelta(_.find(actions.getSelected(this.props.vehicleJourneys)[0].vehicle_journey_at_stops, {'dummy': false}), newDeparture) - this.refs.additional_time.value = parseInt(this.refs.additional_time.value) - this.props.onDuplicateVehicleJourney(this.refs, val) + this.props.onDuplicateVehicleJourney(this.state.additional_time, this.state.duplicate_number, val) this.props.onModalClose() $('#DuplicateVehicleJourneyModal').modal('hide') } } + onFormChange(e) { + let {name, value} = e.target + this.setState((state, props) => { + return { + [name]: parseInt(value) + } + }) + } + getDefaultValue(type) { let vjas = _.find(actions.getSelected(this.props.vehicleJourneys)[0].vehicle_journey_at_stops, {'dummy': false}) return vjas.departure_time[type] } + render() { + if(this.props.status.isFetching == true) { return false } @@ -65,22 +90,26 @@ class DuplicateVehicleJourney extends Component { <span className={'input-group time' + (actions.getSelected(this.props.vehicleJourneys).length > 1 ? ' disabled' : '')}> <input type='number' + name='duplicate_time_hh' ref='duplicate_time_hh' min='00' max='23' className='form-control' - defaultValue={this.getDefaultValue('hour')} - disabled={(actions.getSelected(this.props.vehicleJourneys).length > 1 ? 'disabled' : '')} + value={this.state.duplicate_time_hh} + onChange={e => this.onFormChange(e)} + disabled={actions.getSelected(this.props.vehicleJourneys) && (actions.getSelected(this.props.vehicleJourneys).length > 1 ? 'disabled' : '')} /> <span>:</span> <input type='number' + name='duplicate_time_mm' ref='duplicate_time_mm' min='00' max='59' className='form-control' - defaultValue={this.getDefaultValue('minute')} - disabled={(actions.getSelected(this.props.vehicleJourneys).length > 1 ? 'disabled' : '')} + value={this.state.duplicate_time_mm} + onChange={e => this.onFormChange(e)} + disabled={actions.getSelected(this.props.vehicleJourneys) && (actions.getSelected(this.props.vehicleJourneys).length > 1 ? 'disabled' : '')} /> </span> </span> @@ -92,11 +121,13 @@ class DuplicateVehicleJourney extends Component { <input type='number' style={{'width': 104}} + name='duplicate_number' ref='duplicate_number' min='1' max='20' - defaultValue='1' + value={this.state.duplicate_number} className='form-control' + onChange={e => this.onFormChange(e)} onKeyDown={(e) => actions.resetValidation(e.currentTarget)} required /> @@ -105,19 +136,21 @@ class DuplicateVehicleJourney extends Component { <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"> + <span className="col-sm-4"> <input type='number' style={{'width': 104}} + name='additional_time' ref='additional_time' - min='-59' - max='59' - defaultValue='0' - className='form-control' + min='-720' + max='720' + value={this.state.additional_time} + className='form-control disabled' + onChange={e => this.onFormChange(e)} onKeyDown={(e) => actions.resetValidation(e.currentTarget)} required - /> - </div> + /> + </span> </div> </div> @@ -131,7 +164,7 @@ class DuplicateVehicleJourney extends Component { Annuler </button> <button - className='btn btn-primary' + className={'btn btn-primary ' + (this.state.additional_time == 0 ? 'disabled' : '')} type='button' onClick={this.handleSubmit.bind(this)} > diff --git a/app/assets/javascripts/es6_browserified/vehicle_journeys/components/tools/EditVehicleJourney.js b/app/assets/javascripts/es6_browserified/vehicle_journeys/components/tools/EditVehicleJourney.js index d49ea578a..2ff4999c6 100644 --- a/app/assets/javascripts/es6_browserified/vehicle_journeys/components/tools/EditVehicleJourney.js +++ b/app/assets/javascripts/es6_browserified/vehicle_journeys/components/tools/EditVehicleJourney.js @@ -14,8 +14,10 @@ class EditVehicleJourney extends Component { var company; if(this.props.modal.modalProps.selectedCompany) { company = this.props.modal.modalProps.selectedCompany - } else { + } else if (typeof this.props.modal.modalProps.vehicleJourney.company === Object) { company = this.props.modal.modalProps.vehicleJourney.company + } else { + company = undefined } this.props.onEditVehicleJourney(this.refs, company) this.props.onModalClose() @@ -96,6 +98,7 @@ class EditVehicleJourney extends Component { <CompanySelect2 company = {this.props.modal.modalProps.vehicleJourney.company} onSelect2Company = {(e) => this.props.onSelect2Company(e)} + onUnselect2Company = {() => this.props.onUnselect2Company()} /> </div> </div> diff --git a/app/assets/javascripts/es6_browserified/vehicle_journeys/components/tools/ShiftVehicleJourney.js b/app/assets/javascripts/es6_browserified/vehicle_journeys/components/tools/ShiftVehicleJourney.js index 269bb1b8c..dd0bade39 100644 --- a/app/assets/javascripts/es6_browserified/vehicle_journeys/components/tools/ShiftVehicleJourney.js +++ b/app/assets/javascripts/es6_browserified/vehicle_journeys/components/tools/ShiftVehicleJourney.js @@ -6,16 +6,27 @@ var actions = require('../../actions') class ShiftVehicleJourney extends Component { constructor(props) { super(props) + this.state = { + additional_time: 0 + } } handleSubmit() { if(actions.validateFields(this.refs) == true) { - this.props.onShiftVehicleJourney(this.refs) + this.props.onShiftVehicleJourney(this.state.additional_time) this.props.onModalClose() $('#ShiftVehicleJourneyModal').modal('hide') } } + handleAdditionalTimeChange() { + this.setState((state, props) => { + return { + additional_time: parseInt(this.refs.additional_time.value) + } + }) + } + render() { if(this.props.status.isFetching == true) { return false @@ -53,14 +64,16 @@ class ShiftVehicleJourney extends Component { <label className='control-label is-required'>Avec un décalage de</label> <input type='number' + style={{'width': 104}} ref='additional_time' - min='-59' - max='59' + min='-720' + max='720' + value={this.state.additional_time} className='form-control' - defaultValue='0' + onChange={this.handleAdditionalTimeChange.bind(this)} onKeyDown={(e) => actions.resetValidation(e.currentTarget)} required - /> + /> </div> </div> </div> @@ -75,7 +88,7 @@ class ShiftVehicleJourney extends Component { Annuler </button> <button - className='btn btn-primary' + className={'btn btn-primary ' + (this.state.additional_time == 0 ? 'disabled' : '')} type='button' onClick={this.handleSubmit.bind(this)} > diff --git a/app/assets/javascripts/es6_browserified/vehicle_journeys/components/tools/select2s/CompanySelect2.js b/app/assets/javascripts/es6_browserified/vehicle_journeys/components/tools/select2s/CompanySelect2.js index d277be003..c1ce0e92a 100644 --- a/app/assets/javascripts/es6_browserified/vehicle_journeys/components/tools/select2s/CompanySelect2.js +++ b/app/assets/javascripts/es6_browserified/vehicle_journeys/components/tools/select2s/CompanySelect2.js @@ -20,10 +20,11 @@ class BSelect4 extends React.Component{ data={(this.props.company) ? [this.props.company.name] : undefined} value={(this.props.company) ? this.props.company.name : undefined} onSelect={(e) => this.props.onSelect2Company(e) } + onUnselect={() => this.props.onUnselect2Company()} multiple={false} ref='company_id' options={{ - allowClear: false, + allowClear: true, theme: 'bootstrap', width: '100%', placeholder: 'Filtrer par transporteur...', diff --git a/app/assets/javascripts/es6_browserified/vehicle_journeys/containers/SaveVehicleJourneys.js b/app/assets/javascripts/es6_browserified/vehicle_journeys/containers/SaveVehicleJourneys.js index 87bbe5353..c1ce90d38 100644 --- a/app/assets/javascripts/es6_browserified/vehicle_journeys/containers/SaveVehicleJourneys.js +++ b/app/assets/javascripts/es6_browserified/vehicle_journeys/containers/SaveVehicleJourneys.js @@ -5,6 +5,7 @@ var SaveVehicleJourneysComponent = require('../components/SaveVehicleJourneys') const mapStateToProps = (state) => { return { + editMode: state.editMode, vehicleJourneys: state.vehicleJourneys, page: state.pagination.page, status: state.status, @@ -12,6 +13,17 @@ const mapStateToProps = (state) => { } } -const SaveVehicleJourneys = connect(mapStateToProps)(SaveVehicleJourneysComponent) +const mapDispatchToProps = (dispatch) => { + return { + onEnterEditMode: () => { + dispatch(actions.enterEditMode()) + }, + onSubmitVehicleJourneys: (next, state) => { + actions.submitVehicleJourneys(dispatch, state, next) + } + } +} + +const SaveVehicleJourneys = connect(mapStateToProps, mapDispatchToProps)(SaveVehicleJourneysComponent) module.exports = SaveVehicleJourneys diff --git a/app/assets/javascripts/es6_browserified/vehicle_journeys/containers/Tools.js b/app/assets/javascripts/es6_browserified/vehicle_journeys/containers/Tools.js index 35f492c98..a4b3056ac 100644 --- a/app/assets/javascripts/es6_browserified/vehicle_journeys/containers/Tools.js +++ b/app/assets/javascripts/es6_browserified/vehicle_journeys/containers/Tools.js @@ -4,7 +4,9 @@ var actions = require('../actions') const mapStateToProps = (state) => { return { - vehicleJourneys: state.vehicleJourneys + vehicleJourneys: state.vehicleJourneys, + editMode: state.editMode, + filters: state.filters } } diff --git a/app/assets/javascripts/es6_browserified/vehicle_journeys/containers/VehicleJourneysList.js b/app/assets/javascripts/es6_browserified/vehicle_journeys/containers/VehicleJourneysList.js index 176a68500..f834e4457 100644 --- a/app/assets/javascripts/es6_browserified/vehicle_journeys/containers/VehicleJourneysList.js +++ b/app/assets/javascripts/es6_browserified/vehicle_journeys/containers/VehicleJourneysList.js @@ -4,6 +4,7 @@ var VehicleJourneys = require('../components/VehicleJourneys') const mapStateToProps = (state) => { return { + editMode: state.editMode, vehicleJourneys: state.vehicleJourneys, status: state.status, filters: state.filters, diff --git a/app/assets/javascripts/es6_browserified/vehicle_journeys/containers/tools/DuplicateVehicleJourney.js b/app/assets/javascripts/es6_browserified/vehicle_journeys/containers/tools/DuplicateVehicleJourney.js index 224b52a19..70e8fde4d 100644 --- a/app/assets/javascripts/es6_browserified/vehicle_journeys/containers/tools/DuplicateVehicleJourney.js +++ b/app/assets/javascripts/es6_browserified/vehicle_journeys/containers/tools/DuplicateVehicleJourney.js @@ -19,8 +19,8 @@ const mapDispatchToProps = (dispatch) => { onOpenDuplicateModal: () =>{ dispatch(actions.openDuplicateModal()) }, - onDuplicateVehicleJourney: (data, departureDelta) =>{ - dispatch(actions.duplicateVehicleJourney(data, departureDelta)) + onDuplicateVehicleJourney: (addtionalTime, duplicateNumber, departureDelta) =>{ + dispatch(actions.duplicateVehicleJourney(addtionalTime, duplicateNumber, departureDelta)) } } } diff --git a/app/assets/javascripts/es6_browserified/vehicle_journeys/containers/tools/EditVehicleJourney.js b/app/assets/javascripts/es6_browserified/vehicle_journeys/containers/tools/EditVehicleJourney.js index 8f4d43519..ac9772b8a 100644 --- a/app/assets/javascripts/es6_browserified/vehicle_journeys/containers/tools/EditVehicleJourney.js +++ b/app/assets/javascripts/es6_browserified/vehicle_journeys/containers/tools/EditVehicleJourney.js @@ -24,7 +24,10 @@ const mapDispatchToProps = (dispatch) => { }, onSelect2Company: (e) => { dispatch(actions.select2Company(e.params.data)) - } + }, + onUnselect2Company: () => { + dispatch(actions.unselect2Company()) + }, } } diff --git a/app/assets/javascripts/es6_browserified/vehicle_journeys/index.js b/app/assets/javascripts/es6_browserified/vehicle_journeys/index.js index 97aa60526..7872899dc 100644 --- a/app/assets/javascripts/es6_browserified/vehicle_journeys/index.js +++ b/app/assets/javascripts/es6_browserified/vehicle_journeys/index.js @@ -8,10 +8,10 @@ var actions = require("./actions") var enableBatching = require('./batch').enableBatching // logger, DO NOT REMOVE -// var applyMiddleware = require('redux').applyMiddleware -// var createLogger = require('redux-logger') -// var thunkMiddleware = require('redux-thunk').default -// var promise = require('redux-promise') +var applyMiddleware = require('redux').applyMiddleware +var createLogger = require('redux-logger') +var thunkMiddleware = require('redux-thunk').default +var promise = require('redux-promise') var selectedJP = [] @@ -19,6 +19,7 @@ if (window.journeyPatternId) selectedJP.push(window.journeyPatternId) var initialState = { + editMode: false, filters: { selectedJourneyPatterns : selectedJP, policy: window.perms, @@ -85,12 +86,12 @@ if (window.jpOrigin){ initialState.filters.queryString = actions.encodeParams(params) } -// const loggerMiddleware = createLogger() +const loggerMiddleware = createLogger() let store = createStore( enableBatching(vehicleJourneysApp), - initialState - // applyMiddleware(thunkMiddleware, promise, loggerMiddleware) + initialState, + applyMiddleware(thunkMiddleware, promise, loggerMiddleware) ) render( diff --git a/app/assets/javascripts/es6_browserified/vehicle_journeys/reducers/editMode.js b/app/assets/javascripts/es6_browserified/vehicle_journeys/reducers/editMode.js new file mode 100644 index 000000000..2e8af1aa8 --- /dev/null +++ b/app/assets/javascripts/es6_browserified/vehicle_journeys/reducers/editMode.js @@ -0,0 +1,12 @@ +const editMode = (state = {}, action ) => { + switch (action.type) { + case "ENTER_EDIT_MODE": + return true + case "EXIT_EDIT_MODE": + return false + default: + return state + } +} + +module.exports = editMode diff --git a/app/assets/javascripts/es6_browserified/vehicle_journeys/reducers/index.js b/app/assets/javascripts/es6_browserified/vehicle_journeys/reducers/index.js index bd4d7226b..4e0839102 100644 --- a/app/assets/javascripts/es6_browserified/vehicle_journeys/reducers/index.js +++ b/app/assets/javascripts/es6_browserified/vehicle_journeys/reducers/index.js @@ -4,6 +4,7 @@ var pagination = require('./pagination') var modal = require('./modal') var status = require('./status') var filters = require('./filters') +var editMode = require('./editMode') var stopPointsList = require('./stopPointsList') const vehicleJourneysApp = combineReducers({ @@ -12,6 +13,7 @@ const vehicleJourneysApp = combineReducers({ modal, status, filters, + editMode, stopPointsList }) 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 229fd2058..1e5ff4294 100644 --- a/app/assets/javascripts/es6_browserified/vehicle_journeys/reducers/modal.js +++ b/app/assets/javascripts/es6_browserified/vehicle_journeys/reducers/modal.js @@ -57,6 +57,9 @@ const modal = (state = {}, action) => { case 'SELECT_CP_EDIT_MODAL': newModalProps = _.assign({}, state.modalProps, {selectedCompany : action.selectedItem}) return _.assign({}, state, {modalProps: newModalProps}) + case 'UNSELECT_CP_EDIT_MODAL': + newModalProps = _.assign({}, state.modalProps, {selectedCompany : undefined}) + return _.assign({}, state, {modalProps: newModalProps}) case 'SELECT_TT_CALENDAR_MODAL': newModalProps = _.assign({}, state.modalProps, {selectedTimetable : action.selectedItem}) return _.assign({}, state, {modalProps: newModalProps}) 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 d463d4b8f..d397e7632 100644 --- a/app/assets/javascripts/es6_browserified/vehicle_journeys/reducers/vehicleJourneys.js +++ b/app/assets/javascripts/es6_browserified/vehicle_journeys/reducers/vehicleJourneys.js @@ -51,17 +51,8 @@ const vehicleJourney= (state = {}, action, keep) => { let shiftedArray, shiftedSchedule, shiftedVjas shiftedArray = state.vehicle_journey_at_stops.map((vjas, i) => { if (!vjas.dummy){ - shiftedSchedule = { - departure_time: { - hour: vjas.departure_time.hour, - minute: actions.simplePad(parseInt(vjas.departure_time.minute) + parseInt(action.data.additional_time.value)) - }, - arrival_time: { - hour: vjas.arrival_time.hour, - minute: actions.simplePad(parseInt(vjas.arrival_time.minute) + parseInt(action.data.additional_time.value)) - } - } - actions.checkSchedules(shiftedSchedule) + shiftedSchedule = actions.getShiftedSchedule(vjas, action.addtionalTime) + shiftedVjas = _.assign({}, state.vehicle_journey_at_stops[i], shiftedSchedule) vjas = _.assign({}, state.vehicle_journey_at_stops[i], shiftedVjas) if(!keep){ @@ -181,13 +172,13 @@ const vehicleJourneys = (state = [], action) => { let dupeVj let dupes = [] let selectedIndex - let val = action.data.additional_time.value + let val = action.addtionalTime let departureDelta = action.departureDelta state.map((vj, i) => { if(vj.selected){ selectedIndex = i - for (i = 0; i< action.data.duplicate_number.value; i++){ - action.data.additional_time.value = (parseInt(val) * (i + 1)) + departureDelta + for (i = 0; i< action.duplicateNumber; i++){ + action.addtionalTime = (val * (i + 1)) + departureDelta dupeVj = vehicleJourney(vj, action, false) dupeVj.published_journey_name = dupeVj.published_journey_name + '-' + i dupeVj.selected = false diff --git a/app/assets/javascripts/workbench.coffee b/app/assets/javascripts/workbench.coffee index 971462e98..0e9fe62a3 100644 --- a/app/assets/javascripts/workbench.coffee +++ b/app/assets/javascripts/workbench.coffee @@ -1,6 +1,6 @@ $(document).on("click", "#referential_filter_btn", (e) -> dates = [1, 2, 3].reduce (arr, key) -> - arr.push $("#q_validity_period_begin_gteq_#{key}i").val(), $("#q_validity_period_end_gteq__#{key}i").val() + arr.push $("#q_validity_period_begin_gteq_#{key}i").val(), $("#q_validity_period_end_lteq_#{key}i").val() arr , [] @@ -8,6 +8,10 @@ noDate = dates.every (date) -> !date + console.log("valid dates :", validDate) + console.log("no dates :", noDate) + console.log(dates) + unless (validDate || noDate) e.preventDefault() alert(window.I18n.fr.referentials.error_period_filter) diff --git a/app/assets/stylesheets/components/_tables.sass b/app/assets/stylesheets/components/_tables.sass index 3fc92d348..b50721f27 100644 --- a/app/assets/stylesheets/components/_tables.sass +++ b/app/assets/stylesheets/components/_tables.sass @@ -85,6 +85,67 @@ border-top: 2px solid $darkgrey margin-top: 15px + // Overhead + > thead > tr.overhead + > th + font-size: 1.4rem + text-align: center + + &.overheaded-default + background-color: rgba($grey, 0.15) + background-clip: padding-box + border-left: 2px solid rgba($grey, 0.15) + border-right: 2px solid rgba($grey, 0.15) + + &.overheaded-danger + background-color: $red + border-left: 2px solid $red + border-right: 2px solid $red + color: #fff + + &.overheaded-warning + background-color: $orange + border-left: 2px solid $orange + border-right: 2px solid $orange + color: #fff + + &.overheaded-success + background-color: $green + border-left: 2px solid $green + border-right: 2px solid $green + color: #fff + + td, th + &.overheaded-default + border-left: 2px solid rgba($grey, 0.15) + border-right: 2px solid rgba($grey, 0.15) + + &.overheaded-danger + border-left: 2px solid $red + border-right: 2px solid $red + + &.overheaded-warning + border-left: 2px solid $orange + border-right: 2px solid $orange + + &.overheaded-success + border-left: 2px solid $green + border-right: 2px solid $green + + tr:last-child + td + &.overheaded-default + border-bottom: 2px solid rgba($grey, 0.15) + + &.overheaded-danger + border-bottom: 2px solid $red + + &.overheaded-warning + border-bottom: 2px solid $orange + + &.overheaded-success + border-bottom: 2px solid $green + // Specific for tables displaying stop points &.has-stoppoints tbody @@ -144,7 +205,6 @@ margin-top: -8px - // select_toolbox .select_toolbox padding: 10px diff --git a/app/concerns/configurable.rb b/app/concerns/configurable.rb new file mode 100644 index 000000000..c7d0f1fd9 --- /dev/null +++ b/app/concerns/configurable.rb @@ -0,0 +1,26 @@ +module Configurable + + module ClassMethods + def config &blk + blk ? blk.(configuration) : configuration + end + + private + def configuration + @__configuration__ ||= Rails::Application::Configuration.new + end + end + + module InstanceMethods + private + + def config + self.class.config + end + end + + def self.included(into) + into.extend ClassMethods + into.send :include, InstanceMethods + end +end diff --git a/app/controllers/api/v1/chouette_controller.rb b/app/controllers/api/v1/chouette_controller.rb index 7805074ee..98c2fff05 100644 --- a/app/controllers/api/v1/chouette_controller.rb +++ b/app/controllers/api/v1/chouette_controller.rb @@ -7,7 +7,6 @@ module Api before_action :authenticate private - def authenticate authenticate_or_request_with_http_token do |token, options| @referential = Api::V1::ApiKey.referential_from_token(token) @@ -16,10 +15,10 @@ module Api switch_referential if @api_key end end + def switch_referential Apartment::Tenant.switch!(@api_key.referential.slug) - end - + end end end end diff --git a/app/controllers/api/v1/iboo_controller.rb b/app/controllers/api/v1/iboo_controller.rb new file mode 100644 index 000000000..4db9e9007 --- /dev/null +++ b/app/controllers/api/v1/iboo_controller.rb @@ -0,0 +1,21 @@ +class Api::V1::IbooController < Api::V1::ChouetteController + protected + def begin_of_association_chain + @current_organisation + end + + private + def authenticate + authenticate_with_http_basic do |code, token| + if organisation = Organisation.find_by(code: code) + if organisation.api_keys.exists?(token: token) + @current_organisation = organisation + end + end + end + + unless @current_organisation + request_http_basic_authentication + end + end +end diff --git a/app/controllers/api/v1/imports_controller.rb b/app/controllers/api/v1/imports_controller.rb new file mode 100644 index 000000000..6050418d8 --- /dev/null +++ b/app/controllers/api/v1/imports_controller.rb @@ -0,0 +1,15 @@ +class Api::V1::ImportsController < Api::V1::IbooController + defaults :resource_class => WorkbenchImport + belongs_to :workbench + + def create + args = workbench_import_params.merge(creator: 'Webservice') + @import = parent.workbench_imports.create(args) + create! + end + + private + def workbench_import_params + params.require(:workbench_import).permit(:file, :name) + end +end diff --git a/app/controllers/api/v1/netex_imports_controller.rb b/app/controllers/api/v1/netex_imports_controller.rb new file mode 100644 index 000000000..8f7c8e67e --- /dev/null +++ b/app/controllers/api/v1/netex_imports_controller.rb @@ -0,0 +1,55 @@ +module Api + module V1 + class NetexImportsController < ChouetteController + include ControlFlow + + def create + respond_to do | format | + format.json(&method(:create_models)) + end + end + + + private + + def find_workbench + @workbench = Workbench.find(netex_import_params['workbench_id']) + rescue ActiveRecord::RecordNotFound + render json: {errors: {'workbench_id' => 'missing'}}, status: 406 + finish_action! + end + + def create_models + find_workbench + create_referential + create_netex_import + end + + def create_netex_import + @netex_import = NetexImport.new(netex_import_params.merge(referential_id: @new_referential.id, creator: 'Webservice')) + @netex_import.save! + rescue ActiveRecord::RecordInvalid + render json: {errors: @netex_import.errors}, status: 406 + finish_action! + end + + def create_referential + @new_referential = + Referential.new( + name: netex_import_params['name'], + organisation_id: @workbench.organisation_id, + workbench_id: @workbench.id) + @new_referential.save! + rescue ActiveRecord::RecordInvalid + render json: {errors: @new_referential.errors}, status: 406 + finish_action! + end + + def netex_import_params + params + .require('netex_import') + .permit(:file, :name, :workbench_id) + end + end + end +end diff --git a/app/controllers/api/v1/workbenches_controller.rb b/app/controllers/api/v1/workbenches_controller.rb new file mode 100644 index 000000000..3c07997ce --- /dev/null +++ b/app/controllers/api/v1/workbenches_controller.rb @@ -0,0 +1,3 @@ +class Api::V1::WorkbenchesController < Api::V1::IbooController + defaults :resource_class => Workbench +end diff --git a/app/controllers/api_keys_controller.rb b/app/controllers/api_keys_controller.rb index 35a84da87..7059cf52e 100644 --- a/app/controllers/api_keys_controller.rb +++ b/app/controllers/api_keys_controller.rb @@ -1,22 +1,32 @@ -class ApiKeysController < ChouetteController - defaults :resource_class => Api::V1::ApiKey - - belongs_to :referential +class ApiKeysController < BreadcrumbController + defaults resource_class: Api::V1::ApiKey def create - create! { referential_path(@referential) } + @api_key = Api::V1::ApiKey.new(api_key_params.merge(organisation: current_organisation)) + create! { organisation_api_keys_path } + end + + def index + @api_keys = decorate_api_keys(current_organisation.api_keys.paginate(page: params[:page])) end + def update - update! { referential_path(@referential) } + update! { organisation_api_key_path(resource) } end + def destroy - destroy! { referential_path(@referential) } + destroy! { organisation_api_keys_path } end private def api_key_params - params.require(:api_key).permit( :name ) - end - -end + params.require(:api_key).permit(:name, :referential_id) + end + def decorate_api_keys(api_keys) + ModelDecorator.decorate( + api_keys, + with: ApiKeyDecorator, + ) + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 8fcaa3b1b..d15aa336d 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -37,6 +37,7 @@ class ApplicationController < ActionController::Base current_organisation end + # Overwriting the sign_out redirect path method def after_sign_out_path_for(resource_or_scope) new_user_session_path diff --git a/app/controllers/autocomplete_time_tables_controller.rb b/app/controllers/autocomplete_time_tables_controller.rb index e977a28b0..e5d2b41ae 100644 --- a/app/controllers/autocomplete_time_tables_controller.rb +++ b/app/controllers/autocomplete_time_tables_controller.rb @@ -15,9 +15,9 @@ class AutocompleteTimeTablesController < InheritedResources::Base protected def select_time_tables - scope = referential.time_tables + scope = referential.time_tables.where("time_tables.id != ?", params[:source_id]) if params[:route_id] - scope = scope.joins(vehicle_journeys: :route).where( "routes.id IN (#{params[:route_id]})") + scope = scope.joins(vehicle_journeys: :route).where( "routes.id IN (#{params[:route_id]}) AND time_tables.id != #{params[:time_table_id]}") end scope end diff --git a/app/controllers/concerns/control_flow.rb b/app/controllers/concerns/control_flow.rb new file mode 100644 index 000000000..0f41a1cda --- /dev/null +++ b/app/controllers/concerns/control_flow.rb @@ -0,0 +1,14 @@ +module ControlFlow + FinishAction = Class.new RuntimeError + + def self.included into + into.rescue_from FinishAction, with: :catch_finish_action + end + + # Allow to exit locally inside an action after rendering (especially in error cases) + def catch_finish_action; end + + def finish_action! msg = 'finish action' + raise FinishAction, msg + end +end diff --git a/app/controllers/import_tasks_controller.rb b/app/controllers/import_tasks_controller.rb index 0e3ed6445..cb377ec5a 100644 --- a/app/controllers/import_tasks_controller.rb +++ b/app/controllers/import_tasks_controller.rb @@ -1,4 +1,3 @@ -# coding: utf-8 class ImportTasksController < ChouetteController defaults :resource_class => ImportTask diff --git a/app/controllers/imports_controller.rb b/app/controllers/imports_controller.rb index 70c5c1a0d..979c9bfcf 100644 --- a/app/controllers/imports_controller.rb +++ b/app/controllers/imports_controller.rb @@ -6,12 +6,24 @@ class ImportsController < BreadcrumbController def show show! do + @import = @import.decorate(context: { + workbench: @workbench + }) + build_breadcrumb :show end end def index - index! do + index! do |format| + format.html { + if collection.out_of_bounds? + redirect_to params.merge(:page => 1) + end + + @imports = decorate_imports(@imports) + } + build_breadcrumb :index end end @@ -34,16 +46,50 @@ class ImportsController < BreadcrumbController end end + protected + def collection + @q = parent.imports.search(params[:q]) + + if sort_column && sort_direction + @imports ||= @q.result(distinct: true).order(sort_column + ' ' + sort_direction).paginate(page: params[:page], per_page: 10) + else + @imports ||= @q.result(distinct: true).order(:name).paginate(page: params[:page], per_page: 10) + end + end + private def build_resource # Manage only NetexImports for the moment @import ||= NetexImport.new(*resource_params) do |import| import.workbench = parent + import.creator = current_user.name end end def import_params - params.require(:import).permit(:name, :file, :type, :referential_id) + params.require(:import).permit( + :name, + :file, + :type, + :referential_id + ) + end + + def sort_column + parent.imports.column_names.include?(params[:sort]) ? params[:sort] : 'name' + end + def sort_direction + %w[asc desc].include?(params[:direction]) ? params[:direction] : 'asc' + end + + def decorate_imports(imports) + ModelDecorator.decorate( + imports, + with: ImportDecorator, + context: { + workbench: @workbench + } + ) end end diff --git a/app/controllers/time_table_combinations_controller.rb b/app/controllers/time_table_combinations_controller.rb index 32f1818b0..ba61a2ea4 100644 --- a/app/controllers/time_table_combinations_controller.rb +++ b/app/controllers/time_table_combinations_controller.rb @@ -3,13 +3,17 @@ class TimeTableCombinationsController < ChouetteController belongs_to :time_table, :parent_class => Chouette::TimeTable end + # include PolicyChecker + def new @combination = TimeTableCombination.new(source_id: parent.id) + authorize @combination @combination.combined_type = 'time_table' end def create @combination = TimeTableCombination.new(params[:time_table_combination].merge(source_id: parent.id)) + authorize @combination @combination.valid? ? perform_combination : render(:new) end diff --git a/app/controllers/time_tables_controller.rb b/app/controllers/time_tables_controller.rb index 2ff7a2c3a..0054963c9 100644 --- a/app/controllers/time_tables_controller.rb +++ b/app/controllers/time_tables_controller.rb @@ -161,6 +161,7 @@ class TimeTablesController < ChouetteController private def ransack_periode scope return scope unless params[:q] + return scope unless params[:q]['end_date_lteq(1i)'].present? begin_range = flatten_date('start_date_gteq') end_range = flatten_date('end_date_lteq') diff --git a/app/decorators/api_key_decorator.rb b/app/decorators/api_key_decorator.rb new file mode 100644 index 000000000..def3a6a01 --- /dev/null +++ b/app/decorators/api_key_decorator.rb @@ -0,0 +1,30 @@ +class ApiKeyDecorator < Draper::Decorator + decorates Api::V1::ApiKey + delegate_all + + + def action_links + links = [] + + links << Link.new( + content: h.t('api_keys.actions.show'), + href: h.organisation_api_key_path(object), + ) + + links << Link.new( + content: h.t('api_keys.actions.edit'), + href: h.edit_organisation_api_key_path(object), + ) + + if h.policy(object).destroy? + links << Link.new( + content: h.destroy_link_content, + href: h.organisation_api_key_path(object), + method: :delete, + data: { confirm: h.t('api_keys.actions.destroy_confirm') } + ) + end + + links + end +end diff --git a/app/decorators/import_decorator.rb b/app/decorators/import_decorator.rb new file mode 100644 index 000000000..eb6a34a13 --- /dev/null +++ b/app/decorators/import_decorator.rb @@ -0,0 +1,36 @@ +class ImportDecorator < Draper::Decorator + decorates Import + + delegate_all + + def action_links + links = [] + + links << Link.new( + content: h.t('imports.actions.show'), + href: h.workbench_import_path( + context[:workbench], + object + ) + ) + + links << Link.new( + content: h.t('imports.actions.download'), + href: object.file.url + ) + + # if h.policy(object).destroy? + links << Link.new( + content: h.destroy_link_content, + href: h.workbench_import_path( + context[:workbench], + object + ), + method: :delete, + data: { confirm: h.t('imports.actions.destroy_confirm') } + ) + + links + end + +end diff --git a/app/decorators/time_table_decorator.rb b/app/decorators/time_table_decorator.rb index 526537310..c6eeac176 100644 --- a/app/decorators/time_table_decorator.rb +++ b/app/decorators/time_table_decorator.rb @@ -21,13 +21,15 @@ class TimeTableDecorator < Draper::Decorator ) end - links << Link.new( - content: h.t('actions.combine'), - href: h.new_referential_time_table_time_table_combination_path( - context[:referential], - object + if h.policy(object).edit? + links << Link.new( + content: h.t('actions.combine'), + href: h.new_referential_time_table_time_table_combination_path( + context[:referential], + object + ) ) - ) + end if h.policy(object).duplicate? links << Link.new( diff --git a/app/helpers/table_builder_helper.rb b/app/helpers/table_builder_helper.rb index 375697bec..f15019458 100644 --- a/app/helpers/table_builder_helper.rb +++ b/app/helpers/table_builder_helper.rb @@ -42,7 +42,8 @@ require 'table_builder_helper/url' # ), # ], # links: [:show, :edit], -# cls: 'table has-search' +# cls: 'table has-search', +# overhead: [ {title: 'one', width: 1, cls: 'toto'}, {title: 'two <span class="test">Info</span>', width: 2, cls: 'default'} ] # ) module TableBuilderHelper # TODO: rename this after migration from `table_builder` @@ -65,19 +66,36 @@ module TableBuilderHelper links: [], # A CSS class to apply to the <table> - cls: '' + cls: '', + + # A set of content, over the th line... + overhead: [] ) content_tag :table, - thead(collection, columns, sortable, selectable, links.any?) + - tbody(collection, columns, selectable, links), + thead(collection, columns, sortable, selectable, links.any?, overhead) + + tbody(collection, columns, selectable, links, overhead), class: cls end private - def thead(collection, columns, sortable, selectable, has_links) + def thead(collection, columns, sortable, selectable, has_links, overhead) content_tag :thead do - content_tag :tr do + # Inserts overhead content if any specified + over_head = '' + + unless overhead.empty? + over_head = content_tag :tr, class: 'overhead' do + oh_cont = [] + + overhead.each do |h| + oh_cont << content_tag(:th, raw(h[:title]), colspan: h[:width], class: h[:cls]) + end + oh_cont.join.html_safe + end + end + + main_head = content_tag :tr do hcont = [] if selectable @@ -85,25 +103,73 @@ module TableBuilderHelper end columns.each do |column| - hcont << content_tag(:th, build_column_header( - column, - sortable, - collection.model, - params, - params[:sort], - params[:direction] - )) + if overhead.empty? + hcont << content_tag(:th, build_column_header( + column, + sortable, + collection.model, + params, + params[:sort], + params[:direction] + )) + + else + i = columns.index(column) + + if overhead[i].blank? + if (i > 0) && (overhead[i - 1][:width] > 1) + clsArrayH = overhead[i - 1][:cls].split + + hcont << content_tag(:th, build_column_header( + column, + sortable, + collection.model, + params, + params[:sort], + params[:direction] + ), class: td_cls(clsArrayH)) + + else + hcont << content_tag(:th, build_column_header( + column, + sortable, + collection.model, + params, + params[:sort], + params[:direction] + )) + end + + else + clsArrayH = overhead[i][:cls].split + + hcont << content_tag(:th, build_column_header( + column, + sortable, + collection.model, + params, + params[:sort], + params[:direction] + ), class: td_cls(clsArrayH)) + + end + + end end # Inserts a blank column for the gear menu - hcont << content_tag(:th, '') if has_links + if has_links || collection.last.try(:action_links).try(:any?) + hcont << content_tag(:th, '') + end hcont.join.html_safe end + + (over_head + main_head).html_safe end end - def tbody(collection, columns, selectable, links) + def tbody(collection, columns, selectable, links, overhead) content_tag :tbody do collection.map do |item| @@ -126,13 +192,57 @@ module TableBuilderHelper item, referential ) - bcont << content_tag(:td, link_to(value, polymorph_url), title: 'Voir') + + if overhead.empty? + bcont << content_tag(:td, link_to(value, polymorph_url), title: 'Voir') + + else + i = columns.index(column) + + if overhead[i].blank? + if (i > 0) && (overhead[i - 1][:width] > 1) + clsArrayAlt = overhead[i - 1][:cls].split + + bcont << content_tag(:td, link_to(value, polymorph_url), title: 'Voir', class: td_cls(clsArrayAlt)) + + else + bcont << content_tag(:td, link_to(value, polymorph_url), title: 'Voir') + end + + else + clsArray = overhead[columns.index(column)][:cls].split + + bcont << content_tag(:td, link_to(value, polymorph_url), title: 'Voir', class: td_cls(clsArray)) + end + end + else - bcont << content_tag(:td, value) + if overhead.empty? + bcont << content_tag(:td, value) + + else + i = columns.index(column) + + if overhead[i].blank? + if (i > 0) && (overhead[i - 1][:width] > 1) + clsArrayAlt = overhead[i - 1][:cls].split + + bcont << content_tag(:td, value, class: td_cls(clsArrayAlt)) + + else + bcont << content_tag(:td, value) + end + + else + clsArray = overhead[i][:cls].split + + bcont << content_tag(:td, value, class: td_cls(clsArray)) + end + end end end - if links.any? + if links.any? || item.try(:action_links).try(:any?) bcont << content_tag( :td, build_links(item, links), @@ -146,6 +256,14 @@ module TableBuilderHelper end end + def td_cls(a) + if a.include? 'full-border' + a.slice!(a.index('full-border')) + + return a.join(' ') + end + end + def build_links(item, links) trigger = content_tag( :div, diff --git a/app/helpers/table_builder_helper/url.rb b/app/helpers/table_builder_helper/url.rb index 0894df0fe..f7ba703ae 100644 --- a/app/helpers/table_builder_helper/url.rb +++ b/app/helpers/table_builder_helper/url.rb @@ -12,7 +12,12 @@ module TableBuilderHelper polymorph_url << item.stop_area if item.respond_to? :stop_area polymorph_url << item if item.respond_to?(:stop_points) || item.is_a?(Chouette::TimeTable) elsif item.respond_to? :referential - polymorph_url << item.referential + if item.respond_to? :workbench + polymorph_url << item.workbench + polymorph_url << item + else + polymorph_url << item.referential + end end else polymorph_url << item diff --git a/app/models/api/v1/api_key.rb b/app/models/api/v1/api_key.rb index 7390db232..767e65f3a 100644 --- a/app/models/api/v1/api_key.rb +++ b/app/models/api/v1/api_key.rb @@ -3,9 +3,32 @@ module Api class ApiKey < ::ActiveRecord::Base before_create :generate_access_token belongs_to :referential, :class_name => '::Referential' + belongs_to :organisation, :class_name => '::Organisation' - def self.model_name - ActiveModel::Name.new self, Api::V1, self.name.demodulize + validates_presence_of :organisation + + class << self + def from(referential, name:) + find_or_create_by!(name: name, referential: referential) + end + + def referential_from_token(token) + array = token.split('-') + if !array.first.empty? && array.size > 1 + ::Referential.find array.first + end + end + + def model_name + ActiveModel::Name.new self, Api::V1, self.name.demodulize + end + + def organisation_from_token(token) + array = token.split('-') + if !array[1].empty? && array.size > 1 + ::Organisation.find array[1] + end + end end def eql?(other) @@ -13,16 +36,11 @@ module Api other.token == self.token end - def self.referential_from_token(token) - array = token.split('-') - return nil unless array.size==2 - ::Referential.find( array.first) - end private def generate_access_token begin - self.token = "#{referential.id}-#{SecureRandom.hex}" + self.token = "#{referential_id}-#{organisation_id}-#{SecureRandom.hex}" end while self.class.exists?(:token => self.token) end end diff --git a/app/models/chouette/footnote.rb b/app/models/chouette/footnote.rb index de427b249..1664faf23 100644 --- a/app/models/chouette/footnote.rb +++ b/app/models/chouette/footnote.rb @@ -1,6 +1,13 @@ class Chouette::Footnote < Chouette::ActiveRecord + include ChecksumSupport + belongs_to :line, inverse_of: :footnotes has_and_belongs_to_many :vehicle_journeys, :class_name => 'Chouette::VehicleJourney' validates_presence_of :line + + def checksum_attributes + attrs = ['code', 'label'] + self.slice(*attrs).values + end end diff --git a/app/models/chouette/journey_pattern.rb b/app/models/chouette/journey_pattern.rb index f238d7339..fa5fba26d 100644 --- a/app/models/chouette/journey_pattern.rb +++ b/app/models/chouette/journey_pattern.rb @@ -1,4 +1,5 @@ class Chouette::JourneyPattern < Chouette::TridentActiveRecord + include ChecksumSupport include JourneyPatternRestrictions # FIXME http://jira.codehaus.org/browse/JRUBY-6358 self.primary_key = "id" @@ -20,6 +21,12 @@ class Chouette::JourneyPattern < Chouette::TridentActiveRecord attr_accessor :control_checked after_update :control_route_sections, :unless => "control_checked" + def checksum_attributes + values = self.slice(*['name', 'published_name', 'registration_number']).values + values << self.stop_points.map(&:stop_area).map(&:user_objectid) + values.flatten + end + def self.state_update route, state transaction do state.each do |item| diff --git a/app/models/chouette/route.rb b/app/models/chouette/route.rb index 76905bf2b..6774e8a86 100644 --- a/app/models/chouette/route.rb +++ b/app/models/chouette/route.rb @@ -1,5 +1,6 @@ class Chouette::Route < Chouette::TridentActiveRecord include RouteRestrictions + include ChecksumSupport extend Enumerize extend ActiveModel::Naming @@ -99,6 +100,14 @@ class Chouette::Route < Chouette::TridentActiveRecord end end + def checksum_attributes + values = self.slice(*['name', 'published_name', 'wayback']).values + values.tap do |attrs| + attrs << self.stop_points.map{|sp| "#{sp.stop_area.user_objectid}#{sp.for_boarding}#{sp.for_alighting}" }.join + attrs << self.routing_constraint_zones.map(&:checksum) + end + end + def geometry points = stop_areas.map(&:to_lat_lng).compact.map do |loc| [loc.lng, loc.lat] diff --git a/app/models/chouette/routing_constraint_zone.rb b/app/models/chouette/routing_constraint_zone.rb index a649b0b8e..9931748b2 100644 --- a/app/models/chouette/routing_constraint_zone.rb +++ b/app/models/chouette/routing_constraint_zone.rb @@ -1,4 +1,6 @@ class Chouette::RoutingConstraintZone < Chouette::TridentActiveRecord + include ChecksumSupport + belongs_to :route has_array_of :stop_points, class_name: 'Chouette::StopPoint' @@ -15,6 +17,10 @@ class Chouette::RoutingConstraintZone < Chouette::TridentActiveRecord .order("routes.name #{direction}") end + def checksum_attributes + self.stop_points.map(&:stop_area).map(&:user_objectid) + end + def stop_points_belong_to_route errors.add(:stop_point_ids, I18n.t('activerecord.errors.models.routing_constraint_zone.attributes.stop_points.stop_points_not_from_route')) unless stop_points.all? { |sp| route.stop_points.include? sp } end diff --git a/app/models/chouette/time_table.rb b/app/models/chouette/time_table.rb index 1753cbed5..3f56f6a1d 100644 --- a/app/models/chouette/time_table.rb +++ b/app/models/chouette/time_table.rb @@ -1,4 +1,5 @@ class Chouette::TimeTable < Chouette::TridentActiveRecord + include ChecksumSupport include TimeTableRestrictions # FIXME http://jira.codehaus.org/browse/JRUBY-6358 self.primary_key = "id" @@ -26,6 +27,14 @@ class Chouette::TimeTable < Chouette::TridentActiveRecord after_save :save_shortcuts + def checksum_attributes + [].tap do |attrs| + attrs << self.int_day_types + attrs << self.dates.map(&:checksum).map(&:to_s).sort + attrs << self.periods.map(&:checksum).map(&:to_s).sort + end + end + def self.object_id_key "Timetable" end @@ -478,7 +487,7 @@ class Chouette::TimeTable < Chouette::TridentActiveRecord def merge!(another_tt) transaction do days = [].tap do |array| - array.push(*self.included_days_in_dates_and_periods, *another_tt.effective_days) + array.push(*self.effective_days, *another_tt.effective_days) array.uniq! end @@ -507,7 +516,7 @@ class Chouette::TimeTable < Chouette::TridentActiveRecord def intersect!(another_tt) transaction do days = [].tap do |array| - array.push(*self.included_days_in_dates_and_periods) + array.push(*self.effective_days) array.delete_if {|day| !another_tt.effective_days.include?(day) } array.uniq! end @@ -527,7 +536,7 @@ class Chouette::TimeTable < Chouette::TridentActiveRecord def disjoin!(another_tt) transaction do days = [].tap do |array| - array.push(*self.included_days_in_dates_and_periods) + array.push(*self.effective_days) array.delete_if {|day| another_tt.effective_days.include?(day) } array.uniq! end diff --git a/app/models/chouette/time_table_date.rb b/app/models/chouette/time_table_date.rb index b881c9a5d..1893eae91 100644 --- a/app/models/chouette/time_table_date.rb +++ b/app/models/chouette/time_table_date.rb @@ -1,4 +1,6 @@ class Chouette::TimeTableDate < Chouette::ActiveRecord + include ChecksumSupport + self.primary_key = "id" belongs_to :time_table, inverse_of: :dates acts_as_list :scope => 'time_table_id = #{time_table_id}',:top_of_list => 0 @@ -12,5 +14,9 @@ class Chouette::TimeTableDate < Chouette::ActiveRecord ActiveModel::Name.new Chouette::TimeTableDate, Chouette, "TimeTableDate" end + def checksum_attributes + attrs = ['date', 'in_out'] + self.slice(*attrs).values + end end diff --git a/app/models/chouette/time_table_period.rb b/app/models/chouette/time_table_period.rb index 6d3486bb6..ed136f3b9 100644 --- a/app/models/chouette/time_table_period.rb +++ b/app/models/chouette/time_table_period.rb @@ -1,4 +1,6 @@ class Chouette::TimeTablePeriod < Chouette::ActiveRecord + include ChecksumSupport + self.primary_key = "id" belongs_to :time_table, inverse_of: :periods acts_as_list :scope => 'time_table_id = #{time_table_id}',:top_of_list => 0 @@ -7,6 +9,10 @@ class Chouette::TimeTablePeriod < Chouette::ActiveRecord validate :start_must_be_before_end + def checksum_attributes + attrs = ['period_start', 'period_end'] + self.slice(*attrs).values + end def self.model_name ActiveModel::Name.new Chouette::TimeTablePeriod, Chouette, "TimeTablePeriod" diff --git a/app/models/chouette/vehicle_journey.rb b/app/models/chouette/vehicle_journey.rb index 5e86a4897..d5ca58959 100644 --- a/app/models/chouette/vehicle_journey.rb +++ b/app/models/chouette/vehicle_journey.rb @@ -1,5 +1,6 @@ module Chouette class VehicleJourney < TridentActiveRecord + include ChecksumSupport include VehicleJourneyRestrictions include StifTransportModeEnumerations # FIXME http://jira.codehaus.org/browse/JRUBY-6358 @@ -55,6 +56,16 @@ module Chouette end end + def checksum_attributes + [].tap do |attrs| + attrs << self.published_journey_name + attrs << self.published_journey_identifier + attrs << self.try(:company).try(:objectid).try(:local_id) + attrs << self.footnotes.map(&:checksum).sort + attrs << self.vehicle_journey_at_stops.map(&:checksum).sort + end + end + def set_default_values if number.nil? self.number = 0 diff --git a/app/models/chouette/vehicle_journey_at_stop.rb b/app/models/chouette/vehicle_journey_at_stop.rb index 35d94aa75..156cc761f 100644 --- a/app/models/chouette/vehicle_journey_at_stop.rb +++ b/app/models/chouette/vehicle_journey_at_stop.rb @@ -2,6 +2,7 @@ module Chouette class VehicleJourneyAtStop < ActiveRecord include ForBoardingEnumerations include ForAlightingEnumerations + include ChecksumSupport DAY_OFFSET_MAX = 1 @@ -66,6 +67,13 @@ module Chouette offset < 0 || offset > DAY_OFFSET_MAX end - + def checksum_attributes + [].tap do |attrs| + attrs << self.departure_time.try(:to_s, :time) + attrs << self.arrival_time.try(:to_s, :time) + attrs << self.departure_day_offset.to_s + attrs << self.arrival_day_offset.to_s + end + end end end diff --git a/app/models/clean_up.rb b/app/models/clean_up.rb index b1135a155..7aab7f32e 100644 --- a/app/models/clean_up.rb +++ b/app/models/clean_up.rb @@ -153,13 +153,13 @@ class CleanUp < ActiveRecord::Base update_attribute(:started_at, Time.now) end - def log_successful message_attributs + def log_successful message_attributes update_attribute(:ended_at, Time.now) - CleanUpResult.create(clean_up: self, message_key: :successfull, message_attributs: message_attributs) + CleanUpResult.create(clean_up: self, message_key: :successfull, message_attributes: message_attributes) end - def log_failed message_attributs + def log_failed message_attributes update_attribute(:ended_at, Time.now) - CleanUpResult.create(clean_up: self, message_key: :failed, message_attributs: message_attributs) + CleanUpResult.create(clean_up: self, message_key: :failed, message_attributes: message_attributes) end end diff --git a/app/models/concerns/checksum_support.rb b/app/models/concerns/checksum_support.rb new file mode 100644 index 000000000..c95e23bcf --- /dev/null +++ b/app/models/concerns/checksum_support.rb @@ -0,0 +1,29 @@ +module ChecksumSupport + extend ActiveSupport::Concern + SEPARATOR = '|' + VALUE_FOR_NIL_ATTRIBUTE = '-' + + included do + before_save :set_current_checksum_source, :update_checksum + end + + def checksum_attributes + self.attributes.values + end + + def current_checksum_source + source = self.checksum_attributes.map{ |x| x unless x.try(:empty?) } + source = source.map{ |x| x || VALUE_FOR_NIL_ATTRIBUTE } + source.map(&:to_s).join(SEPARATOR) + end + + def set_current_checksum_source + self.checksum_source = self.current_checksum_source + end + + def update_checksum + if self.checksum_source_changed? + self.checksum = Digest::SHA256.new.hexdigest(self.checksum_source) + end + end +end diff --git a/app/models/import.rb b/app/models/import.rb index d0736ab0b..b34ca2b48 100644 --- a/app/models/import.rb +++ b/app/models/import.rb @@ -3,13 +3,55 @@ class Import < ActiveRecord::Base belongs_to :workbench belongs_to :referential + belongs_to :parent, polymorphic: true + extend Enumerize enumerize :status, in: %i(new pending successful failed running aborted canceled) validates :file, presence: true + validates_presence_of :workbench, :creator + + before_create :initialize_fields + + def self.model_name + ActiveModel::Name.new Import, Import, "Import" + end + + def self.failing_statuses + symbols_with_indifferent_access(%i(failed aborted canceled)) + end + + def self.finished_statuses + symbols_with_indifferent_access(%i(successful failed aborted canceled)) + end + + def notify_parent + parent.child_change(self) + update(notified_parent_at: DateTime.now) + end - before_create do + def child_change(child) + return if self.class.finished_statuses.include?(status) + + if self.class.failing_statuses.include?(child.status) + return update(status: 'failed') + end + + update(status: 'successful') if ready? + end + + def ready? + current_step == total_steps + end + + private + + def initialize_fields self.token_download = SecureRandom.urlsafe_base64 self.status = Import.status.new end + + def self.symbols_with_indifferent_access(array) + array.flat_map { |symbol| [symbol, symbol.to_s] } + end end diff --git a/app/models/line_referential_sync.rb b/app/models/line_referential_sync.rb index 6730ddd73..75c1e48a2 100644 --- a/app/models/line_referential_sync.rb +++ b/app/models/line_referential_sync.rb @@ -40,11 +40,11 @@ class LineReferentialSync < ActiveRecord::Base end end - def create_sync_message criticity, key, message_attributs = {} + def create_sync_message criticity, key, message_attributes = {} params = { criticity: criticity, message_key: key, - message_attributs: message_attributs + message_attributes: message_attributes } line_referential_sync_messages.create params end @@ -54,13 +54,13 @@ class LineReferentialSync < ActiveRecord::Base create_sync_message :info, :pending end - def log_successful message_attributs + def log_successful message_attributes update_attribute(:ended_at, Time.now) - create_sync_message :info, :successful, message_attributs + create_sync_message :info, :successful, message_attributes end - def log_failed message_attributs + def log_failed message_attributes update_attribute(:ended_at, Time.now) - create_sync_message :error, :failed, message_attributs + create_sync_message :error, :failed, message_attributes end end diff --git a/app/models/netex_import.rb b/app/models/netex_import.rb index de5b84537..575cef816 100644 --- a/app/models/netex_import.rb +++ b/app/models/netex_import.rb @@ -2,12 +2,13 @@ require 'net/http' class NetexImport < Import after_commit :launch_java_import + def launch_java_import logger.warn "Call iev get #{Rails.configuration.iev_url}/boiv_iev/referentials/importer/new?id=#{id}" begin Net::HTTP.get(URI("#{Rails.configuration.iev_url}/boiv_iev/referentials/importer/new?id=#{id}")) rescue Exception => e - logger.error "IEV server error : e.message" + logger.error "IEV server error : #{e.message}" logger.error e.backtrace.inspect end end diff --git a/app/models/organisation.rb b/app/models/organisation.rb index d0742bda6..895ca03d9 100644 --- a/app/models/organisation.rb +++ b/app/models/organisation.rb @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- class Organisation < ActiveRecord::Base include DataFormatEnumerations @@ -14,6 +13,7 @@ class Organisation < ActiveRecord::Base has_many :workbenches has_many :calendars + has_many :api_keys, class_name: 'Api::V1::ApiKey' validates_presence_of :name validates_uniqueness_of :code @@ -26,19 +26,12 @@ class Organisation < ActiveRecord::Base def self.portail_api_request conf = Rails.application.config.try(:stif_portail_api) - raise 'Rails.application.config.stif_portail_api settings is not defined' unless conf + raise 'Rails.application.config.stif_portail_api configuration is not defined' unless conf - conn = Faraday.new(:url => conf[:url]) do |c| - c.headers['Authorization'] = "Token token=\"#{conf[:key]}\"" - c.adapter Faraday.default_adapter - end - - resp = conn.get '/api/v1/organizations' - if resp.status == 200 - JSON.parse resp.body - else - raise "Error on api request status : #{resp.status} => #{resp.body}" - end + HTTPService.get_json_resource( + host: conf[:url], + path: '/api/v1/organizations', + token: conf[:key]) end def self.sync_update code, name, scope diff --git a/app/models/stop_area_referential_sync.rb b/app/models/stop_area_referential_sync.rb index 0e32df4d2..e6cf2ecbc 100644 --- a/app/models/stop_area_referential_sync.rb +++ b/app/models/stop_area_referential_sync.rb @@ -40,11 +40,11 @@ class StopAreaReferentialSync < ActiveRecord::Base end end - def create_sync_message criticity, key, message_attributs = {} + def create_sync_message criticity, key, message_attributes = {} params = { criticity: criticity, message_key: key, - message_attributs: message_attributs + message_attributes: message_attributes } stop_area_referential_sync_messages.create params end @@ -54,13 +54,13 @@ class StopAreaReferentialSync < ActiveRecord::Base create_sync_message :info, :pending end - def log_successful message_attributs + def log_successful message_attributes update_attribute(:ended_at, Time.now) - create_sync_message :info, :successful, message_attributs + create_sync_message :info, :successful, message_attributes end - def log_failed message_attributs + def log_failed message_attributes update_attribute(:ended_at, Time.now) - create_sync_message :error, :failed, message_attributs + create_sync_message :error, :failed, message_attributes end end diff --git a/app/models/user.rb b/app/models/user.rb index c2aa14bda..37d35209a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -41,19 +41,12 @@ class User < ActiveRecord::Base def self.portail_api_request conf = Rails.application.config.try(:stif_portail_api) - raise 'Rails.application.config.stif_portail_api settings is not defined' unless conf + raise 'Rails.application.config.stif_portail_api configuration is not defined' unless conf - conn = Faraday.new(:url => conf[:url]) do |c| - c.headers['Authorization'] = %{Token token="#{conf[:key]}"} - c.adapter Faraday.default_adapter - end - - resp = conn.get '/api/v1/users' - if resp.status == 200 - JSON.parse resp.body - else - raise "Error on api request status : #{resp.status} => #{resp.body}" - end + HTTPService.get_json_resource( + host: conf[:url], + path: '/api/v1/users', + token: conf[:key]) end def self.portail_sync diff --git a/app/models/workbench.rb b/app/models/workbench.rb index 4023c221b..c37cba858 100644 --- a/app/models/workbench.rb +++ b/app/models/workbench.rb @@ -9,6 +9,7 @@ class Workbench < ActiveRecord::Base has_many :group_of_lines, through: :line_referential has_many :stop_areas, through: :stop_area_referential has_many :imports + has_many :workbench_imports validates :name, presence: true validates :organisation, presence: true diff --git a/app/models/workbench_import.rb b/app/models/workbench_import.rb new file mode 100644 index 000000000..9323bd4b5 --- /dev/null +++ b/app/models/workbench_import.rb @@ -0,0 +1,2 @@ +class WorkbenchImport < Import +end diff --git a/app/policies/api_key_policy.rb b/app/policies/api_key_policy.rb new file mode 100644 index 000000000..7b4c22e33 --- /dev/null +++ b/app/policies/api_key_policy.rb @@ -0,0 +1,19 @@ +class ApiKeyPolicy < ApplicationPolicy + class Scope < Scope + def resolve + scope + end + end + + def destroy? + organisation_match? && user.has_permission?('api_keys.destroy') + end + + def create? + organisation_match? && user.has_permission?('api_keys.create') + end + + def update? + organisation_match? && user.has_permission?('api_keys.update') + end +end diff --git a/app/policies/import_policy.rb b/app/policies/import_policy.rb new file mode 100644 index 000000000..9e1d99a66 --- /dev/null +++ b/app/policies/import_policy.rb @@ -0,0 +1,7 @@ +class ImportPolicy < ApplicationPolicy + class Scope < Scope + def resolve + scope + end + end +end diff --git a/app/policies/time_table_combination_policy.rb b/app/policies/time_table_combination_policy.rb new file mode 100644 index 000000000..daa6808e4 --- /dev/null +++ b/app/policies/time_table_combination_policy.rb @@ -0,0 +1,12 @@ +class TimeTableCombinationPolicy < ApplicationPolicy + + class Scope < Scope + def resolve + scope + end + end + + def create? + !archived? && organisation_match? && user.has_permission?('time_tables.update') + end +end diff --git a/app/services/http_service.rb b/app/services/http_service.rb new file mode 100644 index 000000000..ae7d0e413 --- /dev/null +++ b/app/services/http_service.rb @@ -0,0 +1,45 @@ +module HTTPService extend self + + Timeout = Faraday::TimeoutError + + def get_resource(host:, path:, token: nil, params: {}) + Faraday.new(url: host) do |c| + c.headers['Authorization'] = "Token token=#{token.inspect}" if token + c.adapter Faraday.default_adapter + + return c.get path, params + end + end + + def get_json_resource(host:, path:, token: nil, params: {}) + # Stupid Ruby!!! (I mean I just **need** Pattern Matching, maybe I need to write it myself :O) + resp = get_resource(host: host, path: path, token: token, params: params) + if resp.status == 200 + return JSON.parse(resp.body) + else + raise "Error on api request status : #{resp.status} => #{resp.body}" + end + end + + # host: 'http://localhost:3000', + # path: '/api/v1/netex_imports.json', + # token: '13-74009c36638f587c9eafb1ce46e95585', + # params: { netex_import: {referential_id: 13, workbench_id: 1}}, + # upload: {file: [StringIO.new('howdy'), 'application/zip', 'greeting']}) + def post_resource(host:, path:, token: nil, params: {}, upload: nil) + Faraday.new(url: host) do |c| + c.headers['Authorization'] = "Token token=#{token.inspect}" if token + c.request :multipart + c.request :url_encoded + c.adapter Faraday.default_adapter + + if upload + name = upload.keys.first + value, mime_type, as_name = upload.values.first + params.update( name => Faraday::UploadIO.new(value, mime_type, as_name ) ) + end + + return c.post path, params + end + end +end diff --git a/app/services/parent_import_notifier.rb b/app/services/parent_import_notifier.rb new file mode 100644 index 000000000..47e6755e4 --- /dev/null +++ b/app/services/parent_import_notifier.rb @@ -0,0 +1,15 @@ +class ParentImportNotifier + def self.notify_when_finished(imports = nil) + imports ||= imports_pending_notification + imports.each(&:notify_parent) + end + + def self.imports_pending_notification + Import + .where( + notified_parent_at: nil, + status: Import.finished_statuses + ) + .where.not(parent: nil) + end +end diff --git a/app/services/retry_service.rb b/app/services/retry_service.rb new file mode 100644 index 000000000..21b1def36 --- /dev/null +++ b/app/services/retry_service.rb @@ -0,0 +1,54 @@ +require 'result' + +class RetryService + + Retry = Class.new(RuntimeError) + + # @param@ delays: + # An array of delays that are used to retry after a sleep of the indicated + # value in case of failed exceutions. + # Once this array is exhausted the executen fails permanently + # + # @param@ rescue_from: + # During execution all the excpetions from this array +plus RetryService::Retry+ are rescued from and + # trigger just another retry after a `sleep` as indicated above. + # + # @param@ block: + # This optional code is excuted before each retry, it is passed the result of the failed attempt, thus + # an `Exception` and the number of execution already tried. + def initialize( delays: [], rescue_from: [], &blk ) + @intervals = delays + @registered_exceptions = Array(rescue_from) << Retry + @failure_callback = blk + end + + # @param@ blk: + # The code to be executed it will be retried goverened by the `delay` passed into the initializer + # as described there in case it fails with one of the predefined exceptions or `RetryService::Retry` + # + # Eventually it will return a `Result` object. + def execute &blk + result = execute_protected blk + return result if result.ok? + @intervals.each_with_index do | interval, retry_count | + sleep interval + @failure_callback.try(:call, result.value, retry_count + 1) + result = execute_protected blk + return result if result.ok? + end + result + end + + + private + + def execute_protected blk + Result.ok(blk.()) + rescue Exception => e + if @registered_exceptions.any?{ |re| e.is_a? re } + Result.error(e) + else + raise + end + end +end diff --git a/app/services/zip_service.rb b/app/services/zip_service.rb new file mode 100644 index 000000000..778bfd06d --- /dev/null +++ b/app/services/zip_service.rb @@ -0,0 +1,55 @@ +class ZipService + + attr_reader :current_entry, :zip_data + + def initialize data + @zip_data = data + @current_entry = nil + end + + class << self + def convert_entries entries + -> output_stream do + entries.each do |e| + output_stream.put_next_entry e.name + output_stream.write e.get_input_stream.read + end + end + end + + def entries input_stream + Enumerator.new do |enum| + loop{ enum << input_stream.get_next_entry } + end.lazy.take_while{ |e| e } + end + end + + def entry_groups + self.class.entries(input_stream).group_by(&method(:entry_key)) + end + + def entry_group_streams + entry_groups.map(&method(:make_stream)).to_h + end + + def entry_key entry + entry.name.split('/', -1)[-2] + end + + def make_stream pair + name, entries = pair + [name, make_stream_from( entries )] + end + + def make_stream_from entries + Zip::OutputStream.write_buffer(&self.class.convert_entries(entries)) + end + + def next_entry + @current_entry = input_stream.get_next_entry + end + + def input_stream + @__input_stream__ ||= Zip::InputStream.open(StringIO.new(zip_data)) + end +end diff --git a/app/views/api/v1/imports/index.rabl b/app/views/api/v1/imports/index.rabl new file mode 100644 index 000000000..e8cfd101e --- /dev/null +++ b/app/views/api/v1/imports/index.rabl @@ -0,0 +1,3 @@ +collection @imports + +extends "api/v1/imports/show" diff --git a/app/views/api/v1/imports/show.rabl b/app/views/api/v1/imports/show.rabl new file mode 100644 index 000000000..180894cb8 --- /dev/null +++ b/app/views/api/v1/imports/show.rabl @@ -0,0 +1,6 @@ +object @import + +attributes :id, :name, :status +node :referential_ids do |i| + i.workbench.referentials.map(&:id) +end diff --git a/app/views/api/v1/netex_imports/create.json.rabl b/app/views/api/v1/netex_imports/create.json.rabl new file mode 100644 index 000000000..f37703349 --- /dev/null +++ b/app/views/api/v1/netex_imports/create.json.rabl @@ -0,0 +1,3 @@ + +object @netex_import +attributes :id, :workbench_id, :referential_id diff --git a/app/views/api/v1/workbenches/index.rabl b/app/views/api/v1/workbenches/index.rabl new file mode 100644 index 000000000..2f0bf5fee --- /dev/null +++ b/app/views/api/v1/workbenches/index.rabl @@ -0,0 +1,3 @@ +collection @workbenches + +extends "api/v1/workbenches/show" diff --git a/app/views/api/v1/workbenches/show.rabl b/app/views/api/v1/workbenches/show.rabl new file mode 100644 index 000000000..d43727809 --- /dev/null +++ b/app/views/api/v1/workbenches/show.rabl @@ -0,0 +1,3 @@ +object @workbench + +attributes :id, :name diff --git a/app/views/api_keys/_form.html.slim b/app/views/api_keys/_form.html.slim index 74b806677..f3ebf3fe1 100644 --- a/app/views/api_keys/_form.html.slim +++ b/app/views/api_keys/_form.html.slim @@ -1,10 +1,7 @@ -= semantic_form_for [@referential, @api_key] do |form| - = form.inputs do - = form.input :name - += simple_form_for @api_key, url: action_url do |f| + = f.input :name - unless @api_key.new_record? - = form.input :token, :input_html => { :readonly => true } + = f.input :token, :input_html => { readonly: true } - = form.actions do - = form.action :submit, as: :button - = form.action :cancel, as: :link
\ No newline at end of file + = f.association :referential + = f.button :submit, 'submit', class: 'btn-primary' diff --git a/app/views/api_keys/edit.html.slim b/app/views/api_keys/edit.html.slim index 110f0775d..e47deddf7 100644 --- a/app/views/api_keys/edit.html.slim +++ b/app/views/api_keys/edit.html.slim @@ -1,3 +1,2 @@ = title_tag t('api_keys.edit.title') - -== render 'form'
\ No newline at end of file +== render partial: 'form', locals: {action_url: organisation_api_key_path} diff --git a/app/views/api_keys/index.html.slim b/app/views/api_keys/index.html.slim new file mode 100644 index 000000000..fc8d95c7a --- /dev/null +++ b/app/views/api_keys/index.html.slim @@ -0,0 +1,24 @@ +- header_params = ['map-marker', + t('.title'), + ''] +- header_params << link_to(t('actions.add'), new_organisation_api_key_path, class: 'btn btn-default') if policy(Api::V1::ApiKey).create? += pageheader(*header_params) do + + +- if @api_keys.any? + .row + .col-lg-12 + = table_builder_2 @api_keys, + [ \ + TableBuilderHelper::Column.new( \ + key: :name, \ + attribute: 'name' \ + ), \ + TableBuilderHelper::Column.new( \ + key: :token, \ + attribute: 'token' \ + ), \ + ], + cls: 'table has-search' + + = new_pagination @api_keys, 'pull-right' diff --git a/app/views/api_keys/new.html.slim b/app/views/api_keys/new.html.slim index f7b1dd99b..291c9f8a6 100644 --- a/app/views/api_keys/new.html.slim +++ b/app/views/api_keys/new.html.slim @@ -1,3 +1,2 @@ = title_tag t('api_keys.new.title') - -== render "form"
\ No newline at end of file +== render partial: 'form', locals: {action_url: organisation_api_keys_path} diff --git a/app/views/api_keys/show.html.slim b/app/views/api_keys/show.html.slim index b65717408..de30ac125 100644 --- a/app/views/api_keys/show.html.slim +++ b/app/views/api_keys/show.html.slim @@ -12,7 +12,6 @@ - content_for :sidebar do ul.actions - li = link_to t('api_keys.actions.new'), new_referential_api_key_path(@referential), class: "add" - li = link_to t('api_keys.actions.edit'), edit_referential_api_key_path(@referential, @api_key), class: "edit" - li = link_to t('api_keys.actions.destroy'), referential_api_key_path(@referential, @api_key), :method => :delete, :data => {:confirm => t('api_keys.actions.destroy_confirm')}, class: "remove" - br
\ No newline at end of file + li = link_to t('api_keys.actions.edit'), edit_organisation_api_key_path(@api_key), class: "edit" + li = link_to t('api_keys.actions.destroy'), organisation_api_key_path(@api_key), :method => :delete, :data => {:confirm => t('api_keys.actions.destroy_confirm')}, class: "remove" + br diff --git a/app/views/imports/_filters.html.slim b/app/views/imports/_filters.html.slim new file mode 100644 index 000000000..99fcb0232 --- /dev/null +++ b/app/views/imports/_filters.html.slim @@ -0,0 +1,21 @@ += search_form_for @q, url: workbench_imports_path(@workbench), html: { method: :get, class: 'form form-filter' } do |f| + .ffg-row + .input-group.search_bar + = f.search_field :name_or_creator_cont, class: 'form-control', placeholder: t('imports.filters.name_or_creator_cont') + span.input-group-btn + button.btn.btn-default#search_btn type='submit' + span.fa.fa-search + + .ffg-row + .form-group.togglable + = f.label Import.human_attribute_name(:status), required: false, class: 'control-label' + = f.input :status_eq_any, collection: @imports.map(&:status).uniq.compact, as: :check_boxes, label: false, label_method: lambda{|l| ("<span>" + l + "</span>").html_safe}, required: false, wrapper_html: { class: 'checkbox_list'} + + .form-group.togglable + = f.label Import.human_attribute_name(:started_at), required: false, class: 'control-label' + .filter_menu + = f.input :started_at_eq, as: :date, label: false, wrapper_html: { class: 'date smart_date filter_menu-item' }, include_blank: true + + .actions + = link_to t('actions.erase'), workbench_imports_path(@workbench), class: 'btn btn-link' + = f.submit t('actions.filter'), class: 'btn btn-default' diff --git a/app/views/imports/_form.html.slim b/app/views/imports/_form.html.slim index b795e908f..3ec22415f 100644 --- a/app/views/imports/_form.html.slim +++ b/app/views/imports/_form.html.slim @@ -1,6 +1,22 @@ -= simple_form_for import, as: :import, url: workbench_imports_path(workbench) do |f| - = f.input :name - = f.input :file - = f.association :referential, collection: workbench.referentials - = f.input :type, as: :hidden - = f.button :submit += simple_form_for import, as: :import, url: workbench_imports_path(workbench), html: {class: 'form-horizontal', id: 'wb_import_form'}, wrapper: :horizontal_form do |form| + + .row + .col-lg-12 + = form.input :name + + .row + .col-lg-12 + .form-group + = form.label :file, t('activerecord.attributes.import.resources'), class: 'control-label col-sm-4 col-xs-5' + .col-sm-8.col-xs-7 + = form.input_field :file, label: false, class: 'form-control' + + .separator + + .row + .col-lg-12 + = form.association :referential, collection: workbench.referentials, input_html: { 'data-select2ed': 'true', 'data-select2ed-placeholder': t('imports.filters.referential') }, label: t('activerecord.attributes.import.references_type'), label_method: :name, wrapper_html: { class: 'select2ed'} + = form.input :type, as: :hidden + + + = form.button :submit, t('actions.submit'), class: 'btn btn-default formSubmitr', form: 'wb_import_form' diff --git a/app/views/imports/index.html.slim b/app/views/imports/index.html.slim index 6e2d49f73..79e464490 100644 --- a/app/views/imports/index.html.slim +++ b/app/views/imports/index.html.slim @@ -1,12 +1,46 @@ -= title_tag t('.title') -- @imports.each do |import| - .import - li = link_to import.name, workbench_import_path(@workbench, import) - li = import.referential.name if import.referential - li = link_to import.file.file.filename, import.file.url, target: :_blank - hr - -.warning = t('.warning') -- content_for :sidebar do - ul.actions - li = link_to t('imports.actions.new'), new_workbench_import_path(workbench_id: @workbench), class: 'add' +/ PageHeader += pageheader 'map-marker', + t('.title'), + '', + '', + link_to(t('imports.actions.new'), new_workbench_import_path(workbench_id: @workbench), class: 'btn btn-primary') + +/ PageContent +.page_content + .container-fluid + - if params[:q].present? or @imports.any? + .row + .col-lg-12 + = render 'filters' + + - if @imports.any? + .row + .col-lg-12 + = table_builder_2 @imports, + [ \ + TableBuilderHelper::Column.new( \ + key: :status, \ + attribute: 'status' \ + ), \ + TableBuilderHelper::Column.new( \ + key: :started_at, \ + attribute: 'started_at' \ + ), \ + TableBuilderHelper::Column.new( \ + key: :name, \ + attribute: 'name' \ + ), \ + TableBuilderHelper::Column.new( \ + key: :creator, \ + attribute: 'creator' \ + ) \ + ], + links: [], + cls: 'table has-search' + + = new_pagination @imports, 'pull-right' + + - unless @imports.any? + .row.mt-xs + .col-lg-12 + = replacement_msg t('imports.search_no_results') diff --git a/app/views/imports/new.html.slim b/app/views/imports/new.html.slim index 55b655a85..2a18c41f2 100644 --- a/app/views/imports/new.html.slim +++ b/app/views/imports/new.html.slim @@ -1,4 +1,10 @@ -= title_tag 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', import: @import, workbench: @workbench +/ PageHeader += pageheader 'map-marker', + t('.title') + +/ 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', import: @import, workbench: @workbench diff --git a/app/views/imports/show.html.slim b/app/views/imports/show.html.slim index b40e11ea4..65e3f33ff 100644 --- a/app/views/imports/show.html.slim +++ b/app/views/imports/show.html.slim @@ -1,14 +1,126 @@ -.title.row - .col-md-8 - = title_tag job_status_title(@import) +/ PageHeader += pageheader 'map-marker', + @import.name, + '', + t('last_update', time: l(@import.updated_at, format: :short)) do + + / Below is secundary actions & optional contents (filters, ...) + .row + .col-lg-12.text-right.mb-sm + - @import.action_links.each do |link| + = link_to link.href, + method: link.method, + data: link.data, + class: 'btn btn-primary' do + = link.content -.import_show - .links - = link_to font_awesome_classic_tag("fa-file-#{@import.file.file.extension}-o") + t("imports.show.imported_file"), @import.file.url +/ PageContent +.page_content + .container-fluid + .row + .col-lg-6.col-md-6.col-sm-12.col-xs-12 + = definition_list t('metadatas'), { 'Récupération des données' => '-', "Nom de l'archive" => @import.try(:file_identifier)} -- content_for :sidebar do - ul.actions - li - = link_to t('imports.actions.destroy'), workbench_import_path(@workbench, @import.id), method: :delete, data: {confirm: t('imports.actions.destroy_confirm')}, class: 'remove' + / .row + / .col-lg-12 + / = table_builder_2 Import.all, + / [ \ + / TableBuilderHelper::Column.new( \ + / key: :status, \ + / attribute: 'status' \ + / ), \ + / TableBuilderHelper::Column.new( \ + / key: :started_at, \ + / attribute: 'started_at' \ + / ), \ + / TableBuilderHelper::Column.new( \ + / key: :name, \ + / attribute: 'name' \ + / ), \ + / TableBuilderHelper::Column.new( \ + / key: :creator, \ + / attribute: 'creator' \ + / ) \ + / ], + / links: [], + / cls: 'table', + / overhead: [ \ + / {}, \ + / { \ + / title: 'Lorem ipsum dolor sit amet', \ + / width: 1, \ + / cls: 'overheaded-danger full-border' \ + / }, { \ + / title: 'Toto <span title="Lorem ipsum..." class="fa fa-lg fa-info-circle text-info"></span>', \ + / width: 2, \ + / cls: 'overheaded-default' \ + / } \ + / ] - = history_tag(@import) + .row + .col-lg-12 + / TMP static table + table.table + thead + tr + th Nom du jeu de données + th Conformité Netex + th Contrôle STIF + th Contrôle organisation + th + + tbody + tr + td Nom JDD #1 + td.text-center + span.fa.fa-circle.text-success + td.text-center + span.fa.fa-circle.text-danger + td.text-center - + td.actions + .btn-group + .btn.dropdown-toggle data-toggle='dropdown' + span.fa.fa-cog + + ul.dropdown-menu + li + = link_to 'Rapport de contrôle STIF', '#' + li + = link_to 'Rapport de contrôle Orga.', '#' + + tr + td Nom JDD #2 + td.text-center + span.fa.fa-circle.text-warning + td.text-center + span.fa.fa-circle.text-warning + td.text-center - + td.actions + .btn-group + .btn.dropdown-toggle data-toggle='dropdown' + span.fa.fa-cog + + ul.dropdown-menu + li + = link_to 'Rapport de contrôle STIF', '#' + li + = link_to 'Rapport de contrôle Orga.', '#' + + tr + td Nom JDD #3 + td.text-center + span.fa.fa-circle.text-danger + td.text-center + span.fa.fa-circle.text-danger + + td.text-center - + td.actions + .btn-group + .btn.dropdown-toggle data-toggle='dropdown' + span.fa.fa-cog + + ul.dropdown-menu + li + = link_to 'Rapport de contrôle STIF', '#' + li + = link_to 'Rapport de contrôle Orga.', '#' diff --git a/app/views/layouts/navigation/_main_nav_left.html.slim b/app/views/layouts/navigation/_main_nav_left.html.slim index 8e82ac528..74442692d 100644 --- a/app/views/layouts/navigation/_main_nav_left.html.slim +++ b/app/views/layouts/navigation/_main_nav_left.html.slim @@ -33,8 +33,14 @@ .list-group = link_to '#', class: "list-group-item #{params[:controller] == 'workbenches' ? 'active' : ''}" do span Jeux de données - = link_to '#', class: 'list-group-item' do - span Import + + - if @workbench + = link_to workbench_imports_path(@workbench), class: "list-group-item #{(params[:controller] == 'imports') ? 'active' : ''}" do + span Import + - else + = link_to '#', class: 'list-group-item disabled' do + span Import + = link_to calendars_path, class: 'list-group-item' do span Modèles de calendrier = link_to '#', class: 'list-group-item' do diff --git a/app/views/line_referentials/show.html.slim b/app/views/line_referentials/show.html.slim index 95c2c02b0..e2381e7e9 100644 --- a/app/views/line_referentials/show.html.slim +++ b/app/views/line_referentials/show.html.slim @@ -44,6 +44,6 @@ td.text-center .fa.fa-circle class="text-#{criticity_class(log.criticity)}" td - - data = log.message_attributs.symbolize_keys! + - data = log.message_attributes.symbolize_keys! - data[:processing_time] = distance_of_time_in_words(data[:processing_time].to_i) - = t("line_referential_sync.message.#{log.message_key}", log.message_attributs.symbolize_keys!).html_safe + = t("line_referential_sync.message.#{log.message_key}", log.message_attributes.symbolize_keys!).html_safe diff --git a/app/views/networks/index.html.slim b/app/views/networks/index.html.slim index 4c1f9783c..1478c3e09 100644 --- a/app/views/networks/index.html.slim +++ b/app/views/networks/index.html.slim @@ -40,6 +40,6 @@ = new_pagination @networks, 'pull-right' - unless @networks.any? - .row + .row.mt-xs .col-lg-12 = replacement_msg t('networks.search_no_results') diff --git a/app/views/networks/show.html.slim b/app/views/networks/show.html.slim index 09edbad2e..5726daa2b 100644 --- a/app/views/networks/show.html.slim +++ b/app/views/networks/show.html.slim @@ -1,7 +1,7 @@ / PageHeader = pageheader 'map-marker', @network.name, - 'Lorem ipsum dolor sit amet', + '', t('last_update', time: l(@network.updated_at, format: :short)) do / Below is secundary actions & optional contents (filters, ...) diff --git a/app/views/stop_area_referentials/show.html.slim b/app/views/stop_area_referentials/show.html.slim index 56ecbf6da..0fca15fff 100644 --- a/app/views/stop_area_referentials/show.html.slim +++ b/app/views/stop_area_referentials/show.html.slim @@ -37,6 +37,6 @@ td.text-center .fa.fa-circle class="text-#{criticity_class(log.criticity)}" td - - data = log.message_attributs.symbolize_keys! + - data = log.message_attributes.symbolize_keys! - data[:processing_time] = distance_of_time_in_words(data[:processing_time].to_i) - = t("stop_area_referential_sync.message.#{log.message_key}", log.message_attributs.symbolize_keys!).html_safe + = t("stop_area_referential_sync.message.#{log.message_key}", log.message_attributes.symbolize_keys!).html_safe diff --git a/app/views/time_table_combinations/_form.html.slim b/app/views/time_table_combinations/_form.html.slim index b4f818828..581f00457 100644 --- a/app/views/time_table_combinations/_form.html.slim +++ b/app/views/time_table_combinations/_form.html.slim @@ -7,7 +7,7 @@ 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 col-xs-7' } - = 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 :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, :source_id => @combination.source_id)}}, 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', 'select2ed-placeholder': 'Indiquez un modèle de calendrier...', term: 'name_cont', url: autocomplete_calendars_path}}, wrapper_html: {class: @combination.combined_type != 'calendar' ? 'hidden' : ''} diff --git a/app/views/time_tables/edit.html.slim b/app/views/time_tables/edit.html.slim index f129cd63a..a2dfb90f9 100644 --- a/app/views/time_tables/edit.html.slim +++ b/app/views/time_tables/edit.html.slim @@ -10,6 +10,7 @@ #periods = javascript_tag do - | window.actionType = "#{raw params[:action]}" + | window.actionType = "#{raw params[:action]}"; + | window.I18n = #{(I18n.backend.send(:translations).to_json).html_safe}; = javascript_include_tag 'es6_browserified/time_tables/index.js' diff --git a/app/workers/workbench_import_worker.rb b/app/workers/workbench_import_worker.rb new file mode 100644 index 000000000..7f77b46dc --- /dev/null +++ b/app/workers/workbench_import_worker.rb @@ -0,0 +1,118 @@ +class WorkbenchImportWorker + include Sidekiq::Worker + include Rails.application.routes.url_helpers + include Configurable + + RETRY_DELAYS = [3, 5, 8] + + # Workers + # ======= + + def perform(import_id) + @workbench_import = WorkbenchImport.find(import_id) + @response = nil + @workbench_import.update_attributes(status: 'running') + downloaded = download + zip_service = ZipService.new(downloaded) + upload zip_service + end + + def download + logger.info "HTTP GET #{import_url}" + @zipfile_data = HTTPService.get_resource( + host: import_host, + path: import_path, + params: {token: @workbench_import.token_download}).body + end + + def execute_post eg_name, eg_stream + logger.info "HTTP POST #{export_url} (for #{complete_entry_group_name(eg_name)})" + HTTPService.post_resource( + host: export_host, + path: export_path, + token: token(eg_name), + params: params, + upload: {file: [eg_stream, 'application/zip', eg_name]}) + end + + def log_failure reason, count + logger.warn "HTTP POST failed with #{reason}, count = #{count}, response=#{@response}" + end + + def try_again + raise RetryService::Retry + end + + def try_upload_entry_group eg_name, eg_stream + result = execute_post eg_name, eg_stream + return result if result && result.status < 400 + @response = result.body + try_again + end + + def upload zip_service + entry_group_streams = zip_service.entry_group_streams + @workbench_import.update_attributes total_steps: entry_group_streams.size + entry_group_streams.each_with_index(&method(:upload_entry_group)) + rescue StopIteration + @workbench_import.update_attributes( current_step: entry_group_streams.size, status: 'failed' ) + end + + def upload_entry_group entry_pair, element_count + @workbench_import.update_attributes( current_step: element_count.succ ) + retry_service = RetryService.new( + delays: RETRY_DELAYS, + rescue_from: [HTTPService::Timeout], + &method(:log_failure)) + status = retry_service.execute(&upload_entry_group_proc(entry_pair)) + raise StopIteration unless status.ok? + end + + def upload_entry_group_proc entry_pair + eg_name, eg_stream = entry_pair + # This should be fn.try_upload_entry_group(eg_name, eg_stream) ;( + -> do + try_upload_entry_group(eg_name, eg_stream) + end + end + + + + # Queries + # ======= + + def complete_entry_group_name entry_group_name + [@workbench_import.name, entry_group_name].join("--") + end + + def token entry_group_name + Api::V1::ApiKey.from(@workbench_import.referential, name: complete_entry_group_name(entry_group_name)).token + end + + # Constants + # ========= + + def export_host + Rails.application.config.rails_host + end + def export_path + api_v1_netex_imports_path(format: :json) + end + def export_url + @__export_url__ ||= File.join(export_host, export_path) + end + + def import_host + Rails.application.config.rails_host + end + def import_path + @__import_path__ ||= download_workbench_import_path(@workbench_import.workbench, @workbench_import) + end + def import_url + @__import_url__ ||= File.join(import_host, import_path) + end + + def params + @__params__ ||= { netex_import: { referential_id: @workbench_import.referential_id, workbench_id: @workbench_import.workbench_id } } + end +end diff --git a/config/application.rb b/config/application.rb index 910ddd983..05a9752b6 100644 --- a/config/application.rb +++ b/config/application.rb @@ -14,7 +14,7 @@ module ChouetteIhm # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. - config.autoload_paths << config.root.join("lib") + config.autoload_paths << config.root.join('lib') # custom exception pages config.exceptions_app = self.routes diff --git a/config/deploy.rb b/config/deploy.rb index 4ab888e92..fdd0b1d1d 100644 --- a/config/deploy.rb +++ b/config/deploy.rb @@ -24,7 +24,7 @@ require "bundler/capistrano" require 'whenever/capistrano' require 'capistrano/npm' -set :npm_options, '--production --silent --no-progress' +set :npm_options, '--production --no-progress' after 'deploy:finalize_update', 'npm:install' diff --git a/config/environments/development.rb b/config/environments/development.rb index 59cb9eefa..56773d81e 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -61,15 +61,17 @@ Rails.application.configure do config.reflex_api_url = "https://pprod.reflex.stif.info/ws/reflex/V1/service=getData" config.codifligne_api_url = "https://pprod.codifligne.stif.info/rest/v1/lc/getlist" - # config.chouette_authentication_settings = { - # type: "database" - # } - config.chouette_authentication_settings = { - type: "cas", - cas_server: "http://stif-portail-dev.af83.priv/sessions" - } - config.stif_portail_api = - { + if Rails.env.development? && ENV['NO_VPN'] + config.chouette_authentication_settings = { + type: "database" + } + else + config.chouette_authentication_settings = { + type: "cas", + cas_server: "http://stif-portail-dev.af83.priv/sessions" + } + end + config.stif_portail_api = { key: "Ohphie1Voo6the5hohpi", url: "http://stif-portail-dev.af83.priv" } @@ -80,7 +82,8 @@ Rails.application.configure do config.portal_url = "http://stif-boiv-staging.af83.priv" # IEV url - config.iev_url = "localhost:8080" + config.iev_url = ENV.fetch('IEV_URL', 'http://localhost:8080') + config.rails_host = ENV.fetch('RAILS_HOST', 'http://localhost:3000') # file to data for demo config.demo_data = "tmp/demo.zip" diff --git a/config/environments/production.rb b/config/environments/production.rb index 19fc23024..794c8bd88 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -141,6 +141,7 @@ Rails.application.configure do # IEV config.iev_url = "http://worker-server:8080" + config.rails_host = ENV.fetch('RAILS_HOST') # Set node env for browserify-rails config.browserify_rails.node_env = "production" diff --git a/config/environments/test.rb b/config/environments/test.rb index a6db12006..b3312be4a 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -62,6 +62,7 @@ Rails.application.configure do # Reflex api url config.reflex_api_url = "https://195.46.215.128/ws/reflex/V1/service=getData" + config.rails_host = "http://www.example.com" # file to data for demo config.demo_data = "tmp/demo.zip" diff --git a/config/initializers/apartment.rb b/config/initializers/apartment.rb index 29ce6564f..e1e86449c 100644 --- a/config/initializers/apartment.rb +++ b/config/initializers/apartment.rb @@ -18,34 +18,35 @@ Apartment.configure do |config| # config.excluded_models = %w{Tenant} # config.excluded_models = [ - "Referential", - "ReferentialMetadata", - "Organisation", - "User", - "Api::V1::ApiKey", - "RuleParameterSet", - "StopAreaReferential", - "StopAreaReferentialMembership", - "StopAreaReferentialSync", - "StopAreaReferentialSyncMessage", - "Chouette::StopArea", - "LineReferential", - "LineReferentialMembership", - "LineReferentialSync", - "LineReferentialSyncMessage", - "Chouette::Line", - "Chouette::GroupOfLine", - "Chouette::Company", - "Chouette::Network", - "ReferentialCloning", - "Workbench", - "CleanUp", - "CleanUpResult", - "Calendar", - "Import", - "NetexImport", - "ImportMessage", - "ImportResource" + 'Referential', + 'ReferentialMetadata', + 'Organisation', + 'User', + 'Api::V1::ApiKey', + 'RuleParameterSet', + 'StopAreaReferential', + 'StopAreaReferentialMembership', + 'StopAreaReferentialSync', + 'StopAreaReferentialSyncMessage', + 'Chouette::StopArea', + 'LineReferential', + 'LineReferentialMembership', + 'LineReferentialSync', + 'LineReferentialSyncMessage', + 'Chouette::Line', + 'Chouette::GroupOfLine', + 'Chouette::Company', + 'Chouette::Network', + 'ReferentialCloning', + 'Workbench', + 'CleanUp', + 'CleanUpResult', + 'Calendar', + 'Import', + 'NetexImport', + 'WorkbenchImport', + 'ImportMessage', + 'ImportResource' ] # use postgres schemas? diff --git a/config/initializers/workbench_import.rb b/config/initializers/workbench_import.rb new file mode 100644 index 000000000..89ddd72ef --- /dev/null +++ b/config/initializers/workbench_import.rb @@ -0,0 +1,5 @@ +WorkbenchImportWorker.config do | config | + config.dir = ENV.fetch('WORKBENCH_IMPORT_DIR'){ Rails.root.join 'tmp/workbench_import' } + + FileUtils.mkdir_p config.dir +end diff --git a/config/locales/api_keys.en.yml b/config/locales/api_keys.en.yml index 221fa6eef..1480c8e55 100644 --- a/config/locales/api_keys.en.yml +++ b/config/locales/api_keys.en.yml @@ -7,12 +7,14 @@ en: destroy_confirm: "Are you sure you want destroy this api key?" show: title: "Api key" + index: + title: Api key new: title: "Add a new api key" edit: title: "Update api key" - activerecord: - models: + activerecord: + models: api_key: "Api Key" attributes: api_key: diff --git a/config/locales/api_keys.fr.yml b/config/locales/api_keys.fr.yml index 445367c72..20af91a49 100644 --- a/config/locales/api_keys.fr.yml +++ b/config/locales/api_keys.fr.yml @@ -7,6 +7,8 @@ fr: destroy_confirm: "Etes vous sûr de vouloir détruire la clé d'accès API ?" show: title: "Clé d'accès API" + index: + title: Clé d'accès API new: title: "Ajouter une clé d'accès API" edit: diff --git a/config/locales/imports.en.yml b/config/locales/imports.en.yml index b20f0f1da..b92b8843f 100644 --- a/config/locales/imports.en.yml +++ b/config/locales/imports.en.yml @@ -1,7 +1,13 @@ en: imports: + search_no_results: "No import matching your query" + filters: + referential: "Select data space..." + name_or_creator_cont: "Select an import or creator name..." actions: new: "New import" + show: "Import report" + download: "Download original file" destroy: "Destroy" destroy_confirm: "Are you sure you want destroy this import?" index: @@ -50,7 +56,10 @@ en: import: resources: "File to import" created_at: "Created on" + started_at: Started at + name: Name status: "Status" + creator: "Creator" references_type: "Data to be imported" no_save: "No save" rule_parameter_set_id: "Rule parameter set for compliance check" diff --git a/config/locales/imports.fr.yml b/config/locales/imports.fr.yml index 933025c82..f7bf8c178 100644 --- a/config/locales/imports.fr.yml +++ b/config/locales/imports.fr.yml @@ -1,7 +1,13 @@ fr: imports: + search_no_results: "Aucun import ne correspond à votre recherche" + filters: + referential: "Sélectionnez un jeu de données..." + name_or_creator_cont: "Indiquez un nom d'import ou d'opérateur..." actions: new: "Nouvel import" + show: "Rapport d'import" + download: "Téléch. fichier source" destroy: "Supprimer cet import" destroy_confirm: "Etes vous sûr de supprimer cet import ?" index: @@ -50,7 +56,10 @@ fr: import: resources: "Fichier à importer" created_at: "Créé le" - status: "Status" + started_at: Démarrage + name: "Nom de l'import" + status: "Etat" + creator: "Opérateur" no_save: "Pas de sauvegarde" references_type: "Données à importer" rule_parameter_set_id: "Jeu de paramètres pour validation" diff --git a/config/locales/time_tables.en.yml b/config/locales/time_tables.en.yml index ed2f9758e..d67e30edb 100644 --- a/config/locales/time_tables.en.yml +++ b/config/locales/time_tables.en.yml @@ -29,6 +29,10 @@ en: title: "Duplicate timetable" edit: title: "Update timetable %{time_table}" + error_modal: + title: "Error" + withoutPeriodsWithDaysTypes: "A timetable can't have day type(s) without period(s)." + withPeriodsWithoutDayTypes: "A tiemetable can't have period(s) swithout day type(s)." show: title: "Timetable %{time_table}" dates: "Application dates" diff --git a/config/locales/time_tables.fr.yml b/config/locales/time_tables.fr.yml index cf6888d0f..06d1d59e8 100644 --- a/config/locales/time_tables.fr.yml +++ b/config/locales/time_tables.fr.yml @@ -29,6 +29,10 @@ fr: title: "Dupliquer un calendrier" edit: title: "Editer le calendrier %{time_table}" + error_modal: + title: "Erreur" + withoutPeriodsWithDaysTypes: "Un calendrier d'application ne peut pas avoir de journée(s) d'application sans période(s)." + withPeriodsWithoutDayTypes: "Un calendrier d'application ne peut pas avoir de période(s) sans journée(s) d'application." show: title: Calendrier %{time_table} dates: "Dates d'application" diff --git a/config/routes.rb b/config/routes.rb index 28c092e6a..bf1c1cb74 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -32,29 +32,34 @@ ChouetteIhm::Application.routes.draw do namespace :api do namespace :v1 do - resources :time_tables, :only => [:index, :show] - resources :connection_links, :only => [:index, :show] - resources :companies, :only => [:index, :show] - resources :networks, :only => [:index, :show] - resources :stop_areas, :only => [:index, :show] - resources :group_of_lines, :only => [:index, :show] - resources :access_points, :only => [:index, :show] - resources :access_links, :only => [:index, :show] - resources :lines, :only => [:index, :show] do - resources :journey_patterns, :only => [:index, :show] - resources :routes, :only => [:index, :show] do - resources :vehicle_journeys, :only => [:index, :show] - resources :journey_patterns, :only => [:index, :show] - resources :stop_areas, :only => [:index, :show] + resources :workbenches, only: [:index, :show] do + resources :imports, only: [:index, :show, :create] + end + resources :access_links, only: [:index, :show] + resources :access_points, only: [:index, :show] + resources :connection_links, only: [:index, :show] + resources :companies, only: [:index, :show] + resources :group_of_lines, only: [:index, :show] + resources :netex_imports, only: :create + resources :journey_patterns, only: :show + resources :lines, only: [:index, :show] do + resources :journey_patterns, only: [:index, :show] + resources :routes, only: [:index, :show] do + resources :vehicle_journeys, only: [:index, :show] + resources :journey_patterns, only: [:index, :show] + resources :stop_areas, only: [:index, :show] end end - resources :routes, :only => :show - resources :journey_patterns, :only => :show - resources :vehicle_journeys, :only => :show + resources :networks, only: [:index, :show] + resources :routes, only: :show + resources :stop_areas, only: [:index, :show] + resources :time_tables, only: [:index, :show] + resources :vehicle_journeys, only: :show end end resource :organisation, :only => [:show, :edit, :update] do + resources :api_keys resources :users resources :rule_parameter_sets end @@ -77,7 +82,6 @@ ChouetteIhm::Application.routes.draw do end resources :referentials, except: :index do - resources :api_keys resources :autocomplete_stop_areas, only: [:show, :index] do get 'around', on: :member end @@ -180,7 +184,7 @@ ChouetteIhm::Application.routes.draw do resources :timebands resources :access_points do - resources :access_links + resources :access_links end resources :stop_areas, controller: "referential_stop_areas" do diff --git a/config/schedule.rb b/config/schedule.rb index 83c4d7388..8aa21076f 100644 --- a/config/schedule.rb +++ b/config/schedule.rb @@ -38,3 +38,7 @@ end every :day, :at => '4:00 am' do rake "codifligne:sync" end + +every 5.minutes do + rake "import:notify_parent" +end diff --git a/db/migrate/20170710125809_add_check_sum.rb b/db/migrate/20170710125809_add_check_sum.rb new file mode 100644 index 000000000..b91ddb74d --- /dev/null +++ b/db/migrate/20170710125809_add_check_sum.rb @@ -0,0 +1,13 @@ +class AddCheckSum < ActiveRecord::Migration + def change + add_column :vehicle_journey_at_stops, :checksum, :string + add_column :footnotes, :checksum, :string + add_column :routing_constraint_zones, :checksum, :string + add_column :routes, :checksum, :string + add_column :journey_patterns, :checksum, :string + add_column :vehicle_journeys, :checksum, :string + add_column :time_table_dates, :checksum, :string + add_column :time_table_periods, :checksum, :string + add_column :time_tables, :checksum, :string + end +end diff --git a/db/migrate/20170710130230_add_check_sum_source.rb b/db/migrate/20170710130230_add_check_sum_source.rb new file mode 100644 index 000000000..b8e36e954 --- /dev/null +++ b/db/migrate/20170710130230_add_check_sum_source.rb @@ -0,0 +1,13 @@ +class AddCheckSumSource < ActiveRecord::Migration + def change + add_column :vehicle_journey_at_stops, :checksum_source, :string + add_column :footnotes, :checksum_source, :string + add_column :routing_constraint_zones, :checksum_source, :string + add_column :routes, :checksum_source, :string + add_column :journey_patterns, :checksum_source, :string + add_column :vehicle_journeys, :checksum_source, :string + add_column :time_table_dates, :checksum_source, :string + add_column :time_table_periods, :checksum_source, :string + add_column :time_tables, :checksum_source, :string + end +end diff --git a/db/migrate/20170715041954_add_parent_type_and_parent_id_to_imports.rb b/db/migrate/20170715041954_add_parent_type_and_parent_id_to_imports.rb new file mode 100644 index 000000000..96c1c2a59 --- /dev/null +++ b/db/migrate/20170715041954_add_parent_type_and_parent_id_to_imports.rb @@ -0,0 +1,6 @@ +class AddParentTypeAndParentIdToImports < ActiveRecord::Migration + def change + add_column :imports, :parent_id, :bigint + add_column :imports, :parent_type, :string + end +end diff --git a/db/migrate/20170724094628_add_notified_parent_at_to_imports.rb b/db/migrate/20170724094628_add_notified_parent_at_to_imports.rb new file mode 100644 index 000000000..7c6484cfc --- /dev/null +++ b/db/migrate/20170724094628_add_notified_parent_at_to_imports.rb @@ -0,0 +1,5 @@ +class AddNotifiedParentAtToImports < ActiveRecord::Migration + def change + add_column :imports, :notified_parent_at, :datetime + end +end diff --git a/db/migrate/20170727130705_add_current_step_and_total_steps_to_import.rb b/db/migrate/20170727130705_add_current_step_and_total_steps_to_import.rb new file mode 100644 index 000000000..b31e86f17 --- /dev/null +++ b/db/migrate/20170727130705_add_current_step_and_total_steps_to_import.rb @@ -0,0 +1,6 @@ +class AddCurrentStepAndTotalStepsToImport < ActiveRecord::Migration + def change + add_column :imports, :current_step, :integer, default: 0 + add_column :imports, :total_steps, :integer, default: 0 + end +end diff --git a/db/migrate/20170802141224_rename_message_attributs_to_message_attributes_everywhere.rb b/db/migrate/20170802141224_rename_message_attributs_to_message_attributes_everywhere.rb new file mode 100644 index 000000000..f80292b14 --- /dev/null +++ b/db/migrate/20170802141224_rename_message_attributs_to_message_attributes_everywhere.rb @@ -0,0 +1,7 @@ +class RenameMessageAttributsToMessageAttributesEverywhere < ActiveRecord::Migration + def change + rename_column :import_messages, :message_attributs, :message_attributes + rename_column :line_referential_sync_messages, :message_attributs, :message_attributes + rename_column :stop_area_referential_sync_messages, :message_attributs, :message_attributes + end +end diff --git a/db/migrate/20170808110333_change_checksum_source_type_to_text.rb b/db/migrate/20170808110333_change_checksum_source_type_to_text.rb new file mode 100644 index 000000000..731b2a19d --- /dev/null +++ b/db/migrate/20170808110333_change_checksum_source_type_to_text.rb @@ -0,0 +1,17 @@ +class ChangeChecksumSourceTypeToText < ActiveRecord::Migration + def tables + [:vehicle_journey_at_stops, :footnotes, :routing_constraint_zones, :routes, :journey_patterns, :vehicle_journeys, :time_table_dates, :time_table_periods, :time_tables] + end + + def up + self.tables.each do |table| + change_column table, :checksum_source, :text + end + end + + def down + self.tables.each do |table| + change_column table, :checksum_source, :string + end + end +end diff --git a/db/migrate/20170816104020_add_creator_to_imports.rb b/db/migrate/20170816104020_add_creator_to_imports.rb new file mode 100644 index 000000000..5fb808451 --- /dev/null +++ b/db/migrate/20170816104020_add_creator_to_imports.rb @@ -0,0 +1,5 @@ +class AddCreatorToImports < ActiveRecord::Migration + def change + add_column :imports, :creator, :string + end +end diff --git a/db/migrate/20170817122914_add_organisation_to_api_keys.rb b/db/migrate/20170817122914_add_organisation_to_api_keys.rb new file mode 100644 index 000000000..14c742c87 --- /dev/null +++ b/db/migrate/20170817122914_add_organisation_to_api_keys.rb @@ -0,0 +1,5 @@ +class AddOrganisationToApiKeys < ActiveRecord::Migration + def change + add_reference :api_keys, :organisation, index: true, foreign_key: true + end +end diff --git a/db/schema.rb b/db/schema.rb index e64e5c04a..b03200b6a 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: 20170607141317) do +ActiveRecord::Schema.define(version: 20170817122914) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -21,12 +21,12 @@ ActiveRecord::Schema.define(version: 20170607141317) do create_table "access_links", id: :bigserial, force: :cascade do |t| t.integer "access_point_id", limit: 8 t.integer "stop_area_id", limit: 8 - t.string "objectid", null: false + t.string "objectid", limit: 255, null: false t.integer "object_version", limit: 8 - t.string "creator_id" - t.string "name" - t.string "comment" - t.decimal "link_distance", precision: 19, scale: 2 + t.string "creator_id", limit: 255 + t.string "name", limit: 255 + t.string "comment", limit: 255 + t.decimal "link_distance", precision: 19, scale: 2 t.boolean "lift_availability" t.boolean "mobility_restricted_suitability" t.boolean "stairs_availability" @@ -34,9 +34,9 @@ ActiveRecord::Schema.define(version: 20170607141317) do t.time "frequent_traveller_duration" t.time "occasional_traveller_duration" t.time "mobility_restricted_traveller_duration" - t.string "link_type" + t.string "link_type", limit: 255 t.integer "int_user_needs" - t.string "link_orientation" + t.string "link_orientation", limit: 255 t.datetime "created_at" t.datetime "updated_at" end @@ -44,26 +44,26 @@ ActiveRecord::Schema.define(version: 20170607141317) do add_index "access_links", ["objectid"], name: "access_links_objectid_key", unique: true, using: :btree create_table "access_points", id: :bigserial, force: :cascade do |t| - t.string "objectid" + t.string "objectid", limit: 255 t.integer "object_version", limit: 8 - t.string "creator_id" - t.string "name" - t.string "comment" - 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" - t.string "contained_in" + t.string "creator_id", limit: 255 + t.string "name", limit: 255 + t.string "comment", limit: 255 + t.decimal "longitude", precision: 19, scale: 16 + t.decimal "latitude", precision: 19, scale: 16 + t.string "long_lat_type", limit: 255 + t.string "country_code", limit: 255 + t.string "street_name", limit: 255 + t.string "contained_in", limit: 255 t.time "openning_time" t.time "closing_time" - t.string "access_type" + t.string "access_type", limit: 255 t.boolean "lift_availability" t.boolean "mobility_restricted_suitability" t.boolean "stairs_availability" t.integer "stop_area_id", limit: 8 - t.string "zip_code" - t.string "city_name" + t.string "zip_code", limit: 255 + t.string "city_name", limit: 255 t.text "import_xml" t.datetime "created_at" t.datetime "updated_at" @@ -72,19 +72,22 @@ ActiveRecord::Schema.define(version: 20170607141317) do add_index "access_points", ["objectid"], name: "access_points_objectid_key", unique: true, using: :btree create_table "api_keys", id: :bigserial, force: :cascade do |t| - t.integer "referential_id", limit: 8 - t.string "token" - t.string "name" + t.integer "referential_id", limit: 8 + t.string "token", limit: 255 + t.string "name", limit: 255 t.datetime "created_at" t.datetime "updated_at" + t.integer "organisation_id" end + add_index "api_keys", ["organisation_id"], name: "index_api_keys_on_organisation_id", using: :btree + create_table "calendars", id: :bigserial, force: :cascade do |t| - t.string "name" - t.string "short_name" - t.daterange "date_ranges", array: true - t.date "dates", array: true - t.boolean "shared", default: false + t.string "name", limit: 255 + t.string "short_name", limit: 255 + t.daterange "date_ranges", array: true + t.date "dates", array: true + t.boolean "shared", default: false t.integer "organisation_id", limit: 8 t.datetime "created_at" t.datetime "updated_at" @@ -94,7 +97,7 @@ ActiveRecord::Schema.define(version: 20170607141317) do add_index "calendars", ["short_name"], name: "index_calendars_on_short_name", unique: true, using: :btree create_table "clean_up_results", id: :bigserial, force: :cascade do |t| - t.string "message_key" + t.string "message_key", limit: 255 t.hstore "message_attributs" t.integer "clean_up_id", limit: 8 t.datetime "created_at" @@ -104,7 +107,7 @@ ActiveRecord::Schema.define(version: 20170607141317) do add_index "clean_up_results", ["clean_up_id"], name: "index_clean_up_results_on_clean_up_id", using: :btree create_table "clean_ups", id: :bigserial, force: :cascade do |t| - t.string "status" + t.string "status", limit: 255 t.datetime "started_at" t.datetime "ended_at" t.integer "referential_id", limit: 8 @@ -118,20 +121,20 @@ ActiveRecord::Schema.define(version: 20170607141317) do add_index "clean_ups", ["referential_id"], name: "index_clean_ups_on_referential_id", using: :btree create_table "companies", id: :bigserial, force: :cascade do |t| - t.string "objectid", null: false + t.string "objectid", limit: 255, null: false t.integer "object_version", limit: 8 - t.string "creator_id" - t.string "name" - t.string "short_name" - t.string "organizational_unit" - t.string "operating_department_name" - t.string "code" - t.string "phone" - t.string "fax" - t.string "email" - t.string "registration_number" - t.string "url" - t.string "time_zone" + t.string "creator_id", limit: 255 + t.string "name", limit: 255 + t.string "short_name", limit: 255 + t.string "organizational_unit", limit: 255 + t.string "operating_department_name", limit: 255 + t.string "code", limit: 255 + t.string "phone", limit: 255 + t.string "fax", limit: 255 + t.string "email", limit: 255 + t.string "registration_number", limit: 255 + t.string "url", limit: 255 + t.string "time_zone", limit: 255 t.integer "line_referential_id", limit: 8 t.text "import_xml" t.datetime "created_at" @@ -145,13 +148,13 @@ ActiveRecord::Schema.define(version: 20170607141317) do create_table "connection_links", id: :bigserial, force: :cascade do |t| t.integer "departure_id", limit: 8 t.integer "arrival_id", limit: 8 - t.string "objectid", null: false + t.string "objectid", limit: 255, null: false t.integer "object_version", limit: 8 - t.string "creator_id" - t.string "name" - t.string "comment" - t.decimal "link_distance", precision: 19, scale: 2 - t.string "link_type" + t.string "creator_id", limit: 255 + t.string "name", limit: 255 + t.string "comment", limit: 255 + t.decimal "link_distance", precision: 19, scale: 2 + t.string "link_type", limit: 255 t.time "default_duration" t.time "frequent_traveller_duration" t.time "occasional_traveller_duration" @@ -166,15 +169,31 @@ ActiveRecord::Schema.define(version: 20170607141317) do add_index "connection_links", ["objectid"], name: "connection_links_objectid_key", unique: true, using: :btree + create_table "delayed_jobs", id: :bigserial, force: :cascade do |t| + t.integer "priority", default: 0 + t.integer "attempts", default: 0 + t.text "handler" + t.text "last_error" + t.datetime "run_at" + t.datetime "locked_at" + t.datetime "failed_at" + t.string "locked_by", limit: 255 + t.string "queue", limit: 255 + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "delayed_jobs", ["priority", "run_at"], name: "delayed_jobs_priority", using: :btree + create_table "exports", id: :bigserial, force: :cascade do |t| t.integer "referential_id", limit: 8 - t.string "status" - t.string "type" - t.string "options" + t.string "status", limit: 255 + t.string "type", limit: 255 + t.string "options", limit: 255 t.datetime "created_at" t.datetime "updated_at" - t.string "references_type" - t.string "reference_ids" + t.string "references_type", limit: 255 + t.string "reference_ids", limit: 255 end add_index "exports", ["referential_id"], name: "index_exports_on_referential_id", using: :btree @@ -184,23 +203,23 @@ ActiveRecord::Schema.define(version: 20170607141317) do t.integer "line_id", limit: 8 t.integer "connection_link_id", limit: 8 t.integer "stop_point_id", limit: 8 - t.string "objectid", null: false + t.string "objectid", limit: 255, null: false t.integer "object_version", limit: 8 t.datetime "creation_time" - t.string "creator_id" - t.string "name" - t.string "comment" - t.string "description" + t.string "creator_id", limit: 255 + t.string "name", limit: 255 + t.string "comment", limit: 255 + t.string "description", limit: 255 t.boolean "free_access" - t.decimal "longitude", precision: 19, scale: 16 - t.decimal "latitude", precision: 19, scale: 16 - t.string "long_lat_type" - t.decimal "x", precision: 19, scale: 2 - t.decimal "y", precision: 19, scale: 2 - t.string "projection_type" - t.string "country_code" - t.string "street_name" - t.string "contained_in" + t.decimal "longitude", precision: 19, scale: 16 + t.decimal "latitude", precision: 19, scale: 16 + t.string "long_lat_type", limit: 255 + t.decimal "x", precision: 19, scale: 2 + t.decimal "y", precision: 19, scale: 2 + t.string "projection_type", limit: 255 + t.string "country_code", limit: 255 + t.string "street_name", limit: 255 + t.string "contained_in", limit: 255 end add_index "facilities", ["objectid"], name: "facilities_objectid_key", unique: true, using: :btree @@ -211,11 +230,13 @@ ActiveRecord::Schema.define(version: 20170607141317) do end create_table "footnotes", id: :bigserial, force: :cascade do |t| - t.integer "line_id", limit: 8 - t.string "code" - t.string "label" + t.integer "line_id", limit: 8 + t.string "code", limit: 255 + t.string "label", limit: 255 t.datetime "created_at" t.datetime "updated_at" + t.string "checksum" + t.text "checksum_source" end create_table "footnotes_vehicle_journeys", id: false, force: :cascade do |t| @@ -224,12 +245,12 @@ ActiveRecord::Schema.define(version: 20170607141317) do end create_table "group_of_lines", id: :bigserial, force: :cascade do |t| - t.string "objectid", null: false + t.string "objectid", limit: 255, null: false t.integer "object_version", limit: 8 - t.string "creator_id" - t.string "name" - t.string "comment" - t.string "registration_number" + t.string "creator_id", limit: 255 + t.string "name", limit: 255 + t.string "comment", limit: 255 + t.string "registration_number", limit: 255 t.integer "line_referential_id", limit: 8 t.text "import_xml" t.datetime "created_at" @@ -246,8 +267,8 @@ ActiveRecord::Schema.define(version: 20170607141317) do create_table "import_messages", id: :bigserial, force: :cascade do |t| t.integer "criticity" - t.string "message_key" - t.hstore "message_attributs" + t.string "message_key", limit: 255 + t.hstore "message_attributes" t.integer "import_id", limit: 8 t.integer "resource_id", limit: 8 t.datetime "created_at" @@ -260,31 +281,37 @@ ActiveRecord::Schema.define(version: 20170607141317) do create_table "import_resources", id: :bigserial, force: :cascade do |t| t.integer "import_id", limit: 8 - t.string "status" + t.string "status", limit: 255 t.datetime "created_at" t.datetime "updated_at" - t.string "type" - t.string "reference" - t.string "name" + t.string "type", limit: 255 + t.string "reference", limit: 255 + t.string "name", limit: 255 t.hstore "metrics" end add_index "import_resources", ["import_id"], name: "index_import_resources_on_import_id", using: :btree create_table "imports", id: :bigserial, force: :cascade do |t| - t.string "status" - t.string "current_step_id" + t.string "status", limit: 255 + t.string "current_step_id", limit: 255 t.float "current_step_progress" t.integer "workbench_id", limit: 8 t.integer "referential_id", limit: 8 - t.string "name" + t.string "name", limit: 255 t.datetime "created_at" t.datetime "updated_at" - t.string "file" + t.string "file", limit: 255 t.datetime "started_at" t.datetime "ended_at" - t.string "token_download" + t.string "token_download", limit: 255 t.string "type", limit: 255 + t.integer "parent_id", limit: 8 + t.string "parent_type" + t.datetime "notified_parent_at" + t.integer "current_step", default: 0 + t.integer "total_steps", default: 0 + t.string "creator" end add_index "imports", ["referential_id"], name: "index_imports_on_referential_id", using: :btree @@ -318,18 +345,20 @@ ActiveRecord::Schema.define(version: 20170607141317) do create_table "journey_patterns", id: :bigserial, force: :cascade do |t| t.integer "route_id", limit: 8 - t.string "objectid", null: false + t.string "objectid", limit: 255, null: false t.integer "object_version", limit: 8 - t.string "creator_id" - t.string "name" - t.string "comment" - t.string "registration_number" - t.string "published_name" + t.string "creator_id", limit: 255 + t.string "name", limit: 255 + t.string "comment", limit: 255 + t.string "registration_number", limit: 255 + t.string "published_name", limit: 255 t.integer "departure_stop_point_id", limit: 8 t.integer "arrival_stop_point_id", limit: 8 - t.integer "section_status", default: 0, null: false + t.integer "section_status", default: 0, null: false t.datetime "created_at" t.datetime "updated_at" + t.string "checksum" + t.text "checksum_source" end add_index "journey_patterns", ["objectid"], name: "journey_patterns_objectid_key", unique: true, using: :btree @@ -349,8 +378,8 @@ ActiveRecord::Schema.define(version: 20170607141317) do create_table "line_referential_sync_messages", id: :bigserial, force: :cascade do |t| t.integer "criticity" - t.string "message_key" - t.hstore "message_attributs" + t.string "message_key", limit: 255 + t.hstore "message_attributes" t.integer "line_referential_sync_id", limit: 8 t.datetime "created_at" t.datetime "updated_at" @@ -364,42 +393,42 @@ ActiveRecord::Schema.define(version: 20170607141317) do t.datetime "updated_at" t.datetime "started_at" t.datetime "ended_at" - t.string "status" + t.string "status", limit: 255 end add_index "line_referential_syncs", ["line_referential_id"], name: "index_line_referential_syncs_on_line_referential_id", using: :btree create_table "line_referentials", id: :bigserial, force: :cascade do |t| - t.string "name" + t.string "name", limit: 255 t.datetime "created_at" t.datetime "updated_at" - t.integer "sync_interval", default: 1 + t.integer "sync_interval", default: 1 end create_table "lines", id: :bigserial, force: :cascade do |t| t.integer "network_id", limit: 8 t.integer "company_id", limit: 8 - t.string "objectid", null: false + t.string "objectid", limit: 255, null: false t.integer "object_version", limit: 8 - t.string "creator_id" - t.string "name" - t.string "number" - t.string "published_name" - t.string "transport_mode" - t.string "registration_number" - t.string "comment" + t.string "creator_id", limit: 255 + t.string "name", limit: 255 + t.string "number", limit: 255 + t.string "published_name", limit: 255 + t.string "transport_mode", limit: 255 + t.string "registration_number", limit: 255 + t.string "comment", limit: 255 t.boolean "mobility_restricted_suitability" t.integer "int_user_needs" t.boolean "flexible_service" - t.string "url" + t.string "url", limit: 255 t.string "color", limit: 6 t.string "text_color", limit: 6 - t.string "stable_id" + t.string "stable_id", limit: 255 t.integer "line_referential_id", limit: 8 - t.boolean "deactivated", default: false + t.boolean "deactivated", default: false t.text "import_xml" - t.string "transport_submode" - t.integer "secondary_company_ids", limit: 8, array: true + t.string "transport_submode", limit: 255 + t.integer "secondary_company_ids", limit: 8, array: true t.datetime "created_at" t.datetime "updated_at" t.boolean "seasonal" @@ -411,17 +440,17 @@ ActiveRecord::Schema.define(version: 20170607141317) do add_index "lines", ["secondary_company_ids"], name: "index_lines_on_secondary_company_ids", using: :gin create_table "networks", id: :bigserial, force: :cascade do |t| - t.string "objectid", null: false + t.string "objectid", limit: 255, null: false t.integer "object_version", limit: 8 - t.string "creator_id" + t.string "creator_id", limit: 255 t.date "version_date" - t.string "description" - t.string "name" - t.string "registration_number" - t.string "source_name" - t.string "source_type" - t.string "source_identifier" - t.string "comment" + t.string "description", limit: 255 + t.string "name", limit: 255 + t.string "registration_number", limit: 255 + t.string "source_name", limit: 255 + t.string "source_type", limit: 255 + t.string "source_identifier", limit: 255 + t.string "comment", limit: 255 t.text "import_xml" t.integer "line_referential_id", limit: 8 t.datetime "created_at" @@ -433,11 +462,11 @@ ActiveRecord::Schema.define(version: 20170607141317) do add_index "networks", ["registration_number"], name: "networks_registration_number_key", using: :btree create_table "organisations", id: :bigserial, force: :cascade do |t| - t.string "name" + t.string "name", limit: 255 t.datetime "created_at" t.datetime "updated_at" - t.string "data_format", default: "neptune" - t.string "code" + t.string "data_format", limit: 255, default: "neptune" + t.string "code", limit: 255 t.datetime "synced_at" t.hstore "sso_attributes" end @@ -448,12 +477,12 @@ ActiveRecord::Schema.define(version: 20170607141317) do t.integer "start_of_link_id", limit: 8 t.integer "end_of_link_id", limit: 8 t.integer "route_id", limit: 8 - t.string "objectid", null: false + t.string "objectid", limit: 255, null: false t.integer "object_version", limit: 8 - t.string "creator_id" - t.string "name" - t.string "comment" - t.decimal "link_distance", precision: 19, scale: 2 + t.string "creator_id", limit: 255 + t.string "name", limit: 255 + t.string "comment", limit: 255 + t.decimal "link_distance", precision: 19, scale: 2 t.datetime "created_at" t.datetime "updated_at" end @@ -461,7 +490,7 @@ ActiveRecord::Schema.define(version: 20170607141317) do add_index "pt_links", ["objectid"], name: "pt_links_objectid_key", unique: true, using: :btree create_table "referential_clonings", id: :bigserial, force: :cascade do |t| - t.string "status" + t.string "status", limit: 255 t.datetime "started_at" t.datetime "ended_at" t.integer "source_referential_id", limit: 8 @@ -482,30 +511,30 @@ ActiveRecord::Schema.define(version: 20170607141317) do t.daterange "periodes", array: true end - add_index "referential_metadata", ["line_ids"], name: "index_referential_metadata_on_line_ids", using: :gin + add_index "referential_metadata", ["line_ids"], name: "index_referential_metadata_on_line_ids", using: :btree add_index "referential_metadata", ["referential_id"], name: "index_referential_metadata_on_referential_id", using: :btree add_index "referential_metadata", ["referential_source_id"], name: "index_referential_metadata_on_referential_source_id", using: :btree create_table "referentials", id: :bigserial, force: :cascade do |t| - t.string "name" - t.string "slug" + t.string "name", limit: 255 + t.string "slug", limit: 255 t.datetime "created_at" t.datetime "updated_at" - t.string "prefix" - t.string "projection_type" - t.string "time_zone" - t.string "bounds" + t.string "prefix", limit: 255 + t.string "projection_type", limit: 255 + t.string "time_zone", limit: 255 + t.string "bounds", limit: 255 t.integer "organisation_id", limit: 8 t.text "geographical_bounds" t.integer "user_id", limit: 8 - t.string "user_name" - t.string "data_format" + t.string "user_name", limit: 255 + t.string "data_format", limit: 255 t.integer "line_referential_id", limit: 8 t.integer "stop_area_referential_id", limit: 8 t.integer "workbench_id", limit: 8 t.datetime "archived_at" t.integer "created_from_id", limit: 8 - t.boolean "ready", default: false + t.boolean "ready", default: false end add_index "referentials", ["created_from_id"], name: "index_referentials_on_created_from_id", using: :btree @@ -513,44 +542,48 @@ ActiveRecord::Schema.define(version: 20170607141317) do create_table "route_sections", id: :bigserial, force: :cascade do |t| t.integer "departure_id", limit: 8 t.integer "arrival_id", limit: 8 - t.geometry "input_geometry", limit: {:srid=>4326, :type=>"line_string"} - t.geometry "processed_geometry", limit: {:srid=>4326, :type=>"line_string"} - t.string "objectid", null: false + t.string "objectid", limit: 255, null: false t.integer "object_version", limit: 8 - t.string "creator_id" + t.string "creator_id", limit: 255 t.float "distance" t.boolean "no_processing" + t.geometry "input_geometry", limit: {:srid=>4326, :type=>"line_string"} + t.geometry "processed_geometry", limit: {:srid=>4326, :type=>"line_string"} t.datetime "created_at" t.datetime "updated_at" end create_table "routes", id: :bigserial, force: :cascade do |t| t.integer "line_id", limit: 8 - t.string "objectid", null: false + t.string "objectid", limit: 255, null: false t.integer "object_version", limit: 8 - t.string "creator_id" - t.string "name" - t.string "comment" + t.string "creator_id", limit: 255 + t.string "name", limit: 255 + t.string "comment", limit: 255 t.integer "opposite_route_id", limit: 8 - t.string "published_name" - t.string "number" - t.string "direction" - t.string "wayback" + t.string "published_name", limit: 255 + t.string "number", limit: 255 + t.string "direction", limit: 255 + t.string "wayback", limit: 255 t.datetime "created_at" t.datetime "updated_at" + t.string "checksum" + t.text "checksum_source" end add_index "routes", ["objectid"], name: "routes_objectid_key", unique: true, using: :btree create_table "routing_constraint_zones", id: :bigserial, force: :cascade do |t| - t.string "name" + t.string "name", limit: 255 t.datetime "created_at" t.datetime "updated_at" - t.string "objectid", null: false - t.integer "object_version", limit: 8 - t.string "creator_id" - t.integer "route_id", limit: 8 - t.integer "stop_point_ids", limit: 8, array: true + t.string "objectid", limit: 255, null: false + t.integer "object_version", limit: 8 + t.string "creator_id", limit: 255 + t.integer "route_id", limit: 8 + t.integer "stop_point_ids", limit: 8, array: true + t.string "checksum" + t.text "checksum_source" end create_table "routing_constraints_lines", id: false, force: :cascade do |t| @@ -560,7 +593,7 @@ ActiveRecord::Schema.define(version: 20170607141317) do create_table "rule_parameter_sets", id: :bigserial, force: :cascade do |t| t.text "parameters" - t.string "name" + t.string "name", limit: 255 t.datetime "created_at" t.datetime "updated_at" t.integer "organisation_id", limit: 8 @@ -574,8 +607,8 @@ ActiveRecord::Schema.define(version: 20170607141317) do create_table "stop_area_referential_sync_messages", id: :bigserial, force: :cascade do |t| t.integer "criticity" - t.string "message_key" - t.hstore "message_attributs" + t.string "message_key", limit: 255 + t.hstore "message_attributes" t.integer "stop_area_referential_sync_id", limit: 8 t.datetime "created_at" t.datetime "updated_at" @@ -589,43 +622,43 @@ ActiveRecord::Schema.define(version: 20170607141317) do t.datetime "updated_at" t.datetime "ended_at" t.datetime "started_at" - t.string "status" + t.string "status", limit: 255 end add_index "stop_area_referential_syncs", ["stop_area_referential_id"], name: "index_stop_area_referential_syncs_on_stop_area_referential_id", using: :btree create_table "stop_area_referentials", id: :bigserial, force: :cascade do |t| - t.string "name" + t.string "name", limit: 255 t.datetime "created_at" t.datetime "updated_at" end create_table "stop_areas", id: :bigserial, force: :cascade do |t| t.integer "parent_id", limit: 8 - t.string "objectid", null: false + t.string "objectid", limit: 255, null: false t.integer "object_version", limit: 8 - t.string "creator_id" - t.string "name" - t.string "comment" - t.string "area_type" - t.string "registration_number" - t.string "nearest_topic_name" + t.string "creator_id", limit: 255 + t.string "name", limit: 255 + t.string "comment", limit: 255 + t.string "area_type", limit: 255 + t.string "registration_number", limit: 255 + t.string "nearest_topic_name", limit: 255 t.integer "fare_code" 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" + t.string "long_lat_type", limit: 255 + t.string "country_code", limit: 255 + t.string "street_name", limit: 255 t.boolean "mobility_restricted_suitability" t.boolean "stairs_availability" t.boolean "lift_availability" t.integer "int_user_needs" - t.string "zip_code" - t.string "city_name" - t.string "url" - t.string "time_zone" + t.string "zip_code", limit: 255 + t.string "city_name", limit: 255 + t.string "url", limit: 255 + t.string "time_zone", limit: 255 t.integer "stop_area_referential_id", limit: 8 - t.string "status" + t.string "status", limit: 255 t.text "import_xml" t.datetime "deleted_at" t.datetime "created_at" @@ -646,12 +679,12 @@ ActiveRecord::Schema.define(version: 20170607141317) do create_table "stop_points", id: :bigserial, force: :cascade do |t| t.integer "route_id", limit: 8 t.integer "stop_area_id", limit: 8 - t.string "objectid", null: false + t.string "objectid", limit: 255, null: false t.integer "object_version", limit: 8 - t.string "creator_id" + t.string "creator_id", limit: 255 t.integer "position" - t.string "for_boarding" - t.string "for_alighting" + t.string "for_boarding", limit: 255 + t.string "for_alighting", limit: 255 t.datetime "created_at" t.datetime "updated_at" end @@ -661,9 +694,9 @@ ActiveRecord::Schema.define(version: 20170607141317) do create_table "taggings", id: :bigserial, force: :cascade do |t| t.integer "tag_id", limit: 8 t.integer "taggable_id", limit: 8 - t.string "taggable_type" + t.string "taggable_type", limit: 255 t.integer "tagger_id", limit: 8 - t.string "tagger_type" + t.string "tagger_type", limit: 255 t.string "context", limit: 128 t.datetime "created_at" end @@ -672,36 +705,40 @@ ActiveRecord::Schema.define(version: 20170607141317) do add_index "taggings", ["taggable_id", "taggable_type", "context"], name: "index_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree create_table "tags", id: :bigserial, force: :cascade do |t| - t.string "name" - t.integer "taggings_count", default: 0 + t.string "name", limit: 255 + t.integer "taggings_count", default: 0 end add_index "tags", ["name"], name: "index_tags_on_name", unique: true, using: :btree create_table "time_table_dates", id: :bigserial, force: :cascade do |t| - t.integer "time_table_id", limit: 8, null: false + t.integer "time_table_id", limit: 8, null: false t.date "date" - t.integer "position", null: false + t.integer "position", null: false t.boolean "in_out" + t.string "checksum" + t.text "checksum_source" end add_index "time_table_dates", ["time_table_id"], name: "index_time_table_dates_on_time_table_id", using: :btree create_table "time_table_periods", id: :bigserial, force: :cascade do |t| - t.integer "time_table_id", limit: 8, null: false + t.integer "time_table_id", limit: 8, null: false t.date "period_start" t.date "period_end" - t.integer "position", null: false + t.integer "position", null: false + t.string "checksum" + t.text "checksum_source" end 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.string "objectid", limit: 255, null: false t.integer "object_version", limit: 8, default: 1 - t.string "creator_id" - t.string "version" - t.string "comment" + t.string "creator_id", limit: 255 + t.string "version", limit: 255 + t.string "comment", limit: 255 t.integer "int_day_types", default: 0 t.date "start_date" t.date "end_date" @@ -710,6 +747,8 @@ ActiveRecord::Schema.define(version: 20170607141317) do t.datetime "updated_at" t.string "color", limit: 255 t.integer "created_from_id" + t.string "checksum" + t.text "checksum_source" end add_index "time_tables", ["calendar_id"], name: "index_time_tables_on_calendar_id", using: :btree @@ -725,49 +764,49 @@ ActiveRecord::Schema.define(version: 20170607141317) do add_index "time_tables_vehicle_journeys", ["vehicle_journey_id"], name: "index_time_tables_vehicle_journeys_on_vehicle_journey_id", using: :btree create_table "timebands", id: :bigserial, force: :cascade do |t| - t.string "objectid", null: false + t.string "objectid", limit: 255, null: false t.integer "object_version", limit: 8 - t.string "creator_id" - t.string "name" - t.time "start_time", null: false - t.time "end_time", null: false + t.string "creator_id", limit: 255 + t.string "name", limit: 255 + t.time "start_time", null: false + t.time "end_time", null: false t.datetime "created_at" t.datetime "updated_at" end create_table "users", id: :bigserial, force: :cascade do |t| - t.string "email", default: "", null: false - t.string "encrypted_password", default: "" - t.string "reset_password_token" + t.string "email", limit: 255, default: "", null: false + t.string "encrypted_password", limit: 255, default: "" + t.string "reset_password_token", limit: 255 t.datetime "reset_password_sent_at" t.datetime "remember_created_at" - t.integer "sign_in_count", default: 0 + t.integer "sign_in_count", default: 0 t.datetime "current_sign_in_at" t.datetime "last_sign_in_at" - t.string "current_sign_in_ip" - t.string "last_sign_in_ip" + t.string "current_sign_in_ip", limit: 255 + t.string "last_sign_in_ip", limit: 255 t.datetime "created_at" t.datetime "updated_at" t.integer "organisation_id", limit: 8 - t.string "name" - t.string "confirmation_token" + t.string "name", limit: 255 + t.string "confirmation_token", limit: 255 t.datetime "confirmed_at" t.datetime "confirmation_sent_at" - t.string "unconfirmed_email" - t.integer "failed_attempts", default: 0 - t.string "unlock_token" + t.string "unconfirmed_email", limit: 255 + t.integer "failed_attempts", default: 0 + t.string "unlock_token", limit: 255 t.datetime "locked_at" - t.string "authentication_token" - t.string "invitation_token" + t.string "authentication_token", limit: 255 + t.string "invitation_token", limit: 255 t.datetime "invitation_sent_at" t.datetime "invitation_accepted_at" t.integer "invitation_limit" t.integer "invited_by_id", limit: 8 - t.string "invited_by_type" + t.string "invited_by_type", limit: 255 t.datetime "invitation_created_at" - t.string "username" + t.string "username", limit: 255 t.datetime "synced_at" - t.string "permissions", array: true + t.string "permissions", limit: 255, array: true end add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree @@ -778,14 +817,16 @@ ActiveRecord::Schema.define(version: 20170607141317) do create_table "vehicle_journey_at_stops", id: :bigserial, force: :cascade do |t| t.integer "vehicle_journey_id", limit: 8 t.integer "stop_point_id", limit: 8 - t.string "connecting_service_id" - t.string "boarding_alighting_possibility" + t.string "connecting_service_id", limit: 255 + t.string "boarding_alighting_possibility", limit: 255 t.time "arrival_time" t.time "departure_time" - t.string "for_boarding" - t.string "for_alighting" - t.integer "departure_day_offset", default: 0 - t.integer "arrival_day_offset", default: 0 + t.string "for_boarding", limit: 255 + t.string "for_alighting", limit: 255 + t.integer "departure_day_offset", default: 0 + t.integer "arrival_day_offset", default: 0 + t.string "checksum" + t.text "checksum_source" end add_index "vehicle_journey_at_stops", ["stop_point_id"], name: "index_vehicle_journey_at_stops_on_stop_pointid", using: :btree @@ -795,29 +836,31 @@ ActiveRecord::Schema.define(version: 20170607141317) do t.integer "route_id", limit: 8 t.integer "journey_pattern_id", limit: 8 t.integer "company_id", limit: 8 - t.string "objectid", null: false + t.string "objectid", limit: 255, null: false t.integer "object_version", limit: 8 - t.string "creator_id" - t.string "comment" - t.string "status_value" - t.string "transport_mode" - t.string "published_journey_name" - t.string "published_journey_identifier" - t.string "facility" - t.string "vehicle_type_identifier" + t.string "creator_id", limit: 255 + t.string "comment", limit: 255 + t.string "status_value", limit: 255 + t.string "transport_mode", limit: 255 + t.string "published_journey_name", limit: 255 + t.string "published_journey_identifier", limit: 255 + t.string "facility", limit: 255 + t.string "vehicle_type_identifier", limit: 255 t.integer "number", limit: 8 t.boolean "mobility_restricted_suitability" t.boolean "flexible_service" - t.integer "journey_category", default: 0, null: false + t.integer "journey_category", default: 0, null: false t.datetime "created_at" t.datetime "updated_at" + t.string "checksum" + t.text "checksum_source" end add_index "vehicle_journeys", ["objectid"], name: "vehicle_journeys_objectid_key", unique: true, using: :btree add_index "vehicle_journeys", ["route_id"], name: "index_vehicle_journeys_on_route_id", using: :btree create_table "workbenches", id: :bigserial, force: :cascade do |t| - t.string "name" + t.string "name", limit: 255 t.integer "organisation_id", limit: 8 t.datetime "created_at" t.datetime "updated_at" @@ -829,20 +872,19 @@ ActiveRecord::Schema.define(version: 20170607141317) do add_index "workbenches", ["organisation_id"], name: "index_workbenches_on_organisation_id", using: :btree add_index "workbenches", ["stop_area_referential_id"], name: "index_workbenches_on_stop_area_referential_id", using: :btree - add_foreign_key "access_links", "access_points", name: "aclk_acpt_fkey" + add_foreign_key "access_links", "access_points", name: "aclk_acpt_fkey", on_delete: :cascade + add_foreign_key "api_keys", "organisations" add_foreign_key "group_of_lines_lines", "group_of_lines", name: "groupofline_group_fkey", on_delete: :cascade - add_foreign_key "journey_frequencies", "timebands", on_delete: :nullify - add_foreign_key "journey_frequencies", "vehicle_journeys", on_delete: :nullify - add_foreign_key "journey_pattern_sections", "journey_patterns", on_delete: :cascade - add_foreign_key "journey_pattern_sections", "route_sections", on_delete: :cascade + add_foreign_key "journey_frequencies", "timebands", name: "journey_frequencies_timeband_id_fk", on_delete: :nullify + add_foreign_key "journey_frequencies", "vehicle_journeys", name: "journey_frequencies_vehicle_journey_id_fk", on_delete: :nullify + add_foreign_key "journey_pattern_sections", "journey_patterns", name: "journey_pattern_sections_journey_pattern_id_fk", on_delete: :cascade + add_foreign_key "journey_pattern_sections", "route_sections", name: "journey_pattern_sections_route_section_id_fk", on_delete: :cascade add_foreign_key "journey_patterns", "routes", name: "jp_route_fkey", on_delete: :cascade add_foreign_key "journey_patterns", "stop_points", column: "arrival_stop_point_id", name: "arrival_point_fkey", on_delete: :nullify add_foreign_key "journey_patterns", "stop_points", column: "departure_stop_point_id", name: "departure_point_fkey", on_delete: :nullify add_foreign_key "journey_patterns_stop_points", "journey_patterns", name: "jpsp_jp_fkey", on_delete: :cascade add_foreign_key "journey_patterns_stop_points", "stop_points", name: "jpsp_stoppoint_fkey", on_delete: :cascade - add_foreign_key "route_sections", "stop_areas", column: "arrival_id" - add_foreign_key "route_sections", "stop_areas", column: "departure_id" - add_foreign_key "routes", "routes", column: "opposite_route_id", name: "route_opposite_route_fkey" + add_foreign_key "routes", "routes", column: "opposite_route_id", name: "route_opposite_route_fkey", on_delete: :nullify add_foreign_key "stop_areas", "stop_areas", column: "parent_id", name: "area_parent_fkey", on_delete: :nullify add_foreign_key "stop_areas_stop_areas", "stop_areas", column: "child_id", name: "stoparea_child_fkey", on_delete: :cascade add_foreign_key "stop_areas_stop_areas", "stop_areas", column: "parent_id", name: "stoparea_parent_fkey", on_delete: :cascade diff --git a/lib/result.rb b/lib/result.rb new file mode 100644 index 000000000..96e03d323 --- /dev/null +++ b/lib/result.rb @@ -0,0 +1,37 @@ +# A value wrapper adding status information to any value +# Status can be :ok or :error, we are thusly implementing +# what is expressed in Elixir/Erlang as result tuples and +# in Haskell as `Data.Either` +class Result + + attr_reader :status, :value + + class << self + def ok value + make :ok, value + end + def error value + make :error, value + end + + def new *args + raise NoMethodError, "No default constructor for #{self}" + end + + private + def make status, value + allocate.tap do | o | + o.instance_exec do + @status = status + @value = value + end + end + end + end + + def ok?; status == :ok end + + def == other + other.kind_of?(self.class) && other.status == status && other.value == value + end +end diff --git a/lib/tasks/ci.rake b/lib/tasks/ci.rake index 658e4e04e..90e47560e 100644 --- a/lib/tasks/ci.rake +++ b/lib/tasks/ci.rake @@ -3,7 +3,7 @@ namespace :ci do task :setup do cp "config/database/jenkins.yml", "config/database.yml" sh "RAILS_ENV=test rake db:migrate" - sh "npm install" + sh "npm --production --no-progress install" end def git_branch @@ -27,11 +27,14 @@ namespace :ci do sh "bundle exec bundle-audit check --update" end + task :spec do + sh "bundle exec rspec --profile" + end + task :teaspoon do sh "RAILS_ENV=test bundle exec rake teaspoon" end - desc "Deploy after CI" task :deploy do sh "cap #{deploy_env} deploy:migrations" @@ -44,4 +47,4 @@ namespace :ci do end desc "Run continuous integration tasks (spec, ...)" -task :ci => ["ci:setup", "spec", "ci:teaspoon", "cucumber", "ci:check_security", "ci:deploy", "ci:clean"] +task :ci => ["ci:setup", "ci:spec", "ci:teaspoon", "cucumber", "ci:check_security", "ci:deploy", "ci:clean"] diff --git a/lib/tasks/generate.rake b/lib/tasks/generate.rake index f02a75cc2..a9b1a3454 100644 --- a/lib/tasks/generate.rake +++ b/lib/tasks/generate.rake @@ -2,8 +2,15 @@ namespace :generate do desc "Create model diagrams for Chouette" task :model_diagram => :environment do - sh "bundle exec rake erd only='Calendar,Referential,Chouette::Line,Chouette::Route,Chouette::JourneyPattern,Chouette::VehicleJourney,Chouette::VehicleJourneyAtStop,Chouette::TimeTable,Chouette::TimeTableDate,Chouette::TimeTablePeriod,Chouette::Footnote,Chouette::Network,Chouette::Company,Chouette::StopPoint,Chouette::StopArea' filename='offer_datas' title='Offer Datas'" sh "bundle exec rake erd only='Organisation,Referential,User,Workbench' filename='organisation' title='Organisation'" + sh "bundle exec rake erd only='Calendar,Referential,Chouette::Line,Chouette::Route,Chouette::JourneyPattern,Chouette::VehicleJourney,Chouette::VehicleJourneyAtStop,Chouette::TimeTable,Chouette::TimeTableDate,Chouette::TimeTablePeriod,Chouette::Footnote,Chouette::Network,Chouette::Company,Chouette::StopPoint,Chouette::StopArea' filename='offer_datas' title='Offer Datas'" + sh "bundle exec rake erd only='Organisation,StopAreaReferential,StopAreaReferentialSync,StopAreaReferentialSyncMessage,StopAreaReferentialMembership,LineReferential,LineReferentialSync,LineReferentialSyncMessage,LineReferentialMembership' filename='referentiels_externes' title='Référentiels externes'" + sh "bundle exec rake erd only='NetexImport,Import,WorkbenchImport,ImportResource,ImportMessage' filename='import' title='Import'" + #sh "bundle exec rake erd only='' filename='validation' title='Validation'" + #sh "bundle exec rake erd only='VehicleJourney,VehicleJourneyExport' filename='export' title='Export'" + #sh "bundle exec rake erd only='' filename='intégration' title='Integration'" + #sh "bundle exec rake erd only='' filename='fusion' title='Fusion'" + #sh "bundle exec rake erd only='' filename='publication' title='Publication'" end end diff --git a/lib/tasks/imports.rake b/lib/tasks/imports.rake new file mode 100644 index 000000000..6bc84acc8 --- /dev/null +++ b/lib/tasks/imports.rake @@ -0,0 +1,6 @@ +namespace :import do + desc "Notify parent imports when children finish" + task notify_parent: :environment do + ParentImportNotifier.notify_when_finished + end +end diff --git a/spec/concerns/configurable_spec.rb b/spec/concerns/configurable_spec.rb new file mode 100644 index 000000000..330241b72 --- /dev/null +++ b/spec/concerns/configurable_spec.rb @@ -0,0 +1,35 @@ +RSpec.describe Configurable do + + subject do + Class.new do + include Configurable + end + end + + let( :something ){ double('something') } + + it 'can be configured' do + expect{ subject.config.anything }.to raise_error(NoMethodError) + + subject.config.something = something + + expect( subject.config.something ).to eq(something) + # Instances delegate to the class + expect( subject.new.send(:config).something ).to eq(something) + # **All** instances delegate to the class + expect( subject.new.send(:config).something ).to eq(something) + end + + it 'can be configured with a block' do + + subject.config do | c | + c.something = something + end + + expect( subject.config.something ).to eq(something) + # Instances delegate to the class + expect( subject.new.send(:config).something ).to eq(something) + # **All** instances delegate to the class + expect( subject.new.send(:config).something ).to eq(something) + end +end diff --git a/spec/controllers/api/v1/iboo_controller_spec.rb b/spec/controllers/api/v1/iboo_controller_spec.rb new file mode 100644 index 000000000..64a929d1a --- /dev/null +++ b/spec/controllers/api/v1/iboo_controller_spec.rb @@ -0,0 +1,12 @@ +require 'rails_helper' + +RSpec.describe Api::V1::IbooController, type: :controller do + context '#authenticate' do + include_context 'iboo authenticated api user' + + it 'should set current organisation' do + controller.send(:authenticate) + expect(assigns(:current_organisation)).to eq api_key.organisation + end + end +end diff --git a/spec/controllers/api/v1/imports_controller_spec.rb b/spec/controllers/api/v1/imports_controller_spec.rb new file mode 100644 index 000000000..266b25486 --- /dev/null +++ b/spec/controllers/api/v1/imports_controller_spec.rb @@ -0,0 +1,38 @@ +require 'rails_helper' + +RSpec.describe Api::V1::ImportsController, type: :controller do + let(:workbench) { create :workbench, organisation: organisation } + + context 'unauthenticated' do + include_context 'iboo wrong authorisation api user' + + describe 'GET #index' do + it 'should not be successful' do + get :index, workbench_id: workbench.id + expect(response).not_to be_success + end + end + end + + context 'authenticated' do + include_context 'iboo authenticated api user' + + describe 'GET #index' do + it 'should be successful' do + get :index, workbench_id: workbench.id + expect(response).to be_success + end + end + + describe 'POST #create' do + let(:file) { fixture_file_upload('multiple_references_import.zip') } + + it 'should be successful' do + expect { + post :create, workbench_id: workbench.id, workbench_import: {file: file, creator: 'test'}, format: :json + }.to change{WorkbenchImport.count}.by(1) + expect(response).to be_success + end + end + end +end diff --git a/spec/controllers/api/v1/workbenches_controller_spec.rb b/spec/controllers/api/v1/workbenches_controller_spec.rb new file mode 100644 index 000000000..7780da142 --- /dev/null +++ b/spec/controllers/api/v1/workbenches_controller_spec.rb @@ -0,0 +1,25 @@ +require 'rails_helper' + +RSpec.describe Api::V1::WorkbenchesController, type: :controller do + context 'unauthenticated' do + include_context 'iboo wrong authorisation api user' + + describe 'GET #index' do + it 'should not be successful' do + get :index + expect(response).not_to be_success + end + end + end + + context 'authenticated' do + include_context 'iboo authenticated api user' + + describe 'GET #index' do + it 'should be successful' do + get :index + expect(response).to be_success + end + end + end +end diff --git a/spec/controllers/imports_controller_spec.rb b/spec/controllers/imports_controller_spec.rb index 7b575ab61..f07190496 100644 --- a/spec/controllers/imports_controller_spec.rb +++ b/spec/controllers/imports_controller_spec.rb @@ -15,6 +15,7 @@ RSpec.describe ImportsController, :type => :controller do it 'should be successful' do get :download, workbench_id: workbench.id, id: import.id, token: import.token_download expect(response).to be_success + expect( response.body ).to eq(import.file.read) end end end diff --git a/spec/decorators/api_key_decorator_spec.rb b/spec/decorators/api_key_decorator_spec.rb new file mode 100644 index 000000000..9451a3974 --- /dev/null +++ b/spec/decorators/api_key_decorator_spec.rb @@ -0,0 +1,4 @@ +require 'spec_helper' + +describe ApiKeyDecorator do +end diff --git a/spec/factories/api_keys.rb b/spec/factories/api_keys.rb new file mode 100644 index 000000000..963938c64 --- /dev/null +++ b/spec/factories/api_keys.rb @@ -0,0 +1,8 @@ +FactoryGirl.define do + factory :api_key, class: Api::V1::ApiKey do + name { SecureRandom.urlsafe_base64 } + token { "#{referential_id}-#{organisation_id}-#{SecureRandom.hex}" } + referential + organisation + end +end diff --git a/spec/factories/chouette_journey_pattern.rb b/spec/factories/chouette_journey_pattern.rb index bf55b286f..62241f313 100644 --- a/spec/factories/chouette_journey_pattern.rb +++ b/spec/factories/chouette_journey_pattern.rb @@ -1,14 +1,14 @@ FactoryGirl.define do - + factory :journey_pattern_common, :class => Chouette::JourneyPattern do sequence(:name) { |n| "jp name #{n}" } sequence(:published_name) { |n| "jp publishedname #{n}" } sequence(:comment) { |n| "jp comment #{n}" } sequence(:registration_number) { |n| "jp registration_number #{n}" } sequence(:objectid) { |n| "test:JourneyPattern:#{n}" } - + association :route, :factory => :route - + factory :journey_pattern do after(:create) do |j| j.stop_point_ids = j.route.stop_points.map(&:id) @@ -16,7 +16,7 @@ FactoryGirl.define do j.arrival_stop_point_id = j.route.stop_points.last.id end end - + factory :journey_pattern_odd do after(:create) do |j| j.stop_point_ids = j.route.stop_points.select { |sp| sp.position%2==0}.map(&:id) @@ -24,7 +24,7 @@ FactoryGirl.define do j.arrival_stop_point_id = j.stop_point_ids.last end end - + factory :journey_pattern_even do after(:create) do |j| j.stop_point_ids = j.route.stop_points.select { |sp| sp.position%2==1}.map(&:id) diff --git a/spec/factories/chouette_routes.rb b/spec/factories/chouette_routes.rb index c1a9423c5..a707bcbf6 100644 --- a/spec/factories/chouette_routes.rb +++ b/spec/factories/chouette_routes.rb @@ -18,6 +18,7 @@ FactoryGirl.define do after(:create) do |route, evaluator| create_list(:stop_point, evaluator.stop_points_count, route: route) + route.reload end factory :route_with_journey_patterns do diff --git a/spec/factories/chouette_time_table.rb b/spec/factories/chouette_time_table.rb index 6480df79d..b410d4ab8 100644 --- a/spec/factories/chouette_time_table.rb +++ b/spec/factories/chouette_time_table.rb @@ -1,12 +1,4 @@ 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 - end - factory :time_table, :class => Chouette::TimeTable do sequence(:comment) { |n| "Timetable #{n}" } sequence(:objectid) { |n| "test:Timetable:#{n}" } diff --git a/spec/factories/chouette_time_table_dates.rb b/spec/factories/chouette_time_table_dates.rb new file mode 100644 index 000000000..62fdb3917 --- /dev/null +++ b/spec/factories/chouette_time_table_dates.rb @@ -0,0 +1,5 @@ +FactoryGirl.define do + factory :time_table_date, class: Chouette::TimeTableDate do + association :time_table + end +end diff --git a/spec/factories/chouette_time_table_periods.rb b/spec/factories/chouette_time_table_periods.rb new file mode 100644 index 000000000..66640bbcc --- /dev/null +++ b/spec/factories/chouette_time_table_periods.rb @@ -0,0 +1,7 @@ +FactoryGirl.define do + factory :time_table_period, class: Chouette::TimeTablePeriod do + association :time_table + period_start { Date.today } + period_end { Date.today + 1.month } + end +end diff --git a/spec/factories/chouette_vehicle_journey.rb b/spec/factories/chouette_vehicle_journey.rb index e7ecb79ac..d1e00cd1d 100644 --- a/spec/factories/chouette_vehicle_journey.rb +++ b/spec/factories/chouette_vehicle_journey.rb @@ -11,6 +11,7 @@ FactoryGirl.define do end factory :vehicle_journey do + association :company, factory: :company transient do stop_arrival_time '01:00:00' stop_departure_time '03:00:00' diff --git a/spec/factories/chouette_vehicle_journey_at_stop.rb b/spec/factories/chouette_vehicle_journey_at_stop.rb index c452a1317..831e347d4 100644 --- a/spec/factories/chouette_vehicle_journey_at_stop.rb +++ b/spec/factories/chouette_vehicle_journey_at_stop.rb @@ -1,8 +1,9 @@ FactoryGirl.define do - factory :vehicle_journey_at_stop, :class => Chouette::VehicleJourneyAtStop do association :vehicle_journey, :factory => :vehicle_journey + departure_day_offset { 0 } + departure_time { Time.now } + arrival_time { Time.now - 1.hour } end - end diff --git a/spec/factories/clean_up_results.rb b/spec/factories/clean_up_results.rb deleted file mode 100644 index 6d3818eff..000000000 --- a/spec/factories/clean_up_results.rb +++ /dev/null @@ -1,9 +0,0 @@ -FactoryGirl.define do - factory :clean_up_result do - criticity 1 -message_key "MyString" -message_attributs "" -cleanup nil - end - -end diff --git a/spec/factories/import_messages.rb b/spec/factories/import_messages.rb deleted file mode 100644 index 1101107d2..000000000 --- a/spec/factories/import_messages.rb +++ /dev/null @@ -1,11 +0,0 @@ -FactoryGirl.define do - factory :import_message do - criticity 1 - message_key "MyString" - message_attributs "" - import nil - resource nil - resource_attributes {} - end - -end diff --git a/spec/factories/import_tasks.rb b/spec/factories/import_tasks.rb deleted file mode 100644 index 9ca6db899..000000000 --- a/spec/factories/import_tasks.rb +++ /dev/null @@ -1,10 +0,0 @@ -FactoryGirl.define do - factory :import_task do |f| - user_name "dummy" - user_id 123 - no_save false - format "Neptune" - resources { Rack::Test::UploadedFile.new 'spec/fixtures/neptune.zip', 'application/zip', false } - referential { Referential.find_by_slug("first") } - end -end diff --git a/spec/factories/imports.rb b/spec/factories/imports.rb index fc8668606..2c53106c3 100644 --- a/spec/factories/imports.rb +++ b/spec/factories/imports.rb @@ -9,5 +9,10 @@ FactoryGirl.define do status :new started_at nil ended_at nil + creator 'rspec' + + after(:build) do |import| + import.class.skip_callback(:create, :before, :initialize_fields) + end end end diff --git a/spec/factories/netex_imports.rb b/spec/factories/netex_imports.rb new file mode 100644 index 000000000..057e47730 --- /dev/null +++ b/spec/factories/netex_imports.rb @@ -0,0 +1,5 @@ +FactoryGirl.define do + factory :netex_import, class: NetexImport, parent: :import do + file { File.open(Rails.root.join('spec', 'fixtures', 'terminated_job.json')) } + end +end diff --git a/spec/factories/workbench_imports.rb b/spec/factories/workbench_imports.rb new file mode 100644 index 000000000..5cdcfd15f --- /dev/null +++ b/spec/factories/workbench_imports.rb @@ -0,0 +1,5 @@ +FactoryGirl.define do + factory :workbench_import, class: WorkbenchImport, parent: :import do + file { File.open(Rails.root.join('spec', 'fixtures', 'terminated_job.json')) } + end +end diff --git a/spec/fixtures/multiple_references_import.zip b/spec/fixtures/multiple_references_import.zip Binary files differnew file mode 100644 index 000000000..28ddff198 --- /dev/null +++ b/spec/fixtures/multiple_references_import.zip diff --git a/spec/fixtures/neptune.zip b/spec/fixtures/neptune.zip Binary files differdeleted file mode 100644 index 86b688b51..000000000 --- a/spec/fixtures/neptune.zip +++ /dev/null diff --git a/spec/fixtures/nozip.zip b/spec/fixtures/nozip.zip new file mode 100644 index 000000000..505bd213a --- /dev/null +++ b/spec/fixtures/nozip.zip @@ -0,0 +1 @@ +no zip file diff --git a/spec/fixtures/single_reference_import.zip b/spec/fixtures/single_reference_import.zip Binary files differnew file mode 100644 index 000000000..4aee23614 --- /dev/null +++ b/spec/fixtures/single_reference_import.zip diff --git a/spec/javascripts/time_table/actions_spec.js b/spec/javascripts/time_table/actions_spec.js index 9756e797f..3e0c38c4b 100644 --- a/spec/javascripts/time_table/actions_spec.js +++ b/spec/javascripts/time_table/actions_spec.js @@ -157,33 +157,39 @@ describe('actions', () => { let modalProps = {} let timeTablePeriods = [] let metas = {} + let timetableInDates = [] const expectedAction = { type: 'VALIDATE_PERIOD_FORM', modalProps, timeTablePeriods, - metas + metas, + timetableInDates } - expect(actions.validatePeriodForm(modalProps, timeTablePeriods, metas)).toEqual(expectedAction) + expect(actions.validatePeriodForm(modalProps, timeTablePeriods, metas, timetableInDates)).toEqual(expectedAction) }) it('should create an action to include date in period', () => { let index = 1 + let date = actions.formatDate(new Date) const expectedAction = { type: 'INCLUDE_DATE_IN_PERIOD', index, - dayTypes + dayTypes, + date } - expect(actions.includeDateInPeriod(index, dayTypes)).toEqual(expectedAction) + expect(actions.includeDateInPeriod(index, dayTypes, date)).toEqual(expectedAction) }) it('should create an action to exclude date from period', () => { let index = 1 + let date = actions.formatDate(new Date) const expectedAction = { type: 'EXCLUDE_DATE_FROM_PERIOD', index, - dayTypes + dayTypes, + date } - expect(actions.excludeDateFromPeriod(index, dayTypes)).toEqual(expectedAction) + expect(actions.excludeDateFromPeriod(index, dayTypes, date)).toEqual(expectedAction) }) it('should create an action to open confirm modal', () => { diff --git a/spec/javascripts/time_table/reducers/modal_spec.js b/spec/javascripts/time_table/reducers/modal_spec.js index 4246027b8..160f3955f 100644 --- a/spec/javascripts/time_table/reducers/modal_spec.js +++ b/spec/javascripts/time_table/reducers/modal_spec.js @@ -170,12 +170,14 @@ describe('modal reducer', () => { } let ttperiods = [] + let ttdates = [] expect( modalReducer(state, { type: 'VALIDATE_PERIOD_FORM', modalProps : modProps, - timeTablePeriods: ttperiods + timeTablePeriods: ttperiods, + timetableInDates: ttdates }) ).toEqual(Object.assign({}, state, {modalProps: newModalProps})) }) @@ -222,6 +224,8 @@ describe('modal reducer', () => { {id: 265, period_start: '2017-05-14', period_end: '2017-05-24'} ] + let ttdates2 = [] + let newModalProps2 = { active: true, begin: { @@ -242,8 +246,74 @@ describe('modal reducer', () => { modalReducer(state2, { type: 'VALIDATE_PERIOD_FORM', modalProps : modProps2, - timeTablePeriods: ttperiods2 + timeTablePeriods: ttperiods2, + timetableInDates: ttdates2 }) ).toEqual(Object.assign({}, state2, {modalProps: newModalProps2})) }) + + it('should handle VALIDATE_PERIOD_FORM and throw error if period overlaps date', () => { + let state3 = { + confirmModal: {}, + modalProps: { + active: false, + begin: { + day: '01', + month: '08', + year: '2017' + }, + end: { + day: '09', + month: '08', + year: '2017' + }, + index: false, + error: '' + }, + type: '' + } + let modProps3 = { + active: false, + begin: { + day: '01', + month: '08', + year: '2017' + }, + end: { + day: '09', + month: '08', + year: '2017' + }, + index: false, + error: '' + } + let ttperiods3 = [] + + let ttdates3 = [{date: "2017-08-04", include_date: true}] + + let newModalProps3 = { + active: true, + begin: { + day: '01', + month: '08', + year: '2017' + }, + end: { + day: '09', + month: '08', + year: '2017' + }, + index: false, + error: "Une période ne peut chevaucher une date dans un calendrier" + } + + expect( + modalReducer(state3, { + type: 'VALIDATE_PERIOD_FORM', + modalProps : modProps3, + timeTablePeriods: ttperiods3, + timetableInDates: ttdates3 + }) + ).toEqual(Object.assign({}, state3, {modalProps: newModalProps3})) + }) }) diff --git a/spec/javascripts/time_table/reducers/timetable_spec.js b/spec/javascripts/time_table/reducers/timetable_spec.js index 0b418a52e..805a29b5f 100644 --- a/spec/javascripts/time_table/reducers/timetable_spec.js +++ b/spec/javascripts/time_table/reducers/timetable_spec.js @@ -12,12 +12,15 @@ let current_month = [{"day":"lundi","date":"2017-05-01","wday":1,"wnumber":"18", let newCurrentMonth = [{"day":"lundi","date":"2017-05-01","wday":1,"wnumber":"18","mday":1,"include_date":false,"excluded_date":false,"in_periods":true},{"day":"mardi","date":"2017-05-02","wday":2,"wnumber":"18","mday":2,"include_date":false,"excluded_date":false,"in_periods":true},{"day":"mercredi","date":"2017-05-03","wday":3,"wnumber":"18","mday":3,"include_date":false,"excluded_date":false,"in_periods":true},{"day":"jeudi","date":"2017-05-04","wday":4,"wnumber":"18","mday":4,"include_date":false,"excluded_date":false,"in_periods":true},{"day":"vendredi","date":"2017-05-05","wday":5,"wnumber":"18","mday":5,"include_date":false,"excluded_date":false,"in_periods":false},{"day":"samedi","date":"2017-05-06","wday":6,"wnumber":"18","mday":6,"include_date":false,"excluded_date":false,"in_periods":false},{"day":"dimanche","date":"2017-05-07","wday":0,"wnumber":"18","mday":7,"include_date":false,"excluded_date":false,"in_periods":false},{"day":"lundi","date":"2017-05-08","wday":1,"wnumber":"19","mday":8,"include_date":false,"excluded_date":false,"in_periods":false},{"day":"mardi","date":"2017-05-09","wday":2,"wnumber":"19","mday":9,"include_date":false,"excluded_date":false,"in_periods":false},{"day":"mercredi","date":"2017-05-10","wday":3,"wnumber":"19","mday":10,"include_date":false,"excluded_date":false,"in_periods":false},{"day":"jeudi","date":"2017-05-11","wday":4,"wnumber":"19","mday":11,"include_date":false,"excluded_date":false,"in_periods":false},{"day":"vendredi","date":"2017-05-12","wday":5,"wnumber":"19","mday":12,"include_date":false,"excluded_date":false,"in_periods":false},{"day":"samedi","date":"2017-05-13","wday":6,"wnumber":"19","mday":13,"include_date":false,"excluded_date":false,"in_periods":false},{"day":"dimanche","date":"2017-05-14","wday":0,"wnumber":"19","mday":14,"include_date":false,"excluded_date":false,"in_periods":true},{"day":"lundi","date":"2017-05-15","wday":1,"wnumber":"20","mday":15,"include_date":false,"excluded_date":false,"in_periods":true},{"day":"mardi","date":"2017-05-16","wday":2,"wnumber":"20","mday":16,"include_date":false,"excluded_date":false,"in_periods":true},{"day":"mercredi","date":"2017-05-17","wday":3,"wnumber":"20","mday":17,"include_date":false,"excluded_date":false,"in_periods":true},{"day":"jeudi","date":"2017-05-18","wday":4,"wnumber":"20","mday":18,"include_date":false,"excluded_date":false,"in_periods":true},{"day":"vendredi","date":"2017-05-19","wday":5,"wnumber":"20","mday":19,"include_date":false,"excluded_date":false,"in_periods":true},{"day":"samedi","date":"2017-05-20","wday":6,"wnumber":"20","mday":20,"include_date":false,"excluded_date":false,"in_periods":true},{"day":"dimanche","date":"2017-05-21","wday":0,"wnumber":"20","mday":21,"include_date":false,"excluded_date":false,"in_periods":true},{"day":"lundi","date":"2017-05-22","wday":1,"wnumber":"21","mday":22,"include_date":false,"excluded_date":false,"in_periods":true},{"day":"mardi","date":"2017-05-23","wday":2,"wnumber":"21","mday":23,"include_date":false,"excluded_date":false,"in_periods":true},{"day":"mercredi","date":"2017-05-24","wday":3,"wnumber":"21","mday":24,"include_date":false,"excluded_date":false,"in_periods":true},{"day":"jeudi","date":"2017-05-25","wday":4,"wnumber":"21","mday":25,"include_date":false,"excluded_date":false,"in_periods":false},{"day":"vendredi","date":"2017-05-26","wday":5,"wnumber":"21","mday":26,"include_date":false,"excluded_date":false,"in_periods":false},{"day":"samedi","date":"2017-05-27","wday":6,"wnumber":"21","mday":27,"include_date":false,"excluded_date":false,"in_periods":false},{"day":"dimanche","date":"2017-05-28","wday":0,"wnumber":"21","mday":28,"include_date":false,"excluded_date":false,"in_periods":false},{"day":"lundi","date":"2017-05-29","wday":1,"wnumber":"22","mday":29,"include_date":false,"excluded_date":false,"in_periods":false},{"day":"mardi","date":"2017-05-30","wday":2,"wnumber":"22","mday":30,"include_date":false,"excluded_date":false,"in_periods":false},{"day":"mercredi","date":"2017-05-31","wday":3,"wnumber":"22","mday":31,"include_date":false,"excluded_date":false,"in_periods":false}] +let time_table_dates = [] + let json = { current_month: current_month, current_periode_range: current_periode_range, periode_range: periode_range, time_table_periods: time_table_periods, - day_types: strDayTypes + day_types: strDayTypes, + time_table_dates: time_table_dates } describe('timetable reducer with empty state', () => { @@ -26,7 +29,8 @@ describe('timetable reducer with empty state', () => { current_month: [], current_periode_range: "", periode_range: [], - time_table_periods: [] + time_table_periods: [], + time_table_dates: [] } }) @@ -42,6 +46,7 @@ describe('timetable reducer with empty state', () => { current_periode_range: current_periode_range, periode_range: periode_range, time_table_periods: time_table_periods, + time_table_dates: time_table_dates } expect( timetableReducer(state, { @@ -59,6 +64,7 @@ describe('timetable reducer with filled state', () => { current_periode_range: current_periode_range, periode_range: periode_range, time_table_periods: time_table_periods, + time_table_dates: time_table_dates } }) @@ -130,45 +136,79 @@ describe('timetable reducer with filled state', () => { ).toEqual(state) }) - it('should handle INCLUDE_DATE_IN_PERIOD', () => { + it('should handle INCLUDE_DATE_IN_PERIOD and add in_day if TT doesnt have it', () => { + let newDates = state.time_table_dates.concat({date: "2017-05-05", in_out: true}) + let newState = Object.assign({}, state, {time_table_dates: newDates}) state.current_month[4].include_date = true expect( timetableReducer(state, { type: 'INCLUDE_DATE_IN_PERIOD', index: 4, - dayTypes: arrDayTypes + dayTypes: arrDayTypes, + date: "2017-05-05" }) - ).toEqual(state) + ).toEqual(newState) + }) + + it('should handle INCLUDE_DATE_IN_PERIOD and remove in_day if TT has it', () => { + state.current_month[4].include_date = true + state.time_table_dates.push({date: "2017-05-05", in_out: true}) + let newState = Object.assign({}, state, {time_table_dates: []}) + expect( + timetableReducer(state, { + type: 'INCLUDE_DATE_IN_PERIOD', + index: 4, + dayTypes: arrDayTypes, + date: "2017-05-05" + }) + ).toEqual(newState) }) - it('should handle EXCLUDE_DATE_FROM_PERIOD', () => { + it('should handle EXCLUDE_DATE_FROM_PERIOD and add out_day if TT doesnt have it', () => { + let newDates = state.time_table_dates.concat({date: "2017-05-01", in_out: false}) + let newState = Object.assign({}, state, {time_table_dates: newDates}) state.current_month[0].excluded_date = true expect( timetableReducer(state, { type: 'EXCLUDE_DATE_FROM_PERIOD', index: 0, - dayTypes: arrDayTypes + dayTypes: arrDayTypes, + date: "2017-05-01" }) - ).toEqual(state) + ).toEqual(newState) + }) + + it('should handle EXCLUDE_DATE_FROM_PERIOD and remove out_day if TT has it', () => { + state.time_table_dates = [{date: "2017-05-01", in_out: false}] + state.current_month[0].excluded_date = true + let newState = Object.assign({}, state, {time_table_dates: []}) + expect( + timetableReducer(state, { + type: 'EXCLUDE_DATE_FROM_PERIOD', + index: 0, + dayTypes: arrDayTypes, + date: "2017-05-01" + }) + ).toEqual(newState) }) - it('should handle VALIDATE_PERIOD_FORM', () => { - state.current_month[13].in_periods = false - state.time_table_periods[4].period_start = '2017-05-15' + it('should handle VALIDATE_PERIOD_FORM and add period if modalProps index = false', () => { + let newPeriods = state.time_table_periods.concat({"period_start": "2018-05-15", "period_end": "2018-05-24"}) + let newState = Object.assign({}, state, {time_table_periods: newPeriods}) let modalProps = { active: false, begin: { day: '15', month: '05', - year: '2017' + year: '2018' }, end: { day: '24', month: '05', - year: '2017' + year: '2018' }, error: '', - index: 4 + index: false } expect( timetableReducer(state, { @@ -177,8 +217,9 @@ describe('timetable reducer with filled state', () => { timeTablePeriods: state.time_table_periods, metas: { day_types: arrDayTypes - } + }, + timetableInDates: state.time_table_dates.filter(d => d.in_out == true) }) - ).toEqual(state) + ).toEqual(newState) }) }) diff --git a/spec/javascripts/vehicle_journeys/actions_spec.js b/spec/javascripts/vehicle_journeys/actions_spec.js index d96baf8ef..707ae22cb 100644 --- a/spec/javascripts/vehicle_journeys/actions_spec.js +++ b/spec/javascripts/vehicle_journeys/actions_spec.js @@ -165,12 +165,12 @@ describe('when updating vjas time', () => { }) describe('when clicking on validate button inside shifting modal', () => { it('should create an action to shift a vehiclejourney schedule', () => { - const data = {} + const addtionalTime = 0 const expectedAction = { type: 'SHIFT_VEHICLEJOURNEY', - data + addtionalTime } - expect(actions.shiftVehicleJourney(data)).toEqual(expectedAction) + expect(actions.shiftVehicleJourney(addtionalTime)).toEqual(expectedAction) }) }) describe('when clicking on validate button inside editing modal', () => { @@ -187,14 +187,16 @@ describe('when clicking on validate button inside editing modal', () => { }) describe('when clicking on validate button inside duplicating modal', () => { it('should create an action to duplicate a vehiclejourney schedule', () => { - const data = {} + const addtionalTime = 0 const departureDelta = 0 + const duplicateNumber = 1 const expectedAction = { type: 'DUPLICATE_VEHICLEJOURNEY', - data, + addtionalTime, + duplicateNumber, departureDelta } - expect(actions.duplicateVehicleJourney(data, departureDelta)).toEqual(expectedAction) + expect(actions.duplicateVehicleJourney(addtionalTime, duplicateNumber, departureDelta)).toEqual(expectedAction) }) }) describe('when clicking on edit notes modal', () => { @@ -432,3 +434,16 @@ describe('when using select2 to pick a company', () => { expect(actions.select2Company(selectedCompany)).toEqual(expectedAction) }) }) +describe('when using select2 to unselect a company', () => { + it('should create an action to unselect a company inside modal', () => { + let selectedCompany = { + id: 1, + objectid: 2, + name: 'test', + } + const expectedAction = { + type: 'UNSELECT_CP_EDIT_MODAL' + } + expect(actions.unselect2Company()).toEqual(expectedAction) + }) +}) diff --git a/spec/javascripts/vehicle_journeys/reducers/modal_spec.js b/spec/javascripts/vehicle_journeys/reducers/modal_spec.js index c016812da..4530b5ee7 100644 --- a/spec/javascripts/vehicle_journeys/reducers/modal_spec.js +++ b/spec/javascripts/vehicle_journeys/reducers/modal_spec.js @@ -167,4 +167,13 @@ describe('modal reducer', () => { }) ).toEqual(Object.assign({}, state, {modalProps: newModalProps})) }) + + it('should handle UNSELECT_CP_EDIT_MODAL', () => { + let newModalProps = {selectedCompany : undefined} + expect( + modalReducer(state, { + type: 'UNSELECT_CP_EDIT_MODAL' + }) + ).toEqual(Object.assign({}, state, {modalProps: newModalProps})) + }) }) diff --git a/spec/javascripts/vehicle_journeys/reducers/vehicle_journeys_spec.js b/spec/javascripts/vehicle_journeys/reducers/vehicle_journeys_spec.js index 620e2ffdd..3b2137a2a 100644 --- a/spec/javascripts/vehicle_journeys/reducers/vehicle_journeys_spec.js +++ b/spec/javascripts/vehicle_journeys/reducers/vehicle_journeys_spec.js @@ -198,15 +198,12 @@ describe('vehicleJourneys reducer', () => { }, stop_area_object_id : "FR:92024:ZDE:420553:STIF" }] - let fakeData = { - objectid: {value : '11'}, - additional_time: {value: '5'} - } + let addtionalTime = 5 let newVJ = Object.assign({}, state[0], {vehicle_journey_at_stops: newVJAS}) expect( vjReducer(state, { type: 'SHIFT_VEHICLEJOURNEY', - data: fakeData + addtionalTime }) ).toEqual([newVJ, state[1]]) }) @@ -225,17 +222,17 @@ describe('vehicleJourneys reducer', () => { stop_area_object_id : "FR:92024:ZDE:420553:STIF" }] let departureDelta = 1 - let fakeData = { - duplicate_number: {value : 1}, - additional_time: {value: '5'} - } + let addtionalTime = 5 + let duplicateNumber = 1 + let newVJ = Object.assign({}, state[0], {vehicle_journey_at_stops: newVJAS, selected: false}) newVJ.published_journey_name = state[0].published_journey_name + '-0' delete newVJ['objectid'] expect( vjReducer(state, { type: 'DUPLICATE_VEHICLEJOURNEY', - data: fakeData, + addtionalTime, + duplicateNumber, departureDelta }) ).toEqual([state[0], newVJ, state[1]]) diff --git a/spec/lib/result_spec.rb b/spec/lib/result_spec.rb new file mode 100644 index 000000000..949de163c --- /dev/null +++ b/spec/lib/result_spec.rb @@ -0,0 +1,20 @@ +RSpec.describe Result do + + context 'is a wrapper of a value' do + it { expect( described_class.ok('hello').value ).to eq('hello') } + it { expect( described_class.error('hello').value ).to eq('hello') } + end + + context 'it has status information' do + it { expect( described_class.ok('hello') ).to be_ok } + it { expect( described_class.ok('hello').status ).to eq(:ok) } + + it { expect( described_class.error('hello') ).not_to be_ok } + it { expect( described_class.error('hello').status ).to eq(:error) } + end + + context 'nil is just another value' do + it { expect( described_class.ok(nil) ).to be_ok } + it { expect( described_class.ok(nil).value ).to be_nil } + end +end diff --git a/spec/models/api/v1/api_key_spec.rb b/spec/models/api/v1/api_key_spec.rb index eb8826c0e..b700429d3 100644 --- a/spec/models/api/v1/api_key_spec.rb +++ b/spec/models/api/v1/api_key_spec.rb @@ -1,13 +1,25 @@ -require 'spec_helper' +require 'rails_helper' -describe Api::V1::ApiKey, :type => :model do - let!(:referential){create(:referential)} - subject { Api::V1::ApiKey.create( :name => "test", :referential => referential)} +RSpec.describe Api::V1::ApiKey, type: :model do + subject { create(:api_key) } - it "test" do - expect(subject).to be_valid - expect(subject.referential).to eq(referential) + it { should validate_presence_of :organisation } + it 'should have a valid factory' do + expect(build(:api_key)).to be_valid + end + + describe '#referential_from_token' do + it 'should return referential' do + referential = Api::V1::ApiKey.referential_from_token(subject.token) + expect(referential).to eq(subject.referential) + end end -end + describe '#organisation_from_token' do + it 'should return organisation' do + organisation = Api::V1::ApiKey.organisation_from_token(subject.token) + expect(organisation).to eq(subject.organisation) + end + end +end diff --git a/spec/models/chouette/footnote_spec.rb b/spec/models/chouette/footnote_spec.rb index 5c09e3931..98d751499 100644 --- a/spec/models/chouette/footnote_spec.rb +++ b/spec/models/chouette/footnote_spec.rb @@ -1,9 +1,22 @@ require 'spec_helper' -describe Chouette::Footnote do - - subject { build(:footnote) } +describe Chouette::Footnote, type: :model do + let(:footnote) { create(:footnote) } it { should validate_presence_of :line } + describe 'checksum' do + it_behaves_like 'checksum support', :footnote + + context '#checksum_attributes' do + it 'should return code and label' do + expected = [footnote.code, footnote.label] + expect(footnote.checksum_attributes).to include(*expected) + end + + it 'should not return other atrributes' do + expect(footnote.checksum_attributes).to_not include(footnote.updated_at) + end + end + end end diff --git a/spec/models/chouette/journey_pattern_spec.rb b/spec/models/chouette/journey_pattern_spec.rb index 26d220056..047022ade 100644 --- a/spec/models/chouette/journey_pattern_spec.rb +++ b/spec/models/chouette/journey_pattern_spec.rb @@ -1,6 +1,9 @@ require 'spec_helper' describe Chouette::JourneyPattern, :type => :model do + describe 'checksum' do + it_behaves_like 'checksum support', :journey_pattern + end # context 'validate minimum stop_points size' do # let(:journey_pattern) { create :journey_pattern } diff --git a/spec/models/chouette/route/route_base_spec.rb b/spec/models/chouette/route/route_base_spec.rb index 08f201022..c93b311ff 100644 --- a/spec/models/chouette/route/route_base_spec.rb +++ b/spec/models/chouette/route/route_base_spec.rb @@ -1,6 +1,9 @@ RSpec.describe Chouette::Route, :type => :model do subject { create(:route) } + describe 'checksum' do + it_behaves_like 'checksum support', :route + end describe '#objectid' do subject { super().objectid } @@ -62,8 +65,4 @@ RSpec.describe Chouette::Route, :type => :model do end end end - end - - - diff --git a/spec/models/chouette/routing_constraint_zone_spec.rb b/spec/models/chouette/routing_constraint_zone_spec.rb index 0165c369d..054cfb9e6 100644 --- a/spec/models/chouette/routing_constraint_zone_spec.rb +++ b/spec/models/chouette/routing_constraint_zone_spec.rb @@ -9,6 +9,10 @@ describe Chouette::RoutingConstraintZone, type: :model do # shoulda matcher to validate length of array ? xit { is_expected.to validate_length_of(:stop_point_ids).is_at_least(2) } + describe 'checksum' do + it_behaves_like 'checksum support', :routing_constraint_zone + end + describe 'validations' do it 'validates the presence of route_id' do expect { diff --git a/spec/models/chouette/time_table_period_spec.rb b/spec/models/chouette/time_table_period_spec.rb index 07dc602cb..cc1a3ae09 100644 --- a/spec/models/chouette/time_table_period_spec.rb +++ b/spec/models/chouette/time_table_period_spec.rb @@ -4,11 +4,15 @@ describe Chouette::TimeTablePeriod, :type => :model do let!(:time_table) { create(:time_table)} subject { create(:time_table_period ,:time_table => time_table, :period_start => Date.new(2014,6,30), :period_end => Date.new(2014,7,6) ) } - let!(:p2) {create(:time_table_period ,:time_table => time_table, :period_start => Date.new(2014,7,6), :period_end => Date.new(2014,7,14) ) } + let!(:p2) {create(:time_table_period ,:time_table => time_table, :period_start => Date.new(2014,7,6), :period_end => Date.new(2014,7,14) ) } it { is_expected.to validate_presence_of :period_start } it { is_expected.to validate_presence_of :period_end } - + + describe 'checksum' do + it_behaves_like 'checksum support', :time_table_period + end + describe "#overlap" do context "when periods intersect, " do it "should detect period overlap" do diff --git a/spec/models/chouette/time_table_spec.rb b/spec/models/chouette/time_table_spec.rb index f13e13d52..c4eaeaaf0 100644 --- a/spec/models/chouette/time_table_spec.rb +++ b/spec/models/chouette/time_table_spec.rb @@ -840,252 +840,15 @@ end :period_end => Date.new(2013, 05, 30)) expect(time_table.intersects([ Date.new(2013, 05, 27), Date.new(2013, 05, 28)])).to eq([ Date.new(2013, 05, 27), Date.new(2013, 05, 28)]) end - - - end - - describe "#include_day?" do - it "should return true if a date equal day" do - time_table = Chouette::TimeTable.create!(:comment => "Test", :objectid => "test:Timetable:1") - time_table.dates << Chouette::TimeTableDate.new( :date => Date.today, :in_out => true) - expect(time_table.include_day?(Date.today)).to eq(true) - end - - it "should return true if a period include day" do - time_table = Chouette::TimeTable.create!(:comment => "Test", :objectid => "test:Timetable:1", :int_day_types => 12) # Day type monday and tuesday - time_table.periods << Chouette::TimeTablePeriod.new( - :period_start => Date.new(2013, 05, 27), - :period_end => Date.new(2013, 05, 29)) - expect(time_table.include_day?( Date.new(2013, 05, 27))).to eq(true) - end - end - - describe "#include_in_dates?" do - it "should return true if a date equal day" do - time_table = Chouette::TimeTable.create!(:comment => "Test", :objectid => "test:Timetable:1") - time_table.dates << Chouette::TimeTableDate.new( :date => Date.today, :in_out => true) - expect(time_table.include_in_dates?(Date.today)).to eq(true) - end - - it "should return false if a period include day but that is exclued" do - time_table = Chouette::TimeTable.create!(:comment => "Test", :objectid => "test:Timetable:1", :int_day_types => 12) # Day type monday and tuesday - excluded_date = Date.new(2013, 05, 27) - time_table.dates << Chouette::TimeTableDate.new( :date => excluded_date, :in_out => false) - expect(time_table.include_in_dates?( excluded_date)).to be_falsey - end - end - - describe "#include_in_periods?" do - it "should return true if a period include day" do - time_table = Chouette::TimeTable.create!(:comment => "Test", :objectid => "test:Timetable:1", :int_day_types => 4) - time_table.periods << Chouette::TimeTablePeriod.new( - :period_start => Date.new(2012, 1, 1), - :period_end => Date.new(2012, 01, 30)) - expect(time_table.include_in_periods?(Date.new(2012, 1, 2))).to eq(true) - end - - it "should return false if a period include day but that is exclued" do - time_table = Chouette::TimeTable.create!(:comment => "Test", :objectid => "test:Timetable:1", :int_day_types => 12) # Day type monday and tuesday - excluded_date = Date.new(2013, 05, 27) - time_table.dates << Chouette::TimeTableDate.new( :date => excluded_date, :in_out => false) - time_table.periods << Chouette::TimeTablePeriod.new( - :period_start => Date.new(2013, 05, 27), - :period_end => Date.new(2013, 05, 29)) - expect(time_table.include_in_periods?( excluded_date)).to be_falsey - end - end - - describe "#include_in_overlap_dates?" do - it "should return true if a day is included in overlap dates" do - time_table = Chouette::TimeTable.create!(:comment => "Test", :objectid => "test:Timetable:1", :int_day_types => 4) - time_table.periods << Chouette::TimeTablePeriod.new( - :period_start => Date.new(2012, 1, 1), - :period_end => Date.new(2012, 01, 30)) - time_table.dates << Chouette::TimeTableDate.new( :date => Date.new(2012, 1, 2), :in_out => true) - expect(time_table.include_in_overlap_dates?(Date.new(2012, 1, 2))).to eq(true) - end - it "should return false if the day is excluded" do - time_table = Chouette::TimeTable.create!(:comment => "Test", :objectid => "test:Timetable:1", :int_day_types => 4) - time_table.periods << Chouette::TimeTablePeriod.new( - :period_start => Date.new(2012, 1, 1), - :period_end => Date.new(2012, 01, 30)) - time_table.dates << Chouette::TimeTableDate.new( :date => Date.new(2012, 1, 2), :in_out => false) - expect(time_table.include_in_overlap_dates?(Date.new(2012, 1, 2))).to be_falsey - end - end - - describe "#dates" do - it "should have with position 0" do - expect(subject.dates.first.position).to eq(0) - end - context "when first date has been removed" do - before do - subject.dates.first.destroy - end - it "should begin with position 0" do - expect(subject.dates.first.position).to eq(0) - end - end - end - describe "#validity_out_between?" do - let(:empty_tm) {build(:time_table)} - it "should be false if empty calendar" do - expect(empty_tm.validity_out_between?( Date.today, Date.today + 7.day)).to be_falsey - end - it "should be true if caldendar is out during start_date and end_date period" do - start_date = subject.bounding_dates.max - 2.day - end_date = subject.bounding_dates.max + 2.day - expect(subject.validity_out_between?( start_date, end_date)).to be_truthy - end - it "should be false if calendar is out on start date" do - start_date = subject.bounding_dates.max - end_date = subject.bounding_dates.max + 2.day - expect(subject.validity_out_between?( start_date, end_date)).to be_falsey - end - it "should be false if calendar is out on end date" do - start_date = subject.bounding_dates.max - 2.day - end_date = subject.bounding_dates.max - expect(subject.validity_out_between?( start_date, end_date)).to be_truthy - end - it "should be false if calendar is out after start_date" do - start_date = subject.bounding_dates.max + 2.day - end_date = subject.bounding_dates.max + 4.day - expect(subject.validity_out_between?( start_date, end_date)).to be_falsey - end - end - describe "#validity_out_from_on?" do - let(:empty_tm) {build(:time_table)} - it "should be false if empty calendar" do - expect(empty_tm.validity_out_from_on?( Date.today)).to be_falsey - end - it "should be true if caldendar ends on expected date" do - expected_date = subject.bounding_dates.max - expect(subject.validity_out_from_on?( expected_date)).to be_truthy - end - it "should be true if calendar ends before expected date" do - expected_date = subject.bounding_dates.max + 30.day - expect(subject.validity_out_from_on?( expected_date)).to be_truthy - end - it "should be false if calendars ends after expected date" do - expected_date = subject.bounding_dates.max - 30.day - expect(subject.validity_out_from_on?( expected_date)).to be_falsey - end - end - describe "#bounding_dates" do - context "when timetable contains only periods" do - before do - subject.dates = [] - subject.save - end - it "should retreive periods.period_start.min and periods.period_end.max" do - expect(subject.bounding_dates.min).to eq(subject.periods.map(&:period_start).min) - expect(subject.bounding_dates.max).to eq(subject.periods.map(&:period_end).max) - end - end - context "when timetable contains only dates" do - before do - subject.periods = [] - subject.save - end - it "should retreive dates.min and dates.max" do - expect(subject.bounding_dates.min).to eq(subject.dates.map(&:date).min) - expect(subject.bounding_dates.max).to eq(subject.dates.map(&:date).max) - end - end - it "should contains min date" do - min_date = subject.bounding_dates.min - subject.dates.each do |tm_date| - expect(min_date <= tm_date.date).to be_truthy - end - subject.periods.each do |tm_period| - expect(min_date <= tm_period.period_start).to be_truthy - end - - end - it "should contains max date" do - max_date = subject.bounding_dates.max - subject.dates.each do |tm_date| - expect(tm_date.date <= max_date).to be_truthy - end - subject.periods.each do |tm_period| - expect(tm_period.period_end <= max_date).to be_truthy - end - - end - end - describe "#periods" do - it "should begin with position 0" do - expect(subject.periods.first.position).to eq(0) - end - context "when first period has been removed" do - before do - subject.periods.first.destroy - end - it "should begin with position 0" do - expect(subject.periods.first.position).to eq(0) - end - end - it "should have period_start before period_end" do - period = Chouette::TimeTablePeriod.new - period.period_start = Date.today - period.period_end = Date.today + 10 - expect(period.valid?).to be_truthy - end - it "should not have period_start after period_end" do - period = Chouette::TimeTablePeriod.new - period.period_start = Date.today - period.period_end = Date.today - 10 - expect(period.valid?).to be_falsey - end - it "should not have period_start equal to period_end" do - period = Chouette::TimeTablePeriod.new - period.period_start = Date.today - period.period_end = Date.today - expect(period.valid?).to be_falsey - end end - describe "#effective_days_of_periods" do - before do - subject.periods.clear - subject.periods << Chouette::TimeTablePeriod.new( - :period_start => Date.new(2014, 6, 30), - :period_end => Date.new(2014, 7, 6)) - subject.int_day_types = 4|8|16 - end - it "should return monday to wednesday" do - expect(subject.effective_days_of_periods.size).to eq(3) - expect(subject.effective_days_of_periods[0]).to eq(Date.new(2014, 6, 30)) - expect(subject.effective_days_of_periods[1]).to eq(Date.new(2014, 7, 1)) - expect(subject.effective_days_of_periods[2]).to eq(Date.new(2014, 7, 2)) - end - it "should return thursday" do - expect(subject.effective_days_of_periods(Chouette::TimeTable.valid_days(32)).size).to eq(1) - expect(subject.effective_days_of_periods(Chouette::TimeTable.valid_days(32))[0]).to eq(Date.new(2014, 7, 3)) - end - - end + # it { is_expected.to validate_presence_of :comment } + # it { is_expected.to validate_uniqueness_of :objectid } - describe "#included_days" do - before do - subject.dates.clear - subject.dates << Chouette::TimeTableDate.new( :date => Date.new(2014,7,16), :in_out => true) - subject.dates << Chouette::TimeTableDate.new( :date => Date.new(2014,7,17), :in_out => false) - subject.dates << Chouette::TimeTableDate.new( :date => Date.new(2014,7,18), :in_out => true) - subject.dates << Chouette::TimeTableDate.new( :date => Date.new(2014,7,19), :in_out => false) - subject.dates << Chouette::TimeTableDate.new( :date => Date.new(2014,7,20), :in_out => true) - end - it "should return 3 dates" do - days = subject.included_days - expect(days.size).to eq(3) - expect(days[0]).to eq(Date.new(2014, 7, 16)) - expect(days[1]).to eq(Date.new(2014,7, 18)) - expect(days[2]).to eq(Date.new(2014, 7,20)) - end + describe 'checksum' do + it_behaves_like 'checksum support', :time_table end - - describe "#excluded_days" do before do subject.dates.clear @@ -1223,5 +986,4 @@ end expect(subject.tag_list.size).to eq(2) end end - end diff --git a/spec/models/chouette/vehicle_journey_at_stop_spec.rb b/spec/models/chouette/vehicle_journey_at_stop_spec.rb index d999ed1a8..4f9d12730 100644 --- a/spec/models/chouette/vehicle_journey_at_stop_spec.rb +++ b/spec/models/chouette/vehicle_journey_at_stop_spec.rb @@ -1,6 +1,21 @@ require 'spec_helper' RSpec.describe Chouette::VehicleJourneyAtStop, type: :model do + describe 'checksum' do + let(:at_stop) { build_stubbed(:vehicle_journey_at_stop) } + + it_behaves_like 'checksum support', :vehicle_journey_at_stop + + context '#checksum_attributes' do + it 'should return attributes' do + expected = [at_stop.departure_time.to_s(:time), at_stop.arrival_time.to_s(:time)] + expected << at_stop.departure_day_offset.to_s + expected << at_stop.arrival_day_offset.to_s + expect(at_stop.checksum_attributes).to include(*expected) + end + end + end + describe "#day_offset_outside_range?" do let (:at_stop) { build_stubbed(:vehicle_journey_at_stop) } diff --git a/spec/models/chouette/vehicle_journey_spec.rb b/spec/models/chouette/vehicle_journey_spec.rb index 3c04a77cc..52f2ab42d 100644 --- a/spec/models/chouette/vehicle_journey_spec.rb +++ b/spec/models/chouette/vehicle_journey_spec.rb @@ -17,8 +17,13 @@ describe Chouette::VehicleJourney, :type => :model do expect(vehicle_journey).to be_valid end + describe 'checksum' do + it_behaves_like 'checksum support', :vehicle_journey + end + describe "vjas_departure_time_must_be_before_next_stop_arrival_time", skip: "Validation currently commented out because it interferes with day offsets" do + let(:vehicle_journey) { create :vehicle_journey } let(:vjas) { vehicle_journey.vehicle_journey_at_stops } diff --git a/spec/models/import_spec.rb b/spec/models/import_spec.rb index a2855d086..76f945871 100644 --- a/spec/models/import_spec.rb +++ b/spec/models/import_spec.rb @@ -1,10 +1,158 @@ -require 'rails_helper' - RSpec.describe Import, :type => :model do + it { should belong_to(:referential) } it { should belong_to(:workbench) } + it { should belong_to(:parent) } - it { should enumerize(:status).in(:new, :pending, :successful, :failed, :canceled, :running, :aborted ) } + it { should enumerize(:status).in("aborted", "canceled", "failed", "new", "pending", "running", "successful") } it { should validate_presence_of(:file) } + it { should validate_presence_of(:workbench) } + it { should validate_presence_of(:creator) } + + let(:workbench_import) { build_stubbed(:workbench_import) } + let(:workbench_import_with_completed_steps) do + workbench_import = build_stubbed( + :workbench_import, + total_steps: 2, + current_step: 2 + ) + end + + let(:netex_import) do + netex_import = build_stubbed( + :netex_import, + parent: workbench_import + ) + end + + describe "#notify_parent" do + it "must call #child_change on its parent" do + allow(netex_import).to receive(:update) + + expect(workbench_import).to receive(:child_change).with(netex_import) + + netex_import.notify_parent + end + + it "must update the :notified_parent_at field of the child import" do + allow(workbench_import).to receive(:child_change) + + Timecop.freeze(DateTime.now) do + expect(netex_import).to receive(:update).with( + notified_parent_at: DateTime.now + ) + + netex_import.notify_parent + end + end + end + + describe "#child_change" do + shared_examples( + "updates :status to failed when child status indicates failure" + ) do |failure_status| + it "updates :status to failed when child status indicates failure" do + allow(workbench_import).to receive(:update) + + netex_import = build_stubbed( + :netex_import, + parent: workbench_import, + status: failure_status + ) + + expect(workbench_import).to receive(:update).with(status: 'failed') + + workbench_import.child_change(netex_import) + end + end + + include_examples( + "updates :status to failed when child status indicates failure", + "failed" + ) + include_examples( + "updates :status to failed when child status indicates failure", + "aborted" + ) + include_examples( + "updates :status to failed when child status indicates failure", + "canceled" + ) + + it "updates :status to successful when #ready?" do + expect(workbench_import).to receive(:update).with(status: 'successful') + + workbench_import.child_change(netex_import) + end + + it "updates :status to failed when #ready? and child is failed" do + netex_import = build_stubbed( + :netex_import, + parent: workbench_import, + status: :failed + ) + + expect(workbench_import).to receive(:update).with(status: 'failed') + + workbench_import.child_change(netex_import) + end + + shared_examples( + "doesn't update :status if parent import status is finished" + ) do |finished_status| + it "doesn't update :status if parent import status is finished" do + workbench_import = build_stubbed( + :workbench_import, + total_steps: 2, + current_step: 2, + status: finished_status + ) + child = double('Import') + + expect(workbench_import).not_to receive(:update) + + workbench_import.child_change(child) + end + end + + include_examples( + "doesn't update :status if parent import status is finished", + "successful" + ) + include_examples( + "doesn't update :status if parent import status is finished", + "failed" + ) + include_examples( + "doesn't update :status if parent import status is finished", + "aborted" + ) + include_examples( + "doesn't update :status if parent import status is finished", + "canceled" + ) + end + + describe "#ready?" do + it "returns true if #current_step == #total_steps" do + import = build_stubbed( + :import, + total_steps: 4, + current_step: 4 + ) + + expect(import.ready?).to be true + end + + it "returns false if #current_step != #total_steps" do + import = build_stubbed( + :import, + total_steps: 6, + current_step: 3 + ) + + expect(import.ready?).to be false + end + end end diff --git a/spec/models/organisation_spec.rb b/spec/models/organisation_spec.rb index 527f71015..b16324a56 100644 --- a/spec/models/organisation_spec.rb +++ b/spec/models/organisation_spec.rb @@ -1,5 +1,3 @@ -require 'spec_helper' - describe Organisation, :type => :model do it { should validate_presence_of(:name) } it { should validate_uniqueness_of(:code) } @@ -17,7 +15,7 @@ describe Organisation, :type => :model do let(:conf) { Rails.application.config.stif_portail_api } before :each do stub_request(:get, "#{conf[:url]}/api/v1/organizations"). - with(headers: { 'Authorization' => "Token token=\"#{conf[:key]}\"" }). + with(stub_headers(authorization_token: conf[:key])). to_return(body: File.open(File.join(Rails.root, 'spec', 'fixtures', 'organizations.json')), status: 200) end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 3a9ae37e9..51ccfccd3 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -67,7 +67,7 @@ RSpec.describe User, :type => :model do let(:conf) { Rails.application.config.stif_portail_api } before :each do stub_request(:get, "#{conf[:url]}/api/v1/users"). - with(headers: { 'Authorization' => "Token token=\"#{conf[:key]}\"" }). + with(stub_headers(authorization_token: conf[:key])). to_return(body: File.open(File.join(Rails.root, 'spec', 'fixtures', 'users.json')), status: 200) end diff --git a/spec/policies/api_key_policy_spec.rb b/spec/policies/api_key_policy_spec.rb new file mode 100644 index 000000000..5b9d59fa3 --- /dev/null +++ b/spec/policies/api_key_policy_spec.rb @@ -0,0 +1,28 @@ +require 'rails_helper' + +RSpec.describe ApiKeyPolicy do + + let(:user) { User.new } + + subject { described_class } + + permissions ".scope" do + pending "add some examples to (or delete) #{__FILE__}" + end + + permissions :show? do + pending "add some examples to (or delete) #{__FILE__}" + end + + permissions :create? do + pending "add some examples to (or delete) #{__FILE__}" + end + + permissions :update? do + pending "add some examples to (or delete) #{__FILE__}" + end + + permissions :destroy? do + pending "add some examples to (or delete) #{__FILE__}" + end +end diff --git a/spec/requests/api/v1/netex_import_spec.rb b/spec/requests/api/v1/netex_import_spec.rb new file mode 100644 index 000000000..fd5f6d497 --- /dev/null +++ b/spec/requests/api/v1/netex_import_spec.rb @@ -0,0 +1,100 @@ +RSpec.describe "NetexImport", type: :request do + + describe 'POST netex_imports' do + + let( :referential ){ create :referential } + let( :workbench ){ referential.workbench } + + + let( :file_path ){ fixtures_path 'single_reference_import.zip' } + let( :file ){ fixture_file_upload( file_path ) } + + let( :post_request ) do + -> (attributes) do + post api_v1_netex_imports_path(format: :json), + attributes, + authorization + end + end + + let( :legal_attributes ) do + { + name: 'hello world', + file: file, + workbench_id: workbench.id + } + end + + + context 'with correct credentials and correct request' do + let( :authorization ){ authorization_token_header( get_api_key.token ) } + + it 'succeeds' do + post_request.(netex_import: legal_attributes) + expect( response ).to be_success + expect( json_response_body ).to eq( + 'id' => NetexImport.last.id, + 'referential_id' => Referential.last.id, + 'workbench_id' => workbench.id + ) + end + + it 'creates a NetexImport object in the DB' do + expect{ post_request.(netex_import: legal_attributes) }.to change{NetexImport.count}.by(1) + end + + it 'creates a correct Referential' do + legal_attributes # force object creation for correct to change behavior + expect{post_request.(netex_import: legal_attributes)}.to change{Referential.count}.by(1) + Referential.last.tap do | ref | + expect( ref.workbench_id ).to eq(workbench.id) + expect( ref.organisation_id ).to eq(workbench.organisation_id) + end + end + end + + + context 'with incorrect credentials and correct request' do + let( :authorization ){ authorization_token_header( "#{referential.id}-incorrect_token") } + + it 'does not create any DB object and does not succeed' do + legal_attributes # force object creation for correct to change behavior + expect{ post_request.(netex_import: legal_attributes) }.not_to change{Referential.count} + expect( response.status ).to eq(401) + end + + end + + context 'with correct credentials and incorrect request' do + let( :authorization ){ authorization_token_header( get_api_key.token ) } + + shared_examples_for 'illegal attributes' do |bad_attribute, illegal_value=nil, referential_count: 0| + context "missing #{bad_attribute}" do + let!( :illegal_attributes ){ legal_attributes.merge( bad_attribute => illegal_value ) } + it 'does not succeed' do + post_request.(netex_import: illegal_attributes) + expect( response.status ).to eq(406) + expect( json_response_body['errors'][bad_attribute.to_s] ).not_to be_empty + end + + it 'does not create an Import object' do + expect{ post_request.(netex_import: illegal_attributes) }.not_to change{Import.count} + end + + it 'might create a referential' do + expect{ post_request.(netex_import: illegal_attributes) }.to change{Referential.count}.by(referential_count) + end + end + end + + it_behaves_like 'illegal attributes', :file, referential_count: 1 + it_behaves_like 'illegal attributes', :workbench_id + context 'name already taken' do + before do + create :referential, name: 'already taken' + end + it_behaves_like 'illegal attributes', :name, 'already taken' + end + end + end +end diff --git a/spec/routing/api/v1/access_links_routes_spec.rb b/spec/routing/api/v1/access_links_routes_spec.rb new file mode 100644 index 000000000..9164d3f05 --- /dev/null +++ b/spec/routing/api/v1/access_links_routes_spec.rb @@ -0,0 +1,9 @@ +RSpec.describe Api::V1::AccessLinksController, type: :controller do + + it 'routes to index' do + expect( get: '/api/v1/access_links' ).to route_to( + controller: 'api/v1/access_links', + action: 'index' + ) + end +end diff --git a/spec/routing/group_of_lines_spec.rb b/spec/routing/group_of_lines_spec.rb index 2a7262893..01ebeefe4 100644 --- a/spec/routing/group_of_lines_spec.rb +++ b/spec/routing/group_of_lines_spec.rb @@ -1,6 +1,4 @@ -require 'spec_helper' - -describe GroupOfLinesController do +RSpec.describe GroupOfLinesController do describe "routing" do it "not recognize #routes" do expect(get( "/line_referentials/1/group_of_lines/2/routes")).not_to route_to( diff --git a/spec/services/http_service_spec.rb b/spec/services/http_service_spec.rb new file mode 100644 index 000000000..8c8af480c --- /dev/null +++ b/spec/services/http_service_spec.rb @@ -0,0 +1,74 @@ +RSpec.describe HTTPService do + + subject{ described_class } + + %i{host params path result}.each do |param| + let(param){ double(param) } + end + let( :token ){ SecureRandom.hex } + + let( :faraday_connection ){ double('faraday_connection') } + let( :headers ){ {} } + + + context 'get_resource' do + let( :params ){ double('params') } + + it 'sets authorization and returns result' do + expect(Faraday).to receive(:new).with(url: host).and_yield(faraday_connection) + expect(faraday_connection).to receive(:adapter).with(Faraday.default_adapter) + expect(faraday_connection).to receive(:headers).and_return headers + expect(faraday_connection).to receive(:get).with(path, params).and_return(result) + + expect(subject.get_resource(host: host, path: path, token: token, params: params)).to eq(result) + expect(headers['Authorization']).to eq( "Token token=#{token.inspect}" ) + end + end + + context 'post_resource' do + %i{as_name mime_type name upload_io value}.each do | param | + let( param ){ double(param) } + end + + let( :upload_list ){ [value, mime_type, as_name] } + + it 'sets authorization and posts data' do + expect(Faraday::UploadIO).to receive(:new).with(*upload_list).and_return upload_io + expect(params).to receive(:update).with(name => upload_io) + + expect(Faraday).to receive(:new).with(url: host).and_yield(faraday_connection) + expect(faraday_connection).to receive(:adapter).with(Faraday.default_adapter) + expect(faraday_connection).to receive(:headers).and_return headers + expect(faraday_connection).to receive(:request).with(:multipart) + expect(faraday_connection).to receive(:request).with(:url_encoded) + + expect(faraday_connection).to receive(:post).with(path, params).and_return(result) + + expect(subject.post_resource( + host: host, + path: path, + token: token, + params: params, + upload: {name => upload_list} )).to eq(result) + expect(headers['Authorization']).to eq( "Token token=#{token.inspect}" ) + end + + end + + context 'get_json_resource' do + + let( :content ){ SecureRandom.hex } + + it 'delegates an parses the response' do + expect_it.to receive(:get_resource) + .with(host: host, path: path, token: token, params: params) + .and_return(double(body: {content: content}.to_json, status: 200)) + + expect( subject.get_json_resource( + host: host, + path: path, + token: token, + params: params) ).to eq('content' => content) + end + end +end diff --git a/spec/services/parent_import_notifier_spec.rb b/spec/services/parent_import_notifier_spec.rb new file mode 100644 index 000000000..3ab505f88 --- /dev/null +++ b/spec/services/parent_import_notifier_spec.rb @@ -0,0 +1,90 @@ +RSpec.describe ParentImportNotifier do + let(:workbench_import) { create(:workbench_import) } + + describe ".notify_when_finished" do + it "calls #notify_parent on finished imports" do + workbench_import = build_stubbed(:workbench_import) + + finished_statuses = [:failed, :successful, :aborted, :canceled] + netex_imports = [] + + finished_statuses.each do |status| + netex_imports << build_stubbed( + :netex_import, + parent: workbench_import, + status: status + ) + end + + netex_imports.each do |netex_import| + expect(netex_import).to receive(:notify_parent) + end + + ParentImportNotifier.notify_when_finished(netex_imports) + end + + it "doesn't call #notify_parent if its `notified_parent_at` is set" do + netex_import = create( + :netex_import, + parent: workbench_import, + status: :failed, + notified_parent_at: DateTime.now + ) + + expect(netex_import).not_to receive(:notify_parent) + + ParentImportNotifier.notify_when_finished + end + end + + describe ".imports_pending_notification" do + it "includes imports with a parent and `notified_parent_at` unset" do + netex_import = create( + :netex_import, + parent: workbench_import, + status: :successful, + notified_parent_at: nil + ) + + expect( + ParentImportNotifier.imports_pending_notification + ).to eq([netex_import]) + end + + it "doesn't include imports without a parent" do + create(:import, parent: nil) + + expect( + ParentImportNotifier.imports_pending_notification + ).to be_empty + end + + it "doesn't include imports that aren't finished" do + [:new, :pending, :running].each do |status| + create( + :netex_import, + parent: workbench_import, + status: status, + notified_parent_at: nil + ) + end + + expect( + ParentImportNotifier.imports_pending_notification + ).to be_empty + end + + it "doesn't include imports that have already notified their parent" do + create( + :netex_import, + parent: workbench_import, + status: :successful, + notified_parent_at: DateTime.now + ) + + expect( + ParentImportNotifier.imports_pending_notification + ).to be_empty + end + end +end diff --git a/spec/services/retry_service_spec.rb b/spec/services/retry_service_spec.rb new file mode 100644 index 000000000..bb3416373 --- /dev/null +++ b/spec/services/retry_service_spec.rb @@ -0,0 +1,137 @@ +RSpec.describe RetryService do + subject { described_class.new delays: [2, 3], rescue_from: [NameError, ArgumentError] } + + context 'no retry necessary' do + before do + expect( subject ).not_to receive(:sleep) + end + + it 'returns an ok result' do + expect( subject.execute { 42 } ).to eq(Result.ok(42)) + end + it 'does not fail on nil' do + expect( subject.execute { nil } ).to eq(Result.ok(nil)) + end + + it 'fails wihout retries if raising un unregistered exception' do + expect{ subject.execute{ raise KeyError } }.to raise_error(KeyError) + end + + end + + context 'all retries fail' do + before do + expect( subject ).to receive(:sleep).with(2) + expect( subject ).to receive(:sleep).with(3) + end + it 'fails after raising a registered exception n times' do + result = subject.execute{ raise ArgumentError } + expect( result.status ).to eq(:error) + expect( result.value ).to be_kind_of(ArgumentError) + end + it 'fails with an explicit try again (automatically registered exception)' do + result = subject.execute{ raise RetryService::Retry } + expect( result.status ).to eq(:error) + expect( result.value ).to be_kind_of(RetryService::Retry) + end + end + + context "if at first you don't succeed" do + before do + @count = 0 + expect( subject ).to receive(:sleep).with(2) + end + + it 'succeeds the second time' do + expect( subject.execute{ succeed_later(ArgumentError){ 42 } } ).to eq(Result.ok(42)) + end + + it 'succeeds the second time with try again (automatically registered exception)' do + expect( subject.execute{ succeed_later(RetryService::Retry){ 42 } } ).to eq(Result.ok(42)) + end + end + + context 'last chance' do + before do + @count = 0 + expect( subject ).to receive(:sleep).with(2) + expect( subject ).to receive(:sleep).with(3) + end + it 'succeeds the third time with try again (automatically registered exception)' do + result = subject.execute{ succeed_later(RetryService::Retry, count: 2){ 42 } } + expect( result ).to eq( Result.ok(42) ) + end + end + + context 'failure callback once' do + subject do + described_class.new delays: [2, 3], rescue_from: [NameError, ArgumentError] do |reason, count| + @reason=reason + @callback_count=count + @failures += 1 + end + end + + before do + @failures = 0 + @count = 0 + expect( subject ).to receive(:sleep).with(2) + end + + it 'succeeds the second time and calls the failure_callback once' do + subject.execute{ succeed_later(RetryService::Retry){ 42 } } + expect( @failures ).to eq(1) + end + it '... and the failure is passed into the callback' do + subject.execute{ succeed_later(RetryService::Retry){ 42 } } + expect( @reason ).to be_a(RetryService::Retry) + expect( @callback_count ).to eq(1) + end + end + + context 'failure callback twice' do + subject do + described_class.new delays: [2, 3], rescue_from: [NameError, ArgumentError] do |_reason, _count| + @failures += 1 + end + end + + before do + @failures = 0 + @count = 0 + expect( subject ).to receive(:sleep).with(2) + expect( subject ).to receive(:sleep).with(3) + end + + it 'succeeds the third time and calls the failure_callback twice' do + subject.execute{ succeed_later(NameError, count: 2){ 42 } } + expect( @failures ).to eq(2) + end + end + + context 'failure callback in constructor' do + subject do + described_class.new(delays: [1, 2], &method(:add2failures)) + end + before do + @failures = [] + @count = 0 + expect( subject ).to receive(:sleep).with(1) + expect( subject ).to receive(:sleep).with(2) + end + it 'succeeds the second time and calls the failure_callback once' do + subject.execute{ succeed_later(RetryService::Retry, count: 2){ 42 } } + expect( @failures ).to eq([1,2]) + end + end + + def add2failures( e, c) + @failures << c + end + + def succeed_later error, count: 1, &blk + return blk.() unless @count < count + @count += 1 + raise error, 'error' + end +end diff --git a/spec/services/zip_service/zip_entry_data_spec.rb b/spec/services/zip_service/zip_entry_data_spec.rb new file mode 100644 index 000000000..2a7226eb4 --- /dev/null +++ b/spec/services/zip_service/zip_entry_data_spec.rb @@ -0,0 +1,32 @@ +RSpec.describe ZipService do + + subject{ described_class.new(read_fixture('multiple_references_import.zip')) } + + it 'can group all entries' do + expect( subject.entry_groups.keys ).to eq(%w{ref1 ref2}) + end + + context 'creates correct zip data for each subdir' do + it 'e.g. reference1' do + reference1_stream = subject.entry_group_streams['ref1'] + control_stream = Zip::InputStream.open( reference1_stream ) + control_entries = described_class.entries(control_stream) + expect( control_entries.map{ |e| [e.name, e.get_input_stream.read]}.force ).to eq([ + ["multiref/ref1/", ""], + ["multiref/ref1/datum-1", "multi-ref1-datum1\n"], + ["multiref/ref1/datum-2", "multi-ref1-datum2\n"] + ]) + end + it 'e.g. reference2' do + reference2_stream = subject.entry_group_streams['ref2'] + control_stream = Zip::InputStream.open( reference2_stream ) + control_entries = described_class.entries(control_stream) + expect( control_entries.map{ |e| [e.name, e.get_input_stream.read]}.force ).to eq([ + ["multiref/ref2/", ""], + ["multiref/ref2/datum-1", "multi-ref2-datum1\n"], + ["multiref/ref2/datum-2", "multi-ref2-datum2\n"] + ]) + end + end + +end diff --git a/spec/services/zip_service/zip_entry_dirs_spec.rb b/spec/services/zip_service/zip_entry_dirs_spec.rb new file mode 100644 index 000000000..8ca1b0f1a --- /dev/null +++ b/spec/services/zip_service/zip_entry_dirs_spec.rb @@ -0,0 +1,33 @@ +RSpec.describe ZipService do + + let( :zip_service ){ described_class } + + let( :zip_data ){ File.read zip_file } + + shared_examples_for 'a correct zip entry reader' do + it 'gets all entries of the zip file' do + expect( zip_service.new(zip_data).entry_groups.keys ).to eq(expected) + end + end + + context 'single entry' do + let( :zip_file ){ fixtures_path 'multiple_references_import.zip' } + let( :expected ){ %w{ref1 ref2} } + + it_behaves_like 'a correct zip entry reader' + end + + context 'more entries' do + let( :zip_file ){ fixtures_path 'single_reference_import.zip' } + let( :expected ){ %w{ref} } + + it_behaves_like 'a correct zip entry reader' + end + + context 'illegal file' do + let( :zip_file ){ fixtures_path 'nozip.zip' } + let( :expected ){ [] } + + it_behaves_like 'a correct zip entry reader' + end +end diff --git a/spec/services/zip_service/zip_output_streams_spec.rb b/spec/services/zip_service/zip_output_streams_spec.rb new file mode 100644 index 000000000..742f9b996 --- /dev/null +++ b/spec/services/zip_service/zip_output_streams_spec.rb @@ -0,0 +1,8 @@ +RSpec.describe ZipService do + + subject{ described_class.new(read_fixture('multiple_references_import.zip')) } + + it "exposes its size" do + expect( subject.entry_group_streams.size ).to eq(2) + end +end diff --git a/spec/support/api_key.rb b/spec/support/api_key.rb index 9353fac15..cc08cd7f1 100644 --- a/spec/support/api_key.rb +++ b/spec/support/api_key.rb @@ -1,20 +1,28 @@ module ApiKeyHelper + def authorization_token_header(key) + {'Authorization' => "Token token=#{key}"} + end + def get_api_key - Api::V1::ApiKey.first_or_create( :referential_id => referential.id, :name => "test") + Api::V1::ApiKey.first_or_create(referential: referential, organisation: organisation) end + def config_formatted_request_with_authorization( format) request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Token.encode_credentials( get_api_key.token) request.accept = format end + def config_formatted_request_with_dummy_authorization( format) request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Token.encode_credentials( "dummy") request.accept = format end + def config_formatted_request_without_authorization( format) request.env['HTTP_AUTHORIZATION'] = nil request.accept = format end + def json_xml_format? request.accept == "application/json" || request.accept == "application/xml" end diff --git a/spec/support/checksum_support.rb b/spec/support/checksum_support.rb new file mode 100644 index 000000000..14ea3c55e --- /dev/null +++ b/spec/support/checksum_support.rb @@ -0,0 +1,53 @@ +shared_examples 'checksum support' do |factory_name| + let(:instance) { create(factory_name) } + + describe '#current_checksum_source' do + let(:attributes) { ['code_value', 'label_value'] } + let(:seperator) { ChecksumSupport::SEPARATOR } + let(:nil_value) { ChecksumSupport::VALUE_FOR_NIL_ATTRIBUTE } + + before do + allow_any_instance_of(instance.class).to receive(:checksum_attributes).and_return(attributes) + end + + it 'should separate attribute by seperator' do + expect(instance.current_checksum_source).to eq("code_value#{seperator}label_value") + end + + context 'nil value' do + let(:attributes) { ['code_value', nil] } + + it 'should replace nil attributes by default value' do + source = "code_value#{seperator}#{nil_value}" + expect(instance.current_checksum_source).to eq(source) + end + end + + context 'empty array' do + let(:attributes) { ['code_value', []] } + + it 'should convert to nil' do + source = "code_value#{seperator}#{nil_value}" + expect(instance.current_checksum_source).to eq(source) + end + end + end + + it 'should save checksum on create' do + expect(instance.checksum).to_not be_nil + end + + it 'should save checksum_source' do + expect(instance.checksum_source).to_not be_nil + end + + it 'should trigger set_current_checksum_source on save' do + expect(instance).to receive(:set_current_checksum_source) + instance.save + end + + it 'should trigger update_checksum on save' do + expect(instance).to receive(:update_checksum) + instance.save + end +end diff --git a/spec/support/fixtures_helper.rb b/spec/support/fixtures_helper.rb new file mode 100644 index 000000000..20963261b --- /dev/null +++ b/spec/support/fixtures_helper.rb @@ -0,0 +1,18 @@ +module Support + module FixturesHelper + def fixtures_path *segments + Rails.root.join( fixture_path, *segments ) + end + + def open_fixture *segments + File.open(fixtures_path(*segments)) + end + def read_fixture *segments + File.read(fixtures_path(*segments)) + end + end +end + +RSpec.configure do |c| + c.include Support::FixturesHelper +end diff --git a/spec/support/json_helper.rb b/spec/support/json_helper.rb new file mode 100644 index 000000000..a383981a0 --- /dev/null +++ b/spec/support/json_helper.rb @@ -0,0 +1,11 @@ +module Support + module JsonHelper + def json_response_body + JSON.parse(response.body) + end + end +end + +RSpec.configure do | config | + config.include Support::JsonHelper, type: :request +end diff --git a/spec/support/referential.rb b/spec/support/referential.rb index 57b510f69..c431856b8 100644 --- a/spec/support/referential.rb +++ b/spec/support/referential.rb @@ -12,6 +12,7 @@ module ReferentialHelper base.class_eval do extend ClassMethods alias_method :referential, :first_referential + alias_method :organisation, :first_organisation end end diff --git a/spec/support/shared_context.rb b/spec/support/shared_context.rb new file mode 100644 index 000000000..e9b0025a2 --- /dev/null +++ b/spec/support/shared_context.rb @@ -0,0 +1,15 @@ +shared_context 'iboo authenticated api user' do + let(:api_key) { create(:api_key, organisation: organisation) } + + before do + request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(api_key.organisation.code, api_key.token) + end +end + +shared_context 'iboo wrong authorisation api user' do + let(:api_key) { create(:api_key, organisation: organisation) } + + before do + request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials('fake code', api_key.token) + end +end diff --git a/spec/support/webmock/helpers.rb b/spec/support/webmock/helpers.rb new file mode 100644 index 000000000..fc6c77850 --- /dev/null +++ b/spec/support/webmock/helpers.rb @@ -0,0 +1,18 @@ +module Support + module Webmock + module Helpers + def stub_headers(*args) + {headers: make_headers(*args)} + end + + def make_headers(headers={}, authorization_token:) + headers.merge('Authorization' => "Token token=#{authorization_token.inspect}") + end + end + end +end + +RSpec.configure do | conf | + conf.include Support::Webmock::Helpers, type: :model + conf.include Support::Webmock::Helpers, type: :worker +end diff --git a/spec/tasks/reflex_rake_spec.rb b/spec/tasks/reflex_rake_spec.rb index 04c5886aa..6ece223d2 100644 --- a/spec/tasks/reflex_rake_spec.rb +++ b/spec/tasks/reflex_rake_spec.rb @@ -5,7 +5,7 @@ describe 'reflex:sync' do before(:each) do ['getOP', 'getOR'].each do |method| stub_request(:get, "#{Rails.application.config.reflex_api_url}/?format=xml&idRefa=0&method=#{method}"). - to_return(body: File.open("#{fixture_path}/reflex.zip"), status: 200) + to_return(body: open_fixture('reflex.zip'), status: 200) end stop_area_ref = create(:stop_area_referential, name: 'Reflex') @@ -43,7 +43,7 @@ describe 'reflex:sync' do before(:each) do ['getOP', 'getOR'].each do |method| stub_request(:get, "#{Rails.application.config.reflex_api_url}/?format=xml&idRefa=0&method=#{method}"). - to_return(body: File.open("#{fixture_path}/reflex_updated.zip"), status: 200) + to_return(body: open_fixture('reflex_updated.zip'), status: 200) end Stif::ReflexSynchronization.synchronize end diff --git a/spec/workers/stop_area_referential_sync_worker_spec.rb b/spec/workers/stop_area_referential_sync_worker_spec.rb index 48b64e55e..50c7cf45f 100644 --- a/spec/workers/stop_area_referential_sync_worker_spec.rb +++ b/spec/workers/stop_area_referential_sync_worker_spec.rb @@ -1,4 +1,3 @@ -require 'rails_helper' RSpec.describe StopAreaReferentialSyncWorker, type: :worker do let!(:stop_area_referential_sync) { create :stop_area_referential_sync } diff --git a/spec/workers/workbench_import_worker_spec.rb b/spec/workers/workbench_import_worker_spec.rb new file mode 100644 index 000000000..b719cbb98 --- /dev/null +++ b/spec/workers/workbench_import_worker_spec.rb @@ -0,0 +1,119 @@ +RSpec.describe WorkbenchImportWorker, type: [:worker, :request] do + + let( :worker ) { described_class.new } + let( :import ){ build_stubbed :import, token_download: download_token, file: zip_file } + + let( :workbench ){ import.workbench } + let( :referential ){ import.referential } + let( :api_key ){ build_stubbed :api_key, referential: referential, token: "#{referential.id}-#{SecureRandom.hex}" } + let( :params ) do + { netex_import: + { referential_id: referential.id, workbench_id: workbench.id } + } + end + + # http://www.example.com/workbenches/:workbench_id/imports/:id/download + let( :host ){ Rails.configuration.rails_host } + let( :path ){ download_workbench_import_path(workbench, import) } + + let( :downloaded_zip ){ double("downloaded zip") } + let( :download_zip_response ){ OpenStruct.new( body: downloaded_zip ) } + let( :download_token ){ SecureRandom.urlsafe_base64 } + + + let( :upload_path ) { api_v1_netex_imports_path(format: :json) } + + let( :entry_group_streams ) do + entry_count.times.map{ |i| double( "entry group stream #{i}" ) } + end + let( :entry_groups ) do + entry_count.times.map do | i | + {"entry_group_name#{i}" => entry_group_streams[i] } + end + end + + let( :zip_service ){ double("zip service") } + let( :zip_file ){ open_fixture('multiple_references_import.zip') } + + let( :post_response_ok ){ double(status: 201, body: "{}") } + + before do + # Silence Logger + allow_any_instance_of(Logger).to receive(:info) + allow_any_instance_of(Logger).to receive(:warn) + + # That should be `build_stubbed's` job, no? + allow(Import).to receive(:find).with(import.id).and_return(import) + + allow(Api::V1::ApiKey).to receive(:from).and_return(api_key) + allow(ZipService).to receive(:new).with(downloaded_zip).and_return zip_service + expect(zip_service).to receive(:entry_group_streams).and_return(entry_groups) + expect( import ).to receive(:update_attributes).with(status: 'running') + end + + + context 'multireferential zipfile, no errors' do + let( :entry_count ){ 2 } + + it 'downloads a zip file, cuts it, and uploads all pieces' do + + expect(HTTPService).to receive(:get_resource) + .with(host: host, path: path, params: {token: download_token}) + .and_return( download_zip_response ) + + entry_groups.each do | entry_group_name, entry_group_stream | + mock_post entry_group_name, entry_group_stream, post_response_ok + end + + expect( import ).to receive(:update_attributes).with(total_steps: 2) + expect( import ).to receive(:update_attributes).with(current_step: 1) + expect( import ).to receive(:update_attributes).with(current_step: 2) + + worker.perform import.id + + end + end + + context 'multireferential zipfile with error' do + let( :entry_count ){ 3 } + let( :post_response_failure ){ double(status: 406, body: {error: 'What was you thinking'}) } + + it 'downloads a zip file, cuts it, and uploads some pieces' do + expect(HTTPService).to receive(:get_resource) + .with(host: host, path: path, params: {token: download_token}) + .and_return( download_zip_response ) + + # First entry_group succeeds + entry_groups[0..0].each do | entry_group_name, entry_group_stream | + mock_post entry_group_name, entry_group_stream, post_response_ok + end + + # Second entry_group fails (M I S E R A B L Y) + entry_groups[1..1].each do | entry_group_name, entry_group_stream | + mock_post entry_group_name, entry_group_stream, post_response_failure + WorkbenchImportWorker::RETRY_DELAYS.each do | delay | + mock_post entry_group_name, entry_group_stream, post_response_failure + expect_any_instance_of(RetryService).to receive(:sleep).with(delay) + end + end + + expect( import ).to receive(:update_attributes).with(total_steps: 3) + expect( import ).to receive(:update_attributes).with(current_step: 1) + expect( import ).to receive(:update_attributes).with(current_step: 2) + expect( import ).to receive(:update_attributes).with(current_step: 3, status: 'failed') + + worker.perform import.id + + end + end + + def mock_post entry_group_name, entry_group_stream, response + expect( HTTPService ).to receive(:post_resource) + .with(host: host, + path: upload_path, + token: api_key.token, + params: params, + upload: {file: [entry_group_stream, 'application/zip', entry_group_name]}) + .and_return(response) + end +end |
