diff options
| author | Xinhui | 2017-06-28 17:20:23 +0200 |
|---|---|---|
| committer | Xinhui | 2017-06-28 17:20:23 +0200 |
| commit | bed9795be0bb85d7c8c311fe4ee0fb12e46832b4 (patch) | |
| tree | 18459aa9dcb81821a88f2acdf9ba72cee9be5616 | |
| parent | 641b1458236d2718a76ffaf0c04a5998623276bf (diff) | |
| parent | b260c832c8e129dbeacfe065d01bd0732dd80701 (diff) | |
| download | chouette-core-bed9795be0bb85d7c8c311fe4ee0fb12e46832b4.tar.bz2 | |
Merge branch 'master' into staging
109 files changed, 2023 insertions, 657 deletions
@@ -161,6 +161,7 @@ group :test do gem 'cucumber-rails', require: false gem 'simplecov', :require => false gem 'simplecov-rcov', :require => false + gem 'htmlbeautifier' end group :test, :development, :dev do diff --git a/Gemfile.lock b/Gemfile.lock index e87e6bb3b..eea6f4ba5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -261,6 +261,7 @@ GEM hashdiff (0.3.2) highline (1.7.8) hike (1.2.3) + htmlbeautifier (1.3.1) httparty (0.14.0) multi_xml (>= 0.5.2) i18n (0.8.1) @@ -604,6 +605,7 @@ DEPENDENCIES georuby-ext (= 0.0.5) google-analytics-rails has_array_of! + htmlbeautifier i18n-tasks inherited_resources jbuilder (~> 2.0) 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 1a6c67b6b..a8a92c522 100644 --- a/app/assets/javascripts/es6_browserified/time_tables/components/PeriodForm.js +++ b/app/assets/javascripts/es6_browserified/time_tables/components/PeriodForm.js @@ -61,30 +61,30 @@ const PeriodForm = ({modal, timetable, metas, onOpenAddPeriodForm, onClosePeriod <div className="nested-fields"> <div className="wrapper"> <div> - <div className={'form-group date' + (modal.modalProps.error ? ' has-error' : '')}> + <div className={'form-group date smart_date' + (modal.modalProps.error ? ' has-error' : '')}> <div className="form-inline"> - <select value={formatNumber(modal.modalProps.begin.day)} onChange={(e) => onUpdatePeriodForm(e.currentTarget.value, 'begin', 'day')} id="q_validity_period_begin_gteq_3i" className="date required form-control"> + <select value={formatNumber(modal.modalProps.begin.day)} onChange={(e) => onUpdatePeriodForm(e, 'begin', 'day')} id="q_validity_period_begin_gteq_3i" className="date required form-control"> {makeDaysOptions(modal.modalProps.begin.day)} </select> - <select value={formatNumber(modal.modalProps.begin.month)} onChange={(e) => onUpdatePeriodForm(e.currentTarget.value, 'begin', 'month')} id="q_validity_period_begin_gteq_2i" className="date required form-control"> + <select value={formatNumber(modal.modalProps.begin.month)} onChange={(e) => onUpdatePeriodForm(e, 'begin', 'month')} id="q_validity_period_begin_gteq_2i" className="date required form-control"> {makeMonthsOptions(modal.modalProps.begin.month)} </select> - <select value={modal.modalProps.begin.year} onChange={(e) => onUpdatePeriodForm(e.currentTarget.value, 'begin', 'year')} id="q_validity_period_begin_gteq_1i" className="date required form-control"> + <select value={modal.modalProps.begin.year} onChange={(e) => onUpdatePeriodForm(e, 'begin', 'year')} id="q_validity_period_begin_gteq_1i" className="date required form-control"> {makeYearsOptions(modal.modalProps.begin.year)} </select> </div> </div> </div> <div> - <div className={'form-group date' + (modal.modalProps.error ? ' has-error' : '')}> + <div className={'form-group date smart_date' + (modal.modalProps.error ? ' has-error' : '')}> <div className="form-inline"> - <select value={formatNumber(modal.modalProps.end.day)} onChange={(e) => onUpdatePeriodForm(e.currentTarget.value, 'end', 'day')} id="q_validity_period_end_gteq_3i" className="date required form-control"> + <select value={formatNumber(modal.modalProps.end.day)} onChange={(e) => onUpdatePeriodForm(e, 'end', 'day')} id="q_validity_period_end_gteq_3i" className="date required form-control"> {makeDaysOptions(modal.modalProps.end.day)} </select> - <select value={formatNumber(modal.modalProps.end.month)} onChange={(e) => onUpdatePeriodForm(e.currentTarget.value, 'end', 'month')} id="q_validity_period_end_gteq_2i" className="date required form-control"> + <select value={formatNumber(modal.modalProps.end.month)} onChange={(e) => onUpdatePeriodForm(e, 'end', 'month')} id="q_validity_period_end_gteq_2i" className="date required form-control"> {makeMonthsOptions(modal.modalProps.end.month)} </select> - <select value={modal.modalProps.end.year} onChange={(e) => onUpdatePeriodForm(e.currentTarget.value, 'end', 'year')} id="q_validity_period_end_gteq_1i" className="date required form-control"> + <select value={modal.modalProps.end.year} onChange={(e) => onUpdatePeriodForm(e, 'end', 'year')} id="q_validity_period_end_gteq_1i" className="date required form-control"> {makeYearsOptions(modal.modalProps.end.year)} </select> </div> 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 b6004c7f1..a7edbc328 100644 --- a/app/assets/javascripts/es6_browserified/time_tables/containers/PeriodForm.js +++ b/app/assets/javascripts/es6_browserified/time_tables/containers/PeriodForm.js @@ -18,8 +18,10 @@ const mapDispatchToProps = (dispatch) => { onClosePeriodForm: () => { dispatch(actions.closePeriodForm()) }, - onUpdatePeriodForm: (val, group, selectType) => { - dispatch(actions.updatePeriodForm(val, group, selectType)) + onUpdatePeriodForm: (e, group, selectType) => { + dispatch(actions.updatePeriodForm(e.currentTarget.value, group, selectType)) + let selector = '#q_validity_period_' + group + '_gteq_3i' + dispatch(actions.updatePeriodForm($(selector).val(), group, 'day')) }, onValidatePeriodForm: (modalProps, timeTablePeriods, metas) => { dispatch(actions.validatePeriodForm(modalProps, timeTablePeriods, metas)) 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 0af1bb53d..e90d2d307 100644 --- a/app/assets/javascripts/es6_browserified/vehicle_journeys/actions/index.js +++ b/app/assets/javascripts/es6_browserified/vehicle_journeys/actions/index.js @@ -134,9 +134,10 @@ const actions = { type: 'SHIFT_VEHICLEJOURNEY', data }), - duplicateVehicleJourney : (data) => ({ + duplicateVehicleJourney : (data, departureDelta) => ({ type: 'DUPLICATE_VEHICLEJOURNEY', - data + data, + departureDelta }), deleteVehicleJourneys : () => ({ type: 'DELETE_VEHICLEJOURNEYS' @@ -458,20 +459,20 @@ const actions = { schedule.arrival_time.hour = parseInt(schedule.arrival_time.hour) + hours } - if(schedule.departure_time.hour > 23){ + if(parseInt(schedule.departure_time.hour) > 23){ schedule.departure_time.hour = '23' schedule.departure_time.minute = '59' } - if(schedule.arrival_time.hour > 23){ + if(parseInt(schedule.arrival_time.hour) > 23){ schedule.arrival_time.hour = '23' schedule.arrival_time.minute = '59' } - if(schedule.departure_time.hour < 0){ + if(parseInt(schedule.departure_time.hour) < 0){ schedule.departure_time.hour = '00' schedule.departure_time.minute = '00' } - if(schedule.arrival_time.hour > 23){ + if(parseInt(schedule.arrival_time.hour) < 0){ schedule.arrival_time.hour = '00' schedule.arrival_time.minute = '00' } 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 0cf102693..aa1a13b11 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 @@ -18,8 +18,8 @@ class DuplicateVehicleJourney extends Component { } } 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) + val - this.props.onDuplicateVehicleJourney(this.refs) + this.refs.additional_time.value = parseInt(this.refs.additional_time.value) + this.props.onDuplicateVehicleJourney(this.refs, val) this.props.onModalClose() $('#DuplicateVehicleJourneyModal').modal('hide') } @@ -51,10 +51,9 @@ class DuplicateVehicleJourney extends Component { <div className='modal-dialog'> <div className='modal-content'> <div className='modal-header'> - <h4 className='modal-title'>Dupliquer une course</h4> - {(this.props.modal.type == 'duplicate') && ( - <em>Dupliquer les horaires de la course {actions.humanOID(actions.getSelected(this.props.vehicleJourneys)[0].objectid)}</em> - )} + <h4 className='modal-title'> + Dupliquer { actions.getSelected(this.props.vehicleJourneys).length > 1 ? 'plusieurs courses' : 'une course' } + </h4> </div> {(this.props.modal.type == 'duplicate') && ( 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 932c56532..9919ee9dd 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 @@ -70,7 +70,7 @@ class EditVehicleJourney extends Component { <input type='text' className='form-control' - value={(this.props.modal.modalProps.vehicleJourney.journey_pattern.objectid) + ' - ' + (this.props.modal.modalProps.vehicleJourney.journey_pattern.name)} + value={actions.humanOID(this.props.modal.modalProps.vehicleJourney.journey_pattern.objectid) + ' - ' + (this.props.modal.modalProps.vehicleJourney.journey_pattern.name)} disabled={true} /> </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 ee7d01cf5..2164344c2 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 @@ -40,7 +40,7 @@ class ShiftVehicleJourney extends Component { <div className='modal-header'> <h4 className='modal-title'>Mettre à jour une course</h4> {(this.props.modal.type == 'shift') && ( - <em>Mettre à jour les horaires de la course {actions.getSelected(this.props.vehicleJourneys)[0].objectid}</em> + <em>Mettre à jour les horaires de la course {actions.humanOID(actions.getSelected(this.props.vehicleJourneys)[0].objectid)}</em> )} </div> @@ -57,6 +57,7 @@ class ShiftVehicleJourney extends Component { min='-59' max='59' className='form-control' + defaultValue='0' onKeyDown={(e) => actions.resetValidation(e.currentTarget)} required /> 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 6cf6f4039..224b52a19 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) =>{ - dispatch(actions.duplicateVehicleJourney(data)) + onDuplicateVehicleJourney: (data, departureDelta) =>{ + dispatch(actions.duplicateVehicleJourney(data, departureDelta)) } } } diff --git a/app/assets/javascripts/es6_browserified/vehicle_journeys/reducers/filters.js b/app/assets/javascripts/es6_browserified/vehicle_journeys/reducers/filters.js index 09588f824..b4a70ec08 100644 --- a/app/assets/javascripts/es6_browserified/vehicle_journeys/reducers/filters.js +++ b/app/assets/javascripts/es6_browserified/vehicle_journeys/reducers/filters.js @@ -15,7 +15,7 @@ const filters = (state = {}, action) => { minute: '59' } } - newQuery = _.assign({}, state.query, {interval: interval, journeyPattern: {}, vehicleJourney: {}, timetable: {}, withoutSchedule: true, withoutTimeTable: false }) + newQuery = _.assign({}, state.query, {interval: interval, journeyPattern: {}, vehicleJourney: {}, timetable: {}, withoutSchedule: true, withoutTimeTable: true }) return _.assign({}, state, {query: newQuery, queryString: ''}) case 'TOGGLE_WITHOUT_SCHEDULE': newQuery = _.assign({}, state.query, {withoutSchedule: !state.query.withoutSchedule}) 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 c7e8d58e7..d463d4b8f 100644 --- a/app/assets/javascripts/es6_browserified/vehicle_journeys/reducers/vehicleJourneys.js +++ b/app/assets/javascripts/es6_browserified/vehicle_journeys/reducers/vehicleJourneys.js @@ -182,11 +182,12 @@ const vehicleJourneys = (state = [], action) => { let dupes = [] let selectedIndex let val = action.data.additional_time.value + 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 = val * (i + 1) + action.data.additional_time.value = (parseInt(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/smart_date.coffee b/app/assets/javascripts/smart_date.coffee new file mode 100644 index 000000000..afc0c7ddf --- /dev/null +++ b/app/assets/javascripts/smart_date.coffee @@ -0,0 +1,26 @@ +window.legalDaysPerMonth = + false: [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31], + true: [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + + +window.legal +window.correctDay = (dateValues) -> + [day, month, year] = dateValues + return day unless day > 0 && month > 0 && year > 0 + + legallyMaximumDay = legalDaysPerMonth[isLeapYear(year)][month - 1] + Math.min day, legallyMaximumDay + +window.isLeapYear = (year) -> + (year % 4 == 0) && ((year % 400 == 0) || (year % 100 != 0)) + +window.smartCorrectDate = -> + allSelectors = $(@).parent().children('select') + allVals = allSelectors.map (index, sel) -> + parseInt($(sel).val()) + correctedDay = correctDay allVals + daySelector = allSelectors.first() + $(daySelector).val(correctedDay) + +$ -> + $(document).on 'change', '.smart_date select', smartCorrectDate diff --git a/app/assets/stylesheets/components/_dropdown.sass b/app/assets/stylesheets/components/_dropdown.sass index 99dc6292e..8a8d69063 100644 --- a/app/assets/stylesheets/components/_dropdown.sass +++ b/app/assets/stylesheets/components/_dropdown.sass @@ -3,6 +3,8 @@ //-------------// .dropdown-menu + z-index: 2001 + > li > button display: block width: 100% diff --git a/app/assets/stylesheets/components/_tables.sass b/app/assets/stylesheets/components/_tables.sass index e3a33e131..20679a3ba 100644 --- a/app/assets/stylesheets/components/_tables.sass +++ b/app/assets/stylesheets/components/_tables.sass @@ -296,6 +296,9 @@ border-right: 1px solid rgba($grey, 0.5) .th + > div + min-height: 19px + > *:first-child padding-right: 30px diff --git a/app/controllers/calendars_controller.rb b/app/controllers/calendars_controller.rb index 86d567882..23af4129b 100644 --- a/app/controllers/calendars_controller.rb +++ b/app/controllers/calendars_controller.rb @@ -5,6 +5,12 @@ class CalendarsController < BreadcrumbController respond_to :html respond_to :js, only: :index + def show + show! do + @calendar = @calendar.decorate + end + end + private def calendar_params permitted_params = [:id, :name, :short_name, periods_attributes: [:id, :begin, :end, :_destroy], date_values_attributes: [:id, :value, :_destroy]] @@ -49,7 +55,7 @@ class CalendarsController < BreadcrumbController date << params[:q][key].to_i params[:q].delete(key) end - params[:q]['contains_date'] = Date.new(*date) + params[:q]['contains_date'] = Date.new(*date) rescue nil end end diff --git a/app/controllers/devise/cas_sessions_controller.rb b/app/controllers/devise/cas_sessions_controller.rb index 0a9d9ecb2..d000d0309 100644 --- a/app/controllers/devise/cas_sessions_controller.rb +++ b/app/controllers/devise/cas_sessions_controller.rb @@ -20,14 +20,14 @@ class Devise::CasSessionsController < Devise::SessionsController if LoginPolicy.new(current_user).boiv? redirect_to after_sign_in_path_for(current_user) else - redirect_to root_path, flash: {alert: t('devise.sessions.new.unauthorized')} + destroy message: t('devise.sessions.new.unauthorized') end end def unregistered end - def destroy + def destroy message: nil # if :cas_create_user is false a CAS session might be open but not signed_in # in such case we destroy the session here if signed_in?(resource_name) @@ -36,7 +36,11 @@ class Devise::CasSessionsController < Devise::SessionsController reset_session end - redirect_to(cas_logout_url) + if message + redirect_to(cas_logout_url, flash: {alert: message}) + else + redirect_to(cas_logout_url) + end end def single_sign_out diff --git a/app/controllers/lines_controller.rb b/app/controllers/lines_controller.rb index 7eedaeb05..1e2056aad 100644 --- a/app/controllers/lines_controller.rb +++ b/app/controllers/lines_controller.rb @@ -25,6 +25,11 @@ class LinesController < BreadcrumbController def show @group_of_lines = resource.group_of_lines show! do + @line = @line.decorate(context: { + line_referential: @line_referential, + current_organisation: current_organisation + }) + build_breadcrumb :show end end diff --git a/app/controllers/referentials_controller.rb b/app/controllers/referentials_controller.rb index aa5b359da..1239d512f 100644 --- a/app/controllers/referentials_controller.rb +++ b/app/controllers/referentials_controller.rb @@ -24,19 +24,21 @@ class ReferentialsController < BreadcrumbController end def show - resource.switch - show! do |format| - format.json { - render :json => { :lines_count => resource.lines.count, - :networks_count => resource.networks.count, - :vehicle_journeys_count => resource.vehicle_journeys.count + resource.vehicle_journey_frequencies.count, - :time_tables_count => resource.time_tables.count, - :referential_id => resource.id} - } - format.html { build_breadcrumb :show} - end - - @reflines = lines_collection.paginate(page: params[:page], per_page: 10) + resource.switch + show! do |format| + @referential = @referential.decorate + + format.json { + render :json => { :lines_count => resource.lines.count, + :networks_count => resource.networks.count, + :vehicle_journeys_count => resource.vehicle_journeys.count + resource.vehicle_journey_frequencies.count, + :time_tables_count => resource.time_tables.count, + :referential_id => resource.id} + } + format.html { build_breadcrumb :show} + end + + @reflines = lines_collection.paginate(page: params[:page], per_page: 10) end def edit diff --git a/app/controllers/routes_controller.rb b/app/controllers/routes_controller.rb index 73febc4b9..786bd57cc 100644 --- a/app/controllers/routes_controller.rb +++ b/app/controllers/routes_controller.rb @@ -42,6 +42,12 @@ class RoutesController < ChouetteController end show! do + @route = @route.decorate(context: { + referential: @referential, + line: @line, + route_sp: @route_sp + }) + build_breadcrumb :show end end diff --git a/app/controllers/routing_constraint_zones_controller.rb b/app/controllers/routing_constraint_zones_controller.rb index 7707427b0..9d2fd712c 100644 --- a/app/controllers/routing_constraint_zones_controller.rb +++ b/app/controllers/routing_constraint_zones_controller.rb @@ -16,6 +16,10 @@ class RoutingConstraintZonesController < ChouetteController def show @routing_constraint_zone = collection.find(params[:id]) + @routing_constraint_zone = @routing_constraint_zone.decorate(context: { + referential: @referential, + line: @line + }) end protected diff --git a/app/controllers/time_tables_controller.rb b/app/controllers/time_tables_controller.rb index 5c4552afb..3704f2885 100644 --- a/app/controllers/time_tables_controller.rb +++ b/app/controllers/time_tables_controller.rb @@ -14,6 +14,10 @@ class TimeTablesController < ChouetteController @year = params[:year] ? params[:year].to_i : Date.today.cwyear @time_table_combination = TimeTableCombination.new show! do + @time_table = @time_table.decorate(context: { + referential: @referential + }) + build_breadcrumb :show end end diff --git a/app/controllers/workbenches_controller.rb b/app/controllers/workbenches_controller.rb index ccd55965b..d03841356 100644 --- a/app/controllers/workbenches_controller.rb +++ b/app/controllers/workbenches_controller.rb @@ -11,11 +11,18 @@ class WorkbenchesController < BreadcrumbController scope = ransack_status(scope) # Ignore archived_at_not_null/archived_at_null managed by ransack_status scope - q_for_result = - scope.ransack(params[:q].merge(archived_at_not_null: nil, archived_at_null: nil)) - @wbench_refs = sort_result(q_for_result.result).paginate(page: params[:page], per_page: 30) + # We clone params[:q] so we can delete fake ransack filter arguments before calling search method, + # which will allow us to preserve params[:q] for sorting + ransack_params = params[:q].merge(archived_at_not_null: nil, archived_at_null: nil).clone + ransack_params.delete('associated_lines_id_eq') + + @q = scope.ransack(ransack_params) + @wbench_refs = sort_result(@q.result).paginate(page: params[:page], per_page: 30) + @wbench_refs = ModelDecorator.decorate( + @wbench_refs, + with: ReferentialDecorator + ) - @q = scope.ransack(params[:q]) show! do build_breadcrumb :show end @@ -33,7 +40,7 @@ class WorkbenchesController < BreadcrumbController private def resource - @workbench = Workbench.find params[:id] + @workbench = current_organisation.workbenches.find params[:id] end def sort_result collection @@ -59,7 +66,6 @@ class WorkbenchesController < BreadcrumbController def ransack_associated_lines scope if params[:q] && params[:q]['associated_lines_id_eq'] scope = scope.include_metadatas_lines([params[:q]['associated_lines_id_eq']]) - params[:q].delete('associated_lines_id_eq') end scope end diff --git a/app/decorators/calendar_decorator.rb b/app/decorators/calendar_decorator.rb new file mode 100644 index 000000000..37e2cfe80 --- /dev/null +++ b/app/decorators/calendar_decorator.rb @@ -0,0 +1,18 @@ +class CalendarDecorator < Draper::Decorator + delegate_all + + def action_links + links = [] + + if h.policy(object).destroy? + links << Link.new( + content: h.destroy_link_content, + href: h.calendar_path(object), + method: :delete, + data: { confirm: h.t('calendars.actions.destroy_confirm') } + ) + end + + links + end +end diff --git a/app/decorators/company_decorator.rb b/app/decorators/company_decorator.rb index 3a0cc16ce..51c1f3c61 100644 --- a/app/decorators/company_decorator.rb +++ b/app/decorators/company_decorator.rb @@ -1,4 +1,6 @@ class CompanyDecorator < Draper::Decorator + decorates Chouette::Company + delegate_all def self.collection_decorator_class @@ -9,4 +11,43 @@ class CompanyDecorator < Draper::Decorator object.lines.count end + # Requires: + # context: { + # line_referential: + # } + def action_links + links = [] + + if h.policy(Chouette::Company).create? + links << Link.new( + content: h.t('companies.actions.new'), + href: h.new_line_referential_company_path(context[:line_referential]) + ) + end + + if h.policy(object).update? + links << Link.new( + content: h.t('companies.actions.edit'), + href: h.edit_line_referential_company_path( + context[:line_referential], + object + ) + ) + end + + if h.policy(object).destroy? + links << Link.new( + content: t('companies.actions.destroy'), + href: h.line_referential_company_path( + context[:line_referential], + object + ), + method: :delete, + data: { confirm: h.t('companies.actions.destroy_confirm') } + ) + end + + links + end + end diff --git a/app/decorators/line_decorator.rb b/app/decorators/line_decorator.rb new file mode 100644 index 000000000..f351103b2 --- /dev/null +++ b/app/decorators/line_decorator.rb @@ -0,0 +1,45 @@ +class LineDecorator < Draper::Decorator + decorates Chouette::Line + + delegate_all + + # Requires: + # context: { + # line_referential: , + # current_organisation: + # } + def action_links + links = [] + + links << Link.new( + content: h.t('lines.actions.show_network'), + href: [context[:line_referential], object.network] + ) + + links << Link.new( + content: h.t('lines.actions.show_company'), + href: [context[:line_referential], object.company] + ) + + if h.policy(Chouette::Line).create? && + context[:line_referential].organisations.include?( + context[:current_organisation] + ) + links << Link.new( + content: h.t('lines.actions.new'), + href: h.new_line_referential_line_path(context[:line_referential]) + ) + end + + if h.policy(object).destroy? + links << Link.new( + content: h.destroy_link_content('lines.actions.destroy_confirm'), + href: h.line_referential_line_path(context[:line_referential], object), + method: :delete, + data: { confirm: h.t('lines.actions.destroy_confirm') } + ) + end + + links + end +end diff --git a/app/decorators/model_decorator.rb b/app/decorators/model_decorator.rb new file mode 100644 index 000000000..dee014cc3 --- /dev/null +++ b/app/decorators/model_decorator.rb @@ -0,0 +1,3 @@ +class ModelDecorator < PaginatingDecorator + delegate :model +end diff --git a/app/decorators/referential_decorator.rb b/app/decorators/referential_decorator.rb new file mode 100644 index 000000000..b95b04f9f --- /dev/null +++ b/app/decorators/referential_decorator.rb @@ -0,0 +1,56 @@ +class ReferentialDecorator < Draper::Decorator + delegate_all + + def action_links + links = [ + Link.new( + content: h.t('time_tables.index.title'), + href: h.referential_time_tables_path(object) + ) + ] + + if h.policy(object).clone? + links << Link.new( + content: h.t('actions.clone'), + href: h.new_referential_path(from: object.id) + ) + end + + if h.policy(object).edit? + + if object.archived? + links << Link.new( + content: h.t('actions.unarchive'), + href: h.unarchive_referential_path(object.id), + method: :put + ) + else + links << HTMLElement.new( + :button, + 'Purger', + type: 'button', + data: { + toggle: 'modal', + target: '#purgeModal' + } + ) + links << Link.new( + content: h.t('actions.archive'), + href: h.archive_referential_path(object.id), + method: :put + ) + end + end + + if h.policy(object).destroy? && !object.archived? + links << Link.new( + content: h.destroy_link_content, + href: h.referential_path(object), + method: :delete, + data: { confirm: h.t('referentials.actions.destroy_confirm') } + ) + end + + links + end +end diff --git a/app/decorators/route_decorator.rb b/app/decorators/route_decorator.rb new file mode 100644 index 000000000..99b174dff --- /dev/null +++ b/app/decorators/route_decorator.rb @@ -0,0 +1,64 @@ +class RouteDecorator < Draper::Decorator + decorates Chouette::Route + + delegate_all + + # Requires: + # context: { + # referential: , + # line: , + # route_sp + # } + def action_links + links = [] + + if context[:route_sp].any? + links << Link.new( + content: h.t('journey_patterns.index.title'), + href: [ + context[:referential], + context[:line], + object, + :journey_patterns_collection + ] + ) + end + + if object.journey_patterns.present? + links << Link.new( + content: h.t('vehicle_journeys.actions.index'), + href: [ + context[:referential], + context[:line], + object, + :vehicle_journeys + ] + ) + end + + links << Link.new( + content: h.t('vehicle_journey_exports.new.title'), + href: h.referential_line_route_vehicle_journey_exports_path( + context[:referential], + context[:line], + object, + format: :zip + ) + ) + + if h.policy(object).destroy? + links << Link.new( + content: h.destroy_link_content, + href: h.referential_line_route_path( + context[:referential], + context[:line], + object + ), + method: :delete, + data: { confirm: h.t('routes.actions.destroy_confirm') } + ) + end + + links + end +end diff --git a/app/decorators/routing_constraint_zone_decorator.rb b/app/decorators/routing_constraint_zone_decorator.rb new file mode 100644 index 000000000..0b438a554 --- /dev/null +++ b/app/decorators/routing_constraint_zone_decorator.rb @@ -0,0 +1,42 @@ +class RoutingConstraintZoneDecorator < Draper::Decorator + decorates Chouette::RoutingConstraintZone + + delegate_all + + # Requires: + # context: { + # referential: , + # line: + # } + def action_links + links = [] + + if h.policy(object).update? + links << Link.new( + content: h.t('actions.edit'), + href: h.edit_referential_line_routing_constraint_zone_path( + context[:referential], + context[:line], + object + ) + ) + end + + if h.policy(object).destroy? + links << Link.new( + content: h.destroy_link_content, + href: h.referential_line_routing_constraint_zone_path( + context[:referential], + context[:line], + object + ), + method: :delete, + data: { + confirm: h.t('routing_constraint_zones.actions.destroy_confirm') + } + ) + end + + links + end +end diff --git a/app/decorators/time_table_decorator.rb b/app/decorators/time_table_decorator.rb new file mode 100644 index 000000000..526537310 --- /dev/null +++ b/app/decorators/time_table_decorator.rb @@ -0,0 +1,53 @@ +class TimeTableDecorator < Draper::Decorator + decorates Chouette::TimeTable + + delegate_all + + # Requires: + # context: { + # referential: , + # } + def action_links + links = [] + + if object.calendar + links << Link.new( + content: h.t('actions.actualize'), + href: h.actualize_referential_time_table_path( + context[:referential], + object + ), + method: :post + ) + 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).duplicate? + links << Link.new( + content: h.t('actions.duplicate'), + href: h.duplicate_referential_time_table_path( + context[:referential], + object + ) + ) + end + + if h.policy(object).destroy? + links << Link.new( + content: h.destroy_link_content, + href: h.referential_time_table_path(context[:referential], object), + method: :delete, + data: { confirm: h.t('time_tables.actions.destroy_confirm') } + ) + end + + links + end +end diff --git a/app/helpers/lines_helper.rb b/app/helpers/lines_helper.rb index 45e6cd939..ccf3a12a2 100644 --- a/app/helpers/lines_helper.rb +++ b/app/helpers/lines_helper.rb @@ -6,11 +6,11 @@ module LinesHelper end def sorted_transport_submode - Chouette::Line.transport_submode.values.sort_by{|m| t("enumerize.line.transport_submode.#{m}") } + Chouette::Line.transport_submode.values.sort_by{|m| t("enumerize.line.transport_submode.#{m}").parameterize } end def sorted_transport_mode - Chouette::Line.transport_mode.values.sort_by{|m| t("enumerize.line.transport_mode.#{m}") } + Chouette::Line.transport_mode.values.sort_by{|m| t("enumerize.line.transport_mode.#{m}").parameterize } end def colors?(line) diff --git a/app/helpers/links_helper.rb b/app/helpers/links_helper.rb new file mode 100644 index 000000000..683b66a52 --- /dev/null +++ b/app/helpers/links_helper.rb @@ -0,0 +1,5 @@ +module LinksHelper + def destroy_link_content(translation_key = 'actions.destroy') + content_tag(:span, nil, class: 'fa fa-trash') + t(translation_key) + end +end diff --git a/app/helpers/multiple_selection_toolbox_helper.rb b/app/helpers/multiple_selection_toolbox_helper.rb new file mode 100644 index 000000000..85294af6d --- /dev/null +++ b/app/helpers/multiple_selection_toolbox_helper.rb @@ -0,0 +1,40 @@ +module MultipleSelectionToolboxHelper + # Box of links that floats at the bottom right of the page + def multiple_selection_toolbox(actions) + links = content_tag :ul do + delete_path = nil + + if params[:controller] = 'workbenches' + delete_path = referentials_workbench_path + end + + actions.map do |action| + if action == :delete + action_link = link_to( + '#', + method: :delete, + data: { + path: delete_path, + confirm: 'Etes-vous sûr(e) de vouloir effectuer cette action ?' + }, + title: t("actions.#{action}") + ) do + content_tag :span, '', class: 'fa fa-trash' + end + end + + content_tag :li, action_link, class: 'st_action' + end.join.html_safe + end + + label = content_tag( + :span, + ("<span>0</span> élément(s) sélectionné(s)").html_safe, + class: 'info-msg' + ) + + content_tag :div, '', class: 'select_toolbox noselect' do + links + label + end + end +end diff --git a/app/helpers/table_builder_helper.rb b/app/helpers/table_builder_helper.rb new file mode 100644 index 000000000..b93e9b22b --- /dev/null +++ b/app/helpers/table_builder_helper.rb @@ -0,0 +1,234 @@ +require 'table_builder_helper/column' +require 'table_builder_helper/custom_links' +require 'table_builder_helper/url' + +# table_builder_2 +# A Rails helper that constructs an HTML table from a collection of objects. It +# receives the collection and an array of columns that get transformed into +# `<td>`s. A column of checkboxes can be added to the left side of the table +# for multiple selection. Columns are sortable by default, but sorting can be +# disabled either at the table level or at the column level. An optional +# `links` argument takes a set of symbols corresponding to controller actions +# that should be inserted in a gear menu next to each row in the table. That +# menu will also be populated with links defined in `collection#action_links`, +# a list of `Link` objects defined in a decorator for the given object. +# +# Depends on `params` and `current_referential`. +# +# Example: +# table_builder_2( +# @companies, +# [ +# TableBuilderHelper::Column.new( +# name: 'ID Codif', +# attribute: Proc.new { |n| n.try(:objectid).try(:local_id) }, +# sortable: false +# ), +# TableBuilderHelper::Column.new( +# key: :name, +# attribute: 'name' +# ), +# TableBuilderHelper::Column.new( +# key: :phone, +# attribute: 'phone' +# ), +# TableBuilderHelper::Column.new( +# key: :email, +# attribute: 'email' +# ), +# TableBuilderHelper::Column.new( +# key: :url, +# attribute: 'url' +# ), +# ], +# links: [:show, :edit], +# cls: 'table has-search' +# ) +module TableBuilderHelper + # TODO: rename this after migration from `table_builder` + def table_builder_2( + # An `ActiveRecord::Relation`, wrapped in a decorator to provide a list of + # `Link` objects via an `#action_links` method + collection, + + # An array of `TableBuilderHelper::Column`s + columns, + + # When false, no columns will be sortable + sortable: true, + + # When true, adds a column of checkboxes to the left side of the table + selectable: false, + + # A set of controller actions that will be added as links to the top of the + # gear menu + links: [], + + # A CSS class to apply to the <table> + cls: '' + ) + content_tag :table, + thead(collection, columns, sortable, selectable, links.any?) + + tbody(collection, columns, selectable, links), + class: cls + end + + private + + def thead(collection, columns, sortable, selectable, has_links) + content_tag :thead do + content_tag :tr do + hcont = [] + + if selectable + hcont << content_tag(:th, checkbox(id_name: '0', value: 'all')) + end + + columns.each do |column| + hcont << content_tag(:th, build_column_header( + column, + sortable, + collection.model, + params, + params[:sort], + params[:direction] + )) + end + + # Inserts a blank column for the gear menu + hcont << content_tag(:th, '') if has_links + + hcont.join.html_safe + end + end + end + + def tbody(collection, columns, selectable, links) + content_tag :tbody do + collection.map do |item| + + content_tag :tr do + bcont = [] + + if selectable + bcont << content_tag( + :td, + checkbox(id_name: item.try(:id), value: item.try(:id)) + ) + end + + columns.each do |column| + value = column.value(item) + + if column_is_linkable?(column) + # Build a link to the `item` + polymorph_url = URL.polymorphic_url_parts(item) + bcont << content_tag(:td, link_to(value, polymorph_url), title: 'Voir') + else + bcont << content_tag(:td, value) + end + end + + if links.any? + bcont << content_tag( + :td, + build_links(item, links), + class: 'actions' + ) + end + + bcont.join.html_safe + end + end.join.html_safe + end + end + + def build_links(item, links) + trigger = content_tag( + :div, + class: 'btn dropdown-toggle', + data: { toggle: 'dropdown' } + ) do + content_tag :span, '', class: 'fa fa-cog' + end + + menu = content_tag :ul, class: 'dropdown-menu' do + ( + CustomLinks.new(item, pundit_user, links).links + + item.action_links.select { |link| link.is_a?(Link) } + ).map do |link| + gear_menu_link(link) + end.join.html_safe + end + + content_tag :div, trigger + menu, class: 'btn-group' + end + + def build_column_header( + column, + table_is_sortable, + collection_model, + params, + sort_on, + sort_direction + ) + if !table_is_sortable + return column.header_label(collection_model) + end + + return column.name if !column.sortable + + direction = + if column.key.to_s == sort_on && sort_direction == 'desc' + 'asc' + else + 'desc' + end + + link_to(params.merge({direction: direction, sort: column.key})) do + arrow_up = content_tag( + :span, + '', + class: "fa fa-sort-asc #{direction == 'desc' ? 'active' : ''}" + ) + arrow_down = content_tag( + :span, + '', + class: "fa fa-sort-desc #{direction == 'asc' ? 'active' : ''}" + ) + + arrow_icons = content_tag :span, arrow_up + arrow_down, class: 'orderers' + + ( + column.header_label(collection_model) + + arrow_icons + ).html_safe + end + end + + def checkbox(id_name:, value:) + content_tag :div, '', class: 'checkbox' do + check_box_tag(id_name, value).concat( + content_tag(:label, '', for: id_name) + ) + end + end + + def column_is_linkable?(column) + column.attribute == 'name' || column.attribute == 'comment' + end + + def gear_menu_link(link) + content_tag( + :li, + link_to( + link.href, + method: link.method, + data: link.data + ) do + link.content + end, + class: ('delete-action' if link.method == :delete) + ) + end +end diff --git a/app/helpers/table_builder_helper/column.rb b/app/helpers/table_builder_helper/column.rb new file mode 100644 index 000000000..800a8282e --- /dev/null +++ b/app/helpers/table_builder_helper/column.rb @@ -0,0 +1,36 @@ +module TableBuilderHelper + class Column + attr_reader :key, :name, :attribute, :sortable + + def initialize(key: nil, name: '', attribute:, sortable: true) + if key.nil? && name.empty? + raise ColumnMustHaveKeyOrNameError + end + + @key = key + @name = name + @attribute = attribute + @sortable = sortable + end + + def value(obj) + if @attribute.is_a?(Proc) + @attribute.call(obj) + else + obj.try(@attribute) + end + end + + def header_label(model = nil) + return @name unless @name.empty? + + # Transform `Chouette::Line` into "line" + model_key = model.to_s.demodulize.underscore + + I18n.t("activerecord.attributes.#{model_key}.#{@key}") + end + end + + + class ColumnMustHaveKeyOrNameError < StandardError; end +end diff --git a/app/helpers/table_builder_helper/custom_links.rb b/app/helpers/table_builder_helper/custom_links.rb new file mode 100644 index 000000000..abb907678 --- /dev/null +++ b/app/helpers/table_builder_helper/custom_links.rb @@ -0,0 +1,77 @@ +require 'table_builder_helper/url' + +module TableBuilderHelper + class CustomLinks + ACTIONS_TO_HTTP_METHODS = { + delete: :delete, + archive: :put, + unarchive: :put + } + + def initialize(obj, user_context, actions) + @obj = obj + @user_context = user_context + @actions = actions + end + + def links + actions_after_policy_check.map do |action| + Link.new( + content: I18n.t("actions.#{action}"), + href: polymorphic_url(action), + method: method_for_action(action) + ) + end + end + + def polymorphic_url(action) + polymorph_url = [] + + unless [:show, :delete].include?(action) + polymorph_url << action + end + + polymorph_url += URL.polymorphic_url_parts(@obj) + end + + def method_for_action(action) + ACTIONS_TO_HTTP_METHODS[action] + end + + def actions_after_policy_check + @actions.select do |action| + # Has policy and can destroy + (action == :delete && + Pundit.policy(@user_context, @obj).present? && + Pundit.policy(@user_context, @obj).destroy?) || + + # Doesn't have policy + (action == :delete && + !Pundit.policy(@user_context, @obj).present?) || + + # Has policy and can update + (action == :edit && + Pundit.policy(@user_context, @obj).present? && + Pundit.policy(@user_context, @obj).update?) || + + # Doesn't have policy + (action == :edit && + !Pundit.policy(@user_context, @obj).present?) || + + # Object isn't archived + (action == :archive && !@obj.archived?) || + + # Object is archived + (action == :unarchive && @obj.archived?) || + + action_is_allowed_regardless_of_policy(action) + end + end + + private + + def action_is_allowed_regardless_of_policy(action) + ![:delete, :edit, :archive, :unarchive].include?(action) + end + end +end diff --git a/app/helpers/table_builder_helper/url.rb b/app/helpers/table_builder_helper/url.rb new file mode 100644 index 000000000..f60864ac1 --- /dev/null +++ b/app/helpers/table_builder_helper/url.rb @@ -0,0 +1,25 @@ +module TableBuilderHelper + # Depends on `current_referential`, defined in object controllers + class URL + def self.polymorphic_url_parts(item) + polymorph_url = [] + + unless item.is_a?(Calendar) || item.is_a?(Referential) + if current_referential + polymorph_url << current_referential + polymorph_url << item.line if item.respond_to? :line + polymorph_url << item.route.line if item.is_a?(Chouette::RoutingConstraintZone) + polymorph_url << item if item.respond_to? :line_referential + 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 + end + else + polymorph_url << item + end + + polymorph_url + end + end +end diff --git a/app/models/calendar.rb b/app/models/calendar.rb index 39e0d6d49..70fea9619 100644 --- a/app/models/calendar.rb +++ b/app/models/calendar.rb @@ -1,5 +1,9 @@ require 'range_ext' +require_relative 'calendar/date_value' +require_relative 'calendar/period' + class Calendar < ActiveRecord::Base + belongs_to :organisation has_many :time_tables @@ -30,69 +34,11 @@ class Calendar < ActiveRecord::Base end end - class Period - include ActiveAttr::Model - - attribute :id, type: Integer - attribute :begin, type: Date - attribute :end, type: Date - - validates_presence_of :begin, :end - validate :check_end_greather_than_begin - - def check_end_greather_than_begin - if self.begin and self.end and self.begin > self.end - errors.add(:end, :invalid) - end - end - - def self.from_range(index, range) - Period.new id: index, begin: range.begin, end: range.end - end - - def range - if self.begin and self.end and self.begin <= self.end - Range.new self.begin, self.end - end - end - - def intersect?(*other) - return false if range.nil? - - other = other.flatten - other = other.delete_if { |o| o.id == id } if id - - other.any? do |period| - if other_range = period.range - (range & other_range).present? - end - end - end - - def cover? date - range.cover? date - end - - # Stuff required for coocon - def new_record? - !persisted? - end - - def persisted? - id.present? - end - - def mark_for_destruction - self._destroy = true - end - - attribute :_destroy, type: Boolean - alias_method :marked_for_destruction?, :_destroy - end + ### Calendar::Period # Required by coocon def build_period - Period.new + Calendar::Period.new end def periods @@ -100,22 +46,16 @@ class Calendar < ActiveRecord::Base end def init_periods - if date_ranges - date_ranges.each_with_index.map { |r, index| Period.from_range(index, r) } - else - [] - end + (date_ranges || []) + .each_with_index + .map( &Calendar::Period.method(:from_range) ) end private :init_periods validate :validate_periods def validate_periods - periods_are_valid = true - - unless periods.all?(&:valid?) - periods_are_valid = false - end + periods_are_valid = periods.all?(&:valid?) periods.each do |period| if period.intersect?(periods) @@ -141,7 +81,7 @@ class Calendar < ActiveRecord::Base ['begin', 'end'].map do |attr| period_attribute[attr] = flatten_date_array(period_attribute, attr) end - period = Period.new(period_attribute.merge(id: index)) + period = Calendar::Period.new(period_attribute.merge(id: index)) @periods << period unless period.marked_for_destruction? end @@ -164,40 +104,11 @@ class Calendar < ActiveRecord::Base private :clear_periods -### dates - - class DateValue - include ActiveAttr::Model - - attribute :id, type: Integer - attribute :value, type: Date - - validates_presence_of :value - - def self.from_date(index, date) - DateValue.new id: index, value: date - end - - # Stuff required for coocon - def new_record? - !persisted? - end - - def persisted? - id.present? - end - - def mark_for_destruction - self._destroy = true - end - - attribute :_destroy, type: Boolean - alias_method :marked_for_destruction?, :_destroy - end + ### Calendar::DateValue # Required by coocon def build_date_value - DateValue.new + Calendar::DateValue.new end def date_values @@ -206,7 +117,7 @@ class Calendar < ActiveRecord::Base def init_date_values if dates - dates.each_with_index.map { |d, index| DateValue.from_date(index, d) } + dates.each_with_index.map { |d, index| Calendar::DateValue.from_date(index, d) } else [] end @@ -216,11 +127,7 @@ class Calendar < ActiveRecord::Base validate :validate_date_values def validate_date_values - date_values_are_valid = true - - unless date_values.all?(&:valid?) - date_values_are_valid = false - end + date_values_are_valid = date_values.all?(&:valid?) date_values.each do |date_value| if date_values.count { |d| d.value == date_value.value } > 1 @@ -244,7 +151,7 @@ class Calendar < ActiveRecord::Base @date_values = [] attributes.each do |index, date_value_attribute| date_value_attribute['value'] = flatten_date_array(date_value_attribute, 'value') - date_value = DateValue.new(date_value_attribute.merge(id: index)) + date_value = Calendar::DateValue.new(date_value_attribute.merge(id: index)) @date_values << date_value unless date_value.marked_for_destruction? end diff --git a/app/models/calendar/date_value.rb b/app/models/calendar/date_value.rb new file mode 100644 index 000000000..a4a405d43 --- /dev/null +++ b/app/models/calendar/date_value.rb @@ -0,0 +1,32 @@ +class Calendar < ActiveRecord::Base + + class DateValue + include ActiveAttr::Model + + attribute :id, type: Integer + attribute :value, type: Date + + validates_presence_of :value + + def self.from_date(index, date) + new id: index, value: date + end + + # Stuff required for coocon + def new_record? + !persisted? + end + + def persisted? + id.present? + end + + def mark_for_destruction + self._destroy = true + end + + attribute :_destroy, type: Boolean + alias_method :marked_for_destruction?, :_destroy + end + +end diff --git a/app/models/calendar/period.rb b/app/models/calendar/period.rb new file mode 100644 index 000000000..1c423dfcc --- /dev/null +++ b/app/models/calendar/period.rb @@ -0,0 +1,63 @@ +class Calendar < ActiveRecord::Base + + class Period + include ActiveAttr::Model + + attribute :id, type: Integer + attribute :begin, type: Date + attribute :end, type: Date + + validates_presence_of :begin, :end + validate :check_end_greather_than_begin + + def check_end_greather_than_begin + if self.begin && self.end && self.begin >= self.end + errors.add(:base, I18n.t('calendars.errors.short_period')) + end + end + + def self.from_range(range, index) + last = range.exclude_end? ? range.end - 1.day : range.end + Period.new id: index, begin: range.begin, end: last + end + + def range + if self.begin && self.end && self.begin <= self.end + Range.new self.begin, self.end + end + end + + def intersect?(*other) + return false if range.nil? + + other = other.flatten + other = other.delete_if { |o| o.id == id } if id + + other.any? do |period| + if other_range = period.range + (range & other_range).present? + end + end + end + + def cover? date + range.cover? date + end + + # Stuff required for coocon + def new_record? + !persisted? + end + + def persisted? + id.present? + end + + def mark_for_destruction + self._destroy = true + end + + attribute :_destroy, type: Boolean + alias_method :marked_for_destruction?, :_destroy + end +end diff --git a/app/models/chouette/journey_pattern.rb b/app/models/chouette/journey_pattern.rb index 34b0d9345..a146dcff1 100644 --- a/app/models/chouette/journey_pattern.rb +++ b/app/models/chouette/journey_pattern.rb @@ -14,19 +14,18 @@ class Chouette::JourneyPattern < Chouette::TridentActiveRecord validates_presence_of :route validates_presence_of :name + validates :stop_points, length: { minimum: 2, too_short: :minimum }, on: :update enum section_status: { todo: 0, completed: 1, control: 2 } attr_accessor :control_checked after_update :control_route_sections, :unless => "control_checked" - def self.state_update route, state transaction do state.each do |item| item.delete('errors') jp = find_by(objectid: item['object_id']) || state_create_instance(route, item) next if item['deletable'] && jp.persisted? && jp.destroy - # Update attributes and stop_points associations jp.update_attributes(state_permited_attributes(item)) jp.state_stop_points_update(item) if !jp.errors.any? && jp.persisted? diff --git a/app/models/chouette/time_table.rb b/app/models/chouette/time_table.rb index 4186af6d2..c566452f4 100644 --- a/app/models/chouette/time_table.rb +++ b/app/models/chouette/time_table.rb @@ -477,63 +477,22 @@ class Chouette::TimeTable < Chouette::TridentActiveRecord # merge effective days from another timetable def merge!(another_tt) transaction do - # merge dates - self.dates ||= [] - another_tt.included_days.each do |d| - add_included_day d - end + self.periods = another_tt.clone_periods + self.periods + self.periods = self.optimize_periods - # if one tt has no period, just merge lists - if self.periods.empty? || another_tt.periods.empty? - if !another_tt.periods.empty? - # copy periods - self.periods = another_tt.clone_periods - end - else - # check if periods can be kept - common_day_types = self.int_day_types & another_tt.int_day_types & 508 - # if common day types : merge periods - if common_day_types != 0 - periods = self.optimize_periods - another_periods = another_tt.optimize_periods - # add not common days of both periods as peculiar days - self.effective_days_of_periods(self.class.valid_days(self.int_day_types ^ common_day_types)).each do |d| - self.dates |= [Chouette::TimeTableDate.new(:date => d, :in_out => true)] - end - another_tt.effective_days_of_periods(self.class.valid_days(another_tt.int_day_types ^ common_day_types)).each do |d| - add_included_day d - end - # merge periods - self.periods = periods | another_periods - self.int_day_types = common_day_types - self.periods = self.optimize_periods - else - # convert all period in days - self.effective_days_of_periods.each do |d| - self.dates << Chouette::TimeTableDate.new(:date => d, :in_out => true) unless self.include_in_dates?(d) - end - another_tt.effective_days_of_periods.each do |d| - add_included_day d + # For included dates + another_tt.included_days.map{ |d| add_included_day(d) } + + # For excluded dates + existing_out_date = self.dates.where(in_out: false).map(&:date) + another_tt.dates.where(in_out: false).each do |d| + unless existing_out_date.include?(d.date) + self.dates << Chouette::TimeTableDate.new(:date => d.date, :in_out => false) end end - end - # if remained excluded dates are valid in other tt , remove it from result - self.dates.each do |date| - date.in_out = true if date.in_out == false && another_tt.include_day?(date.date) + self.save! end - # if peculiar dates are valid in new periods, remove them - if !self.periods.empty? - days_in_period = self.effective_days_of_periods - dates = [] - self.dates.each do |date| - dates << date unless date.in_out && days_in_period.include?(date.date) - end - self.dates = dates - end - self.dates.to_a.sort! { |a,b| a.date <=> b.date} - self.save! - end self.convert_continuous_dates_to_periods end diff --git a/app/models/chouette/vehicle_journey.rb b/app/models/chouette/vehicle_journey.rb index 44dd85864..3a5851310 100644 --- a/app/models/chouette/vehicle_journey.rb +++ b/app/models/chouette/vehicle_journey.rb @@ -256,7 +256,7 @@ module Chouette .where( %Q( "vehicle_journey_at_stops"."departure_time" >= ? - AND "vehicle_journey_at_stops"."departure_time" < ? + AND "vehicle_journey_at_stops"."departure_time" <= ? #{ if allow_empty 'OR "vehicle_journey_at_stops"."id" IS NULL' diff --git a/app/models/clean_up.rb b/app/models/clean_up.rb index a791065aa..e39928a17 100644 --- a/app/models/clean_up.rb +++ b/app/models/clean_up.rb @@ -19,6 +19,7 @@ class CleanUp < ActiveRecord::Base result['time_table'] = send("destroy_time_tables_#{self.date_type}").try(:count) result['time_table_date'] = send("destroy_time_tables_dates_#{self.date_type}").try(:count) result['time_table_period'] = send("destroy_time_tables_periods_#{self.date_type}").try(:count) + result['vehicle_journey'] = destroy_vehicle_journey_without_time_table.try(:count) self.overlapping_periods.each do |period| exclude_dates_in_overlapping_period(period) end @@ -100,9 +101,16 @@ class CleanUp < ActiveRecord::Base end end + def destroy_vehicle_journey_without_time_table + Chouette::VehicleJourney.without_any_time_table.destroy_all + end + def destroy_time_tables(time_tables) + # Delete vehicle_journey time_table association time_tables.each do |time_table| - time_table.vehicle_journeys.map(&:destroy) + time_table.vehicle_journeys.each do |vj| + vj.time_tables.delete(time_table) + end end time_tables.destroy_all end diff --git a/app/models/concerns/default_attributes_support.rb b/app/models/concerns/default_attributes_support.rb index ecad26856..7928093e6 100644 --- a/app/models/concerns/default_attributes_support.rb +++ b/app/models/concerns/default_attributes_support.rb @@ -63,10 +63,10 @@ module DefaultAttributesSupport def fix_uniq_objectid base_objectid = objectid.rpartition(":").first self.objectid = "#{base_objectid}:#{id}" - if !valid? + if !valid?(:objectid) base_objectid="#{objectid}_" cnt=1 - while !valid? + while !valid?(:objectid) self.objectid = "#{base_objectid}#{cnt}" cnt += 1 end diff --git a/app/models/user.rb b/app/models/user.rb index 1a06746da..4ba05b164 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -31,13 +31,14 @@ class User < ActiveRecord::Base @@edit_offer_permissions = ['routes.create', 'routes.edit', 'routes.destroy', 'journey_patterns.create', 'journey_patterns.edit', 'journey_patterns.destroy', 'vehicle_journeys.create', 'vehicle_journeys.edit', 'vehicle_journeys.destroy', 'time_tables.create', 'time_tables.edit', 'time_tables.destroy', 'footnotes.edit', 'footnotes.create', 'footnotes.destroy', 'routing_constraint_zones.create', 'routing_constraint_zones.edit', - 'routing_constraint_zones.destroy', 'referentials.create', 'referentials.edit', 'referentials.destroy'] + 'routing_constraint_zones.destroy', 'referentials.create', 'referentials.edit', 'referentials.destroy', 'boiv:edit-offer'] mattr_reader :edit_offer_permissions def self.all_permissions edit_offer_permissions end + # Callback invoked by DeviseCasAuthenticable::Model#authernticate_with_cas_ticket def cas_extra_attributes=(extra_attributes) extra = extra_attributes.inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo} self.name = extra[:full_name] diff --git a/app/views/calendars/_date_value_fields.html.slim b/app/views/calendars/_date_value_fields.html.slim index 2d072fa0a..7bde06a94 100644 --- a/app/views/calendars/_date_value_fields.html.slim +++ b/app/views/calendars/_date_value_fields.html.slim @@ -8,6 +8,6 @@ .wrapper div - = f.input :value, as: :date, label: false, wrapper_html: { class: 'date' } + = f.input :value, as: :date, label: false, wrapper_html: { class: 'date smart_date' } div = link_to_remove_association '', f, class: 'fa fa-trash', data: { confirm: 'Etes-vous sûr(e) ?' }, title: t('actions.delete') diff --git a/app/views/calendars/_filters.html.slim b/app/views/calendars/_filters.html.slim index 6bcf25f99..4c30f69dc 100644 --- a/app/views/calendars/_filters.html.slim +++ b/app/views/calendars/_filters.html.slim @@ -15,7 +15,7 @@ .form-group = f.label Calendar.human_attribute_name(:date), class: 'control-label' - = f.input :contains_date, as: :date, label: false, wrapper_html: { class: 'date' }, class: 'form-control', include_blank: true + = f.input :contains_date, as: :date, label: false, wrapper_html: { class: 'date smart_date' }, class: 'form-control', include_blank: true .actions = link_to 'Effacer', calendars_path, class: 'btn btn-link' diff --git a/app/views/calendars/_period_fields.html.slim b/app/views/calendars/_period_fields.html.slim index 1e201a39f..95e204554 100644 --- a/app/views/calendars/_period_fields.html.slim +++ b/app/views/calendars/_period_fields.html.slim @@ -8,8 +8,8 @@ .wrapper div - = f.input :begin, as: :date, label: false, wrapper_html: { class: 'date' } + = f.input :begin, as: :date, label: false, wrapper_html: { class: 'date smart_date' } div - = f.input :end, as: :date, label: false, wrapper_html: { class: 'date' } + = f.input :end, as: :date, label: false, wrapper_html: { class: 'date smart_date' } div = link_to_remove_association '', f, class: 'fa fa-trash', data: { confirm: 'Etes-vous sûr(e) ?' }, title: t('actions.delete') diff --git a/app/views/calendars/show.html.slim b/app/views/calendars/show.html.slim index 3886cefaa..d7849005b 100644 --- a/app/views/calendars/show.html.slim +++ b/app/views/calendars/show.html.slim @@ -8,10 +8,12 @@ / Below is secondary actions & optional contents (filters, ...) .row.mb-sm .col-lg-12.text-right - - if policy(@calendar).destroy? - = link_to calendar_path(@calendar), method: :delete, data: { confirm: t('calendars.actions.destroy_confirm') }, class: 'btn btn-primary' do - span.fa.fa-trash - span = t('actions.destroy') + - @calendar.action_links.each do |link| + = link_to link.href, + method: link.method, + data: link.data, + class: 'btn btn-primary' do + = link.content / PageContent .page_content @@ -23,4 +25,4 @@ Calendar.human_attribute_name(:shared) => t("#{@calendar.shared}"), 'Organisation' => @calendar.organisation.name, Calendar.human_attribute_name(:dates) => @calendar.dates.collect{|d| l(d, format: :short)}.join(', ').html_safe, - Calendar.human_attribute_name(:date_ranges) => @calendar.date_ranges.collect{|d| t('validity_range', debut: l(d.begin, format: :short), end: l(d.end, format: :short))}.join('<br>').html_safe } + Calendar.human_attribute_name(:date_ranges) => @calendar.date_ranges.collect{|d| t('validity_range', debut: l(d.begin, format: :short), end: l(d.end - (d.exclude_end? ? 1.day : 0), format: :short))}.join('<br>').html_safe } diff --git a/app/views/lines/show.html.slim b/app/views/lines/show.html.slim index dbc019e72..6f75432e1 100644 --- a/app/views/lines/show.html.slim +++ b/app/views/lines/show.html.slim @@ -7,17 +7,12 @@ / Below is secundary actions & optional contents .row .col-lg-12.text-right.mb-sm - = link_to t('lines.actions.show_network'), [@line_referential, @line.network], class: 'btn btn-primary' - = link_to t('lines.actions.show_company'), [@line_referential, @line.company], class: 'btn btn-primary' - - - if policy(Chouette::Line).create? && @line_referential.organisations.include?(current_organisation) - = link_to t('lines.actions.new'), new_line_referential_line_path(@line_referential), class: 'btn btn-primary' - - if false && policy(@line).update? - = link_to t('lines.actions.edit'), edit_line_referential_line_path(@line_referential, @line), class: 'btn btn-primary' - - if policy(@line).destroy? - = link_to line_referential_line_path(@line_referential, @line), method: :delete, data: {confirm: t('lines.actions.destroy_confirm')}, class: 'btn btn-primary' do - span.fa.fa-trash - span = t('lines.actions.destroy') + - @line.action_links.each do |link| + = link_to link.href, + method: link.method, + data: link.data, + class: 'btn btn-primary' do + = link.content / PageContent .page_content diff --git a/app/views/referentials/_period_fields.html.slim b/app/views/referentials/_period_fields.html.slim index 1e201a39f..95e204554 100644 --- a/app/views/referentials/_period_fields.html.slim +++ b/app/views/referentials/_period_fields.html.slim @@ -8,8 +8,8 @@ .wrapper div - = f.input :begin, as: :date, label: false, wrapper_html: { class: 'date' } + = f.input :begin, as: :date, label: false, wrapper_html: { class: 'date smart_date' } div - = f.input :end, as: :date, label: false, wrapper_html: { class: 'date' } + = f.input :end, as: :date, label: false, wrapper_html: { class: 'date smart_date' } div = link_to_remove_association '', f, class: 'fa fa-trash', data: { confirm: 'Etes-vous sûr(e) ?' }, title: t('actions.delete') diff --git a/app/views/referentials/show.html.slim b/app/views/referentials/show.html.slim index 3c1e36302..8308cf770 100644 --- a/app/views/referentials/show.html.slim +++ b/app/views/referentials/show.html.slim @@ -8,23 +8,15 @@ / Below is secondary actions & optional contents (filters, ...) .row.mb-sm .col-lg-12.text-right - = link_to t('time_tables.index.title'), referential_time_tables_path(@referential), class: 'btn btn-primary' - - - if policy(@referential).clone? - = link_to t('actions.clone'), new_referential_path(from: @referential.id), class: 'btn btn-primary' - - - if policy(@referential).edit? - button.btn.btn-primary type='button' data-toggle='modal' data-target='#purgeModal' Purger - - - if @referential.archived? - = link_to t('actions.unarchive'), unarchive_referential_path(@referential.id), method: :put, class: 'btn btn-primary' + - @referential.action_links.each do |link| + - if link.is_a?(HTMLElement) + = link.to_html(class: 'btn btn-primary') - else - = link_to t('actions.archive'), archive_referential_path(@referential.id), method: :put, class: 'btn btn-primary' - - - if policy(@referential).destroy? - = link_to referential_path(@referential), method: :delete, data: {confirm: t('referentials.actions.destroy_confirm')}, class: 'btn btn-primary' do - span.fa.fa-trash - span = t('actions.destroy') + = link_to link.href, + method: link.method, + data: link.data, + class: 'btn btn-primary' do + = link.content / PageContent .page_content @@ -76,9 +68,9 @@ .row .col-lg-8.col-ld-offset-2.col-md-8.col-md-offset-2.col-sm-8.col-sm-offset-2.col-xs-12 = f.input :date_type, as: :radio_buttons, label: false - = f.input :begin_date, as: :date, label: t('titles.clean_up.begin_date'),:wrapper_html => { class: 'date', title: t('titles.clean_up.begin_date') } + = f.input :begin_date, as: :date, label: t('titles.clean_up.begin_date'),:wrapper_html => { class: 'date smart_date', title: t('titles.clean_up.begin_date') } - = f.input :end_date, as: :date, label: t('titles.clean_up.end_date'), :wrapper_html => { class: 'date cleanup_end_date_wrapper hidden', title: t('titles.clean_up.end_date') } + = f.input :end_date, as: :date, label: t('titles.clean_up.end_date'), :wrapper_html => { class: 'date cleanup_end_date_wrapper hidden smert_date', title: t('titles.clean_up.end_date') } .modal-footer button.btn.btn-link type='button' data-dismiss='modal' Annuler diff --git a/app/views/routes/show.html.slim b/app/views/routes/show.html.slim index 6d2d4e90d..92a5080ae 100644 --- a/app/views/routes/show.html.slim +++ b/app/views/routes/show.html.slim @@ -8,17 +8,12 @@ / Below is secundary actions & optional contents (filters, ...) .row.mb-sm .col-lg-12.text-right - - if @route_sp.any? - = link_to t('journey_patterns.index.title'), [@referential, @line, @route, :journey_patterns_collection], class: 'btn btn-primary' - - if @route.journey_patterns.present? - = link_to t('vehicle_journeys.actions.index'), [@referential, @line, @route, :vehicle_journeys], class: 'btn btn-primary' - - = link_to t('vehicle_journey_exports.new.title'), referential_line_route_vehicle_journey_exports_path(@referential, @line, @route, format: :zip), class: 'btn btn-primary' - - - if policy(@route).destroy? - = link_to referential_line_route_path(@referential, @line, @route), method: :delete, data: {confirm: t('routes.actions.destroy_confirm')}, class: 'btn btn-primary' do - span.fa.fa-trash - span = t('actions.destroy') + - @route.action_links.each do |link| + = link_to link.href, + method: link.method, + data: link.data, + class: 'btn btn-primary' do + = link.content / PageContent .page_content diff --git a/app/views/routing_constraint_zones/_filters.html.slim b/app/views/routing_constraint_zones/_filters.html.slim index 18ef40d61..07d351b1f 100644 --- a/app/views/routing_constraint_zones/_filters.html.slim +++ b/app/views/routing_constraint_zones/_filters.html.slim @@ -9,7 +9,7 @@ .ffg-row .form-group = f.label 'Itinéraire associé', required: false, class: 'control-label' - = f.input :route_id, as: :select, collection: @line.routing_constraint_zones.pluck(:route_id), label: false, label_method: lambda {|r| @line.routing_constraint_zones.find_by(route_id: r).route_name}, input_html: { 'data-select2ed': 'true', 'data-select2ed-placeholder': 'Indiquez un itinéraire...' }, wrapper_html: { class: 'select2ed'} + = f.input :route_id_eq, as: :select, collection: @line.routing_constraint_zones.pluck(:route_id).uniq, label: false, label_method: lambda { |r| @line.routing_constraint_zones.find_by(route_id: r).route_name }, input_html: { 'data-select2ed': 'true', 'data-select2ed-placeholder': 'Indiquez un itinéraire...' }, wrapper_html: { class: 'select2ed'} .actions = link_to 'Effacer', referential_line_routing_constraint_zones_path(@referential, @line), class: 'btn btn-link' diff --git a/app/views/routing_constraint_zones/show.html.slim b/app/views/routing_constraint_zones/show.html.slim index f0c244387..1dad4f561 100644 --- a/app/views/routing_constraint_zones/show.html.slim +++ b/app/views/routing_constraint_zones/show.html.slim @@ -7,13 +7,12 @@ / Below is secundary actions & optional contents .row .col-lg-12.text-right.mb-sm - - if policy(@routing_constraint_zone).update? - = link_to t('actions.edit'), edit_referential_line_routing_constraint_zone_path(@referential, @line, @routing_constraint_zone), class: 'btn btn-primary' - - - if policy(@routing_constraint_zone).destroy? - = link_to referential_line_routing_constraint_zone_path(@referential, @line, @routing_constraint_zone), method: :delete, data: {confirm: t('routing_constraint_zones.actions.destroy_confirm')}, class: 'btn btn-primary' do - span.fa.fa-trash - span = t('actions.destroy') + - @routing_constraint_zone.action_links.each do |link| + = link_to link.href, + method: link.method, + data: link.data, + class: 'btn btn-primary' do + = link.content / PageContent .page_content diff --git a/app/views/time_tables/_date_fields.html.slim b/app/views/time_tables/_date_fields.html.slim index 1599dd7ff..811c241bb 100644 --- a/app/views/time_tables/_date_fields.html.slim +++ b/app/views/time_tables/_date_fields.html.slim @@ -8,7 +8,7 @@ .wrapper div - = f.input :date, as: :date, label: false, wrapper_html: { class: 'date' } + = f.input :date, as: :date, label: false, wrapper_html: { class: 'date smart_date' } = f.input :in_out, as: :hidden, :input_html => {:value => true} div = link_to_remove_association '', f, class: 'fa fa-trash', data: { confirm: 'Etes-vous sûr(e) ?' }, title: t('actions.delete') diff --git a/app/views/time_tables/_excluded_date_fields.html.slim b/app/views/time_tables/_excluded_date_fields.html.slim index dba5bf952..8e4003920 100644 --- a/app/views/time_tables/_excluded_date_fields.html.slim +++ b/app/views/time_tables/_excluded_date_fields.html.slim @@ -9,7 +9,7 @@ .wrapper div / = f.label @time_table.human_attribute_name("date"), class: 'col-md-1' - = f.input :date, as: :date, label: false, wrapper_html: { class: 'date' } + = f.input :date, as: :date, label: false, wrapper_html: { class: 'date smart_date' } = f.input :in_out, as: :hidden, input_html: {value: false} div = link_to_remove_association '', f, class: 'fa fa-trash', data: { confirm: 'Etes-vous sûr(e) ?' }, title: t('actions.delete') diff --git a/app/views/time_tables/_filter.html.slim b/app/views/time_tables/_filter.html.slim index cee915911..10e46d884 100644 --- a/app/views/time_tables/_filter.html.slim +++ b/app/views/time_tables/_filter.html.slim @@ -18,8 +18,8 @@ .form-group.togglable = f.label @time_tables.human_attribute_name(:bounding_dates), required: false, class: 'control-label' .filter_menu - = f.input :start_date_gteq, as: :date, label: t('simple_form.from'), wrapper_html: { class: 'date filter_menu-item' }, default: @begin_range, include_blank: @begin_range ? false : true - = f.input :end_date_lteq, as: :date, label: t('simple_form.to'), wrapper_html: { class: 'date filter_menu-item' }, default: @end_range, include_blank: @end_range ? false : true + = f.input :start_date_gteq, as: :date, label: t('simple_form.from'), wrapper_html: { class: 'date smart_date filter_menu-item' }, default: @begin_range, include_blank: @begin_range ? false : true + = f.input :end_date_lteq, as: :date, label: t('simple_form.to'), wrapper_html: { class: 'date smart_date filter_menu-item' }, default: @end_range, include_blank: @end_range ? false : true .actions diff --git a/app/views/time_tables/_period_fields.html.slim b/app/views/time_tables/_period_fields.html.slim index f879ded00..6b3fde311 100644 --- a/app/views/time_tables/_period_fields.html.slim +++ b/app/views/time_tables/_period_fields.html.slim @@ -8,8 +8,8 @@ .wrapper div - = f.input :period_start, as: :date, label: false, wrapper_html: { class: 'date' } + = f.input :period_start, as: :date, label: false, wrapper_html: { class: 'date smart_date' } div - = f.input :period_end, as: :date, label: false, wrapper_html: { class: 'date' } + = f.input :period_end, as: :date, label: false, wrapper_html: { class: 'date smart_date' } div = link_to_remove_association '', f, class: 'fa fa-trash', data: { confirm: 'Etes-vous sûr(e) ?' }, title: t('actions.delete') diff --git a/app/views/time_tables/show.html.slim b/app/views/time_tables/show.html.slim index 2e71ebb9e..f596fd480 100644 --- a/app/views/time_tables/show.html.slim +++ b/app/views/time_tables/show.html.slim @@ -10,21 +10,12 @@ / Below is secundary actions & optional contents (filters, ...) .row.mb-sm .col-lg-12.text-right - / - if policy(@time_table).create? && @referential.organisation == current_organisation - / = link_to t('time_tables.actions.new'), new_referential_time_table_path(@referential), class: 'btn btn-primary' - - if @time_table.calendar - = link_to t('actions.actualize'), actualize_referential_time_table_path(@referential, @time_table), method: :post, class: 'btn btn-primary' - - /- if policy(@time_table).create? && @referential.organisation == current_organisation - = link_to t('actions.combine'), new_referential_time_table_time_table_combination_path(@referential, @time_table), class: 'btn btn-primary' - - - if policy(@time_table).duplicate? - = link_to t('actions.duplicate'), duplicate_referential_time_table_path(@referential, @time_table), class: 'btn btn-primary' - - - if policy(@time_table).destroy? - = link_to referential_time_table_path(@referential, @time_table), method: :delete, data: {confirm: t('time_tables.actions.destroy_confirm')}, class: 'btn btn-primary' do - span.fa.fa-trash - span = t('actions.destroy') + - @time_table.action_links.each do |link| + = link_to link.href, + method: link.method, + data: link.data, + class: 'btn btn-primary' do + = link.content / PageContent .page_content diff --git a/app/views/workbenches/_filters.html.slim b/app/views/workbenches/_filters.html.slim index 7c5055963..4eaf910c0 100644 --- a/app/views/workbenches/_filters.html.slim +++ b/app/views/workbenches/_filters.html.slim @@ -12,7 +12,7 @@ = f.input :associated_lines_id_eq, as: :select, collection: @workbench.lines.includes(:company).order(:name), input_html: { 'data-select2ed': 'true', 'data-select2ed-placeholder': 'Indiquez une ligne...' }, label: false, label_method: :display_name, wrapper_html: { class: 'select2ed'} .form-group.togglable - = f.label @wbench_refs.human_attribute_name(:status), required: false, class: 'control-label' + = f.label Referential.human_attribute_name(:status), required: false, class: 'control-label' .form-group.checkbox_list = f.input :archived_at_not_null, label: ("<span>Conservé<span class='fa fa-archive'></span></span>").html_safe, as: :boolean, wrapper_html: { class: 'checkbox-wrapper' } = f.input :archived_at_null, label: ("<span>En préparation<span class='sb sb-lg sb-preparing'></span></span>").html_safe, as: :boolean, wrapper_html: { class: 'checkbox-wrapper' } @@ -22,11 +22,11 @@ = f.input :organisation_name_eq_any, collection: Organisation.order('name').pluck(:name), as: :check_boxes, label: false, label_method: lambda{|w| ("<span>#{w}</span>").html_safe}, required: false, wrapper_html: { class: 'checkbox_list' } .form-group.togglable - = f.label @wbench_refs.human_attribute_name(:validity_period), required: false, class: 'control-label' + = f.label Referential.human_attribute_name(:validity_period), required: false, class: 'control-label' .filter_menu = f.simple_fields_for :validity_period do |p| - = p.input :begin_gteq, as: :date, label: t('simple_form.from'), wrapper_html: { class: 'date filter_menu-item' }, default: @begin_range, include_blank: @begin_range ? false : true - = p.input :end_lteq, as: :date, label: t('simple_form.to'), wrapper_html: { class: 'date filter_menu-item' }, default: @end_range, include_blank: @end_range ? false : true + = p.input :begin_gteq, as: :date, label: t('simple_form.from'), wrapper_html: { class: 'date smart_date filter_menu-item' }, default: @begin_range, include_blank: @begin_range ? false : true + = p.input :end_lteq, as: :date, label: t('simple_form.to'), wrapper_html: { class: 'date smart_date filter_menu-item' }, default: @end_range, include_blank: @end_range ? false : true .actions = link_to 'Effacer', @workbench, class: 'btn btn-link' diff --git a/app/views/workbenches/show.html.slim b/app/views/workbenches/show.html.slim index 77e670923..37c396b46 100644 --- a/app/views/workbenches/show.html.slim +++ b/app/views/workbenches/show.html.slim @@ -22,18 +22,47 @@ - if @wbench_refs.any? .row .col-lg-12 - = table_builder @wbench_refs, - { :name => 'name', - :status => Proc.new {|w| w.archived? ? ("<div class='td-block'><span class='fa fa-archive'></span><span>Conservé</span></div>").html_safe : ("<div class='td-block'><span class='sb sb-lg sb-preparing'></span><span>En préparation</span></div>").html_safe}, - :organisation => Proc.new {|w| w.organisation.name}, - :validity_period => Proc.new {|w| w.validity_period.nil? ? '-' : t('validity_range', debut: l(w.try(:validity_period).try(:begin), format: :short), end: l(w.try(:validity_period).try(:end), format: :short))}, - :lines => Proc.new {|w| w.lines.count}, - :created_at => Proc.new {|w| l(w.created_at, format: :short)}, - :updated_at => Proc.new {|w| l(w.updated_at, format: :short)}, - :published_at => ''}, - [:show, :edit, :archive, :unarchive, :delete], - [:delete], - 'table has-filter has-search' + .select_table + = table_builder_2 @wbench_refs, + [ \ + TableBuilderHelper::Column.new( \ + key: :name, \ + attribute: 'name' \ + ), \ + TableBuilderHelper::Column.new( \ + key: :status, \ + attribute: Proc.new {|w| w.archived? ? ("<div class='td-block'><span class='fa fa-archive'></span><span>Conservé</span></div>").html_safe : ("<div class='td-block'><span class='sb sb-lg sb-preparing'></span><span>En préparation</span></div>").html_safe} \ + ), \ + TableBuilderHelper::Column.new( \ + key: :organisation, \ + attribute: Proc.new {|w| w.organisation.name} \ + ), \ + TableBuilderHelper::Column.new( \ + key: :validity_period, \ + attribute: Proc.new {|w| w.validity_period.nil? ? '-' : t('validity_range', debut: l(w.try(:validity_period).try(:begin), format: :short), end: l(w.try(:validity_period).try(:end), format: :short))} \ + ), \ + TableBuilderHelper::Column.new( \ + key: :lines, \ + attribute: Proc.new {|w| w.lines.count} \ + ), \ + TableBuilderHelper::Column.new( \ + key: :created_at, \ + attribute: Proc.new {|w| l(w.created_at, format: :short)} \ + ), \ + TableBuilderHelper::Column.new( \ + key: :updated_at, \ + attribute: Proc.new {|w| l(w.updated_at, format: :short)} \ + ), \ + TableBuilderHelper::Column.new( \ + key: :published_at, \ + attribute: '' \ + ) \ + ], + selectable: true, + links: [:show, :edit], + cls: 'table has-filter has-search' + + = multiple_selection_toolbox([:delete]) = new_pagination @wbench_refs, 'pull-right' @@ -1,7 +1,8 @@ #!/usr/bin/env ruby begin - load File.expand_path("../spring", __FILE__) -rescue LoadError + load File.expand_path('../spring', __FILE__) +rescue LoadError => e + raise unless e.message.include?('spring') end APP_PATH = File.expand_path('../../config/application', __FILE__) require_relative '../config/boot' @@ -1,7 +1,8 @@ #!/usr/bin/env ruby begin - load File.expand_path("../spring", __FILE__) -rescue LoadError + load File.expand_path('../spring', __FILE__) +rescue LoadError => e + raise unless e.message.include?('spring') end require_relative '../config/boot' require 'rake' diff --git a/config/deploy.rb b/config/deploy.rb index 78def7de7..0445ec480 100644 --- a/config/deploy.rb +++ b/config/deploy.rb @@ -12,7 +12,7 @@ set :group_writable, true set :bundle_cmd, "/var/lib/gems/2.2.0/bin/bundle" set :rake, "#{bundle_cmd} exec /var/lib/gems/2.2.0/bin/rake" -set :keep_releases, 5 +set :keep_releases, -> { fetch(:kept_releases, 5) } after "deploy:restart", "deploy:cleanup" set :rails_env, -> { fetch(:stage) } diff --git a/config/deploy/dev.rb b/config/deploy/dev.rb index 400f1d787..5150311e9 100644 --- a/config/deploy/dev.rb +++ b/config/deploy/dev.rb @@ -1 +1,2 @@ server "stif-boiv-dev.af83.priv", :app, :web, :db, :primary => true +set :kept_releases, 2 diff --git a/config/locales/calendars.en.yml b/config/locales/calendars.en.yml index 26ca913b6..f6c5ba852 100644 --- a/config/locales/calendars.en.yml +++ b/config/locales/calendars.en.yml @@ -18,6 +18,7 @@ en: destroy_confirm: Are you sure you want destroy this calendar? errors: overlapped_periods: Another period is overlapped with this period + short_period: A period needs to last at least two days index: title: Calendars all: All @@ -57,3 +58,4 @@ en: dates: date_in_date_ranges: A date can not be in Dates and in Date ranges. date_in_dates: A date can appear only once in the list of dates. + illegal_date: The date %{date} does not exist. diff --git a/config/locales/calendars.fr.yml b/config/locales/calendars.fr.yml index fe16cbfed..1b403c3f2 100644 --- a/config/locales/calendars.fr.yml +++ b/config/locales/calendars.fr.yml @@ -31,6 +31,7 @@ fr: destroy_confirm: Etes vous sûr de supprimer cet calendrier ? errors: overlapped_periods: Une autre période chevauche cette période + short_period: Une période doit être d'un duréé de deux jours minimum index: title: Calendriers all: Tous @@ -70,3 +71,4 @@ fr: dates: date_in_date_ranges: Une même date ne peut pas être incluse à la fois dans la liste et dans les intervalles de dates. date_in_dates: Une même date ne peut pas être incluse plusieurs fois dans la liste. + illegal_date: La date %{date} n'existe pas. diff --git a/config/locales/journey_patterns.en.yml b/config/locales/journey_patterns.en.yml index d62d79e58..80adc2337 100644 --- a/config/locales/journey_patterns.en.yml +++ b/config/locales/journey_patterns.en.yml @@ -24,6 +24,12 @@ en: form: warning: "Be careful, selection is also applied to the %{count} vehicle journeys associated to this journey pattern" activerecord: + errors: + models: + journey_pattern: + attributes: + stop_points: + minimum: 'Must at least have two stop points' models: journey_pattern: zero: "journey pattern" diff --git a/config/locales/journey_patterns.fr.yml b/config/locales/journey_patterns.fr.yml index 39005e464..0dceb2f43 100644 --- a/config/locales/journey_patterns.fr.yml +++ b/config/locales/journey_patterns.fr.yml @@ -24,6 +24,12 @@ fr: form: warning: "Attention, la sélection s'applique aussi aux %{count} courses de la mission" activerecord: + errors: + models: + journey_pattern: + attributes: + stop_points: + minimum: 'Une mission doit avoir au minimum deux arrêts' models: journey_pattern: zero: "mission" diff --git a/config/locales/routes.en.yml b/config/locales/routes.en.yml index 718521c51..d8160772f 100644 --- a/config/locales/routes.en.yml +++ b/config/locales/routes.en.yml @@ -58,7 +58,7 @@ en: comment: "Comments" number: "Number" direction: "Direction" - wayback: "Wayback" + wayback: "Direction" stop_points: "Nb Stop points" opposite_route: "Reversed route" opposite_route_id: "Reversed route" diff --git a/config/locales/routes.fr.yml b/config/locales/routes.fr.yml index 67e538be4..0af2832a2 100644 --- a/config/locales/routes.fr.yml +++ b/config/locales/routes.fr.yml @@ -58,7 +58,7 @@ fr: number: "Indice" comment: "Commentaire" direction: "Direction" - wayback: "Sens" + wayback: "Direction" stop_points: "Nb arrêts" journey_patterns: "Nb missions" opposite_route: "Itinéraire associé" diff --git a/lib/html_element.rb b/lib/html_element.rb new file mode 100644 index 000000000..469fd7565 --- /dev/null +++ b/lib/html_element.rb @@ -0,0 +1,15 @@ +class HTMLElement + def initialize(tag_name, content = nil, options = nil) + @tag_name = tag_name + @content = content + @options = options + end + + def to_html(options = {}) + ApplicationController.helpers.content_tag( + @tag_name, + @content, + @options.merge(options) + ) + end +end diff --git a/lib/link.rb b/lib/link.rb new file mode 100644 index 000000000..7683a808f --- /dev/null +++ b/lib/link.rb @@ -0,0 +1,10 @@ +class Link + attr_reader :content, :href, :method, :data + + def initialize(content: nil, href:, method: nil, data: nil) + @content = content + @href = href + @method = method + @data = data + end +end diff --git a/spec/controllers/autocomplete_calendars_controller_spec.rb b/spec/controllers/autocomplete_calendars_controller_spec.rb index 3ff75fadf..812cd92f9 100644 --- a/spec/controllers/autocomplete_calendars_controller_spec.rb +++ b/spec/controllers/autocomplete_calendars_controller_spec.rb @@ -1,5 +1,3 @@ -require 'rails_helper' - RSpec.describe AutocompleteCalendarsController, type: :controller do end diff --git a/spec/factories/chouette_2_factories.rb b/spec/factories/chouette_2_factories.rb deleted file mode 100644 index e8eba13e6..000000000 --- a/spec/factories/chouette_2_factories.rb +++ /dev/null @@ -1,79 +0,0 @@ -FactoryGirl.define do - - factory :organisation do - sequence(:name) { |n| "Organisation #{n}" } - sequence(:code) { |n| "000#{n}" } - end - - factory :referential do - sequence(:name) { |n| "Test #{n}" } - sequence(:slug) { |n| "test_#{n}" } - sequence(:prefix) { |n| "test_#{n}" } - association :organisation - association :workbench - association :line_referential - association :stop_area_referential - time_zone "Europe/Paris" - end - - factory :rule_parameter_set do - sequence(:name) { |n| "Test #{n}" } - association :organisation - after(:create) do |rsp| - rsp.parameters = RuleParameterSet.default_for_all_modes( rsp.organisation).parameters - end - end - - factory :user do - association :organisation - sequence(:name) { |n| "chouette#{n}" } - sequence(:username) { |n| "chouette#{n}" } - sequence(:email) { |n| "chouette#{n}@dryade.priv" } - password "secret" - password_confirmation "secret" - end - - 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 - - factory :kml_export do - referential { Referential.find_by_slug("first") } - end - - factory :export do - referential { Referential.find_by_slug("first") } - end - - factory :export_log_message do - association :export - sequence(:key) { |n| "key_#{n}" } - end - - factory :vehicle_translation do - count 1 - duration 1 - end - - factory :compliance_check_result do - association :compliance_check_task - rule_code "2-NEPTUNE-StopArea-6" - severity "warning" - status "nok" - end - - factory :compliance_check_task do - user_id 1 - user_name "Dummy" - status "pending" - referential { Referential.find_by_slug("first") } - end - - factory :time_table_combination - -end diff --git a/spec/factories/clean_ups.rb b/spec/factories/clean_ups.rb index d3746c3b2..7107769ff 100644 --- a/spec/factories/clean_ups.rb +++ b/spec/factories/clean_ups.rb @@ -2,5 +2,6 @@ FactoryGirl.define do factory :clean_up do begin_date { Date.today} end_date { Date.today + 1.month } + date_type { :before } end end diff --git a/spec/factories/compliance_check_results.rb b/spec/factories/compliance_check_results.rb new file mode 100644 index 000000000..7a3a3e956 --- /dev/null +++ b/spec/factories/compliance_check_results.rb @@ -0,0 +1,8 @@ +FactoryGirl.define do + factory :compliance_check_result do + association :compliance_check_task + rule_code "2-NEPTUNE-StopArea-6" + severity "warning" + status "nok" + end +end diff --git a/spec/factories/compliance_check_tasks.rb b/spec/factories/compliance_check_tasks.rb new file mode 100644 index 000000000..e9fdeb5ef --- /dev/null +++ b/spec/factories/compliance_check_tasks.rb @@ -0,0 +1,8 @@ +FactoryGirl.define do + factory :compliance_check_task do + user_id 1 + user_name "Dummy" + status "pending" + referential { Referential.find_by_slug("first") } + end +end diff --git a/spec/factories/export_log_messages.rb b/spec/factories/export_log_messages.rb new file mode 100644 index 000000000..849efd7b1 --- /dev/null +++ b/spec/factories/export_log_messages.rb @@ -0,0 +1,6 @@ +FactoryGirl.define do + factory :export_log_message do + association :export + sequence(:key) { |n| "key_#{n}" } + end +end diff --git a/spec/factories/exports.rb b/spec/factories/exports.rb new file mode 100644 index 000000000..34427edb8 --- /dev/null +++ b/spec/factories/exports.rb @@ -0,0 +1,5 @@ +FactoryGirl.define do + factory :export do + referential { Referential.find_by_slug("first") } + end +end diff --git a/spec/factories/import_tasks.rb b/spec/factories/import_tasks.rb new file mode 100644 index 000000000..9ca6db899 --- /dev/null +++ b/spec/factories/import_tasks.rb @@ -0,0 +1,10 @@ +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/kml_exports.rb b/spec/factories/kml_exports.rb new file mode 100644 index 000000000..feb86c5b6 --- /dev/null +++ b/spec/factories/kml_exports.rb @@ -0,0 +1,5 @@ +FactoryGirl.define do + factory :kml_export do + referential { Referential.find_by_slug("first") } + end +end diff --git a/spec/factories/organisations.rb b/spec/factories/organisations.rb new file mode 100644 index 000000000..239557a0e --- /dev/null +++ b/spec/factories/organisations.rb @@ -0,0 +1,6 @@ +FactoryGirl.define do + factory :organisation do + sequence(:name) { |n| "Organisation #{n}" } + sequence(:code) { |n| "000#{n}" } + end +end diff --git a/spec/factories/referentials.rb b/spec/factories/referentials.rb new file mode 100644 index 000000000..dd5bf1c2e --- /dev/null +++ b/spec/factories/referentials.rb @@ -0,0 +1,12 @@ +FactoryGirl.define do + factory :referential do + sequence(:name) { |n| "Test #{n}" } + sequence(:slug) { |n| "test_#{n}" } + sequence(:prefix) { |n| "test_#{n}" } + association :organisation + association :workbench + association :line_referential + association :stop_area_referential + time_zone "Europe/Paris" + end +end diff --git a/spec/factories/rule_parameter_sets.rb b/spec/factories/rule_parameter_sets.rb new file mode 100644 index 000000000..e20fff8ce --- /dev/null +++ b/spec/factories/rule_parameter_sets.rb @@ -0,0 +1,9 @@ +FactoryGirl.define do + factory :rule_parameter_set do + sequence(:name) { |n| "Test #{n}" } + association :organisation + after(:create) do |rsp| + rsp.parameters = RuleParameterSet.default_for_all_modes( rsp.organisation).parameters + end + end +end diff --git a/spec/factories/time_table_combinations.rb b/spec/factories/time_table_combinations.rb new file mode 100644 index 000000000..364d6460e --- /dev/null +++ b/spec/factories/time_table_combinations.rb @@ -0,0 +1,3 @@ +FactoryGirl.define do + factory :time_table_combination +end diff --git a/spec/factories/users.rb b/spec/factories/users.rb new file mode 100644 index 000000000..8f0ec38c0 --- /dev/null +++ b/spec/factories/users.rb @@ -0,0 +1,10 @@ +FactoryGirl.define do + factory :user do + association :organisation + sequence(:name) { |n| "chouette#{n}" } + sequence(:username) { |n| "chouette#{n}" } + sequence(:email) { |n| "chouette#{n}@dryade.priv" } + password "secret" + password_confirmation "secret" + end +end diff --git a/spec/factories/vehicle_translations.rb b/spec/factories/vehicle_translations.rb new file mode 100644 index 000000000..1f0175222 --- /dev/null +++ b/spec/factories/vehicle_translations.rb @@ -0,0 +1,6 @@ +FactoryGirl.define do + factory :vehicle_translation do + count 1 + duration 1 + end +end diff --git a/spec/features/calendars_spec.rb b/spec/features/calendars_spec.rb deleted file mode 100644 index e15624295..000000000 --- a/spec/features/calendars_spec.rb +++ /dev/null @@ -1,72 +0,0 @@ -# -*- coding: utf-8 -*- -require 'spec_helper' - -describe 'Calendars', type: :feature do - login_user - - let!(:calendars) { Array.new(2) { create :calendar, organisation_id: 1 } } - let!(:shared_calendar_other_org) { create :calendar, shared: true } - let!(:unshared_calendar_other_org) { create :calendar } - - describe 'index' do - before(:each) { visit calendars_path } - - it 'displays calendars of the current organisation' do - expect(page).to have_content(calendars.first.short_name) - # expect(page).to have_content(shared_calendar_other_org.short_name) - # expect(page).not_to have_content(unshared_calendar_other_org.short_name) - end - - context 'filtering' do - it 'supports filtering by short name' do - fill_in 'q[name_or_short_name_cont]', with: calendars.first.short_name - click_button 'search_btn' - expect(page).to have_content(calendars.first.short_name) - expect(page).not_to have_content(calendars.last.short_name) - end - - it 'supports filtering by name' do - fill_in 'q[name_or_short_name_cont]', with: calendars.first.name - click_button 'search_btn' - expect(page).to have_content(calendars.first.name) - expect(page).not_to have_content(calendars.last.name) - end - - - it 'supports filtering by shared' do - shared_calendar = create :calendar, organisation_id: 1, shared: true - visit calendars_path - # select I18n.t('true'), from: 'q[shared]' - find(:css, '#q_shared_true').set(true) - click_button 'filter_btn' - expect(page).to have_content(shared_calendar.short_name) - expect(page).not_to have_content(calendars.first.short_name) - end - - # wip - # it 'supports filtering by date' do - # july_calendar = create :calendar, dates: [Date.new(2017, 7, 7)], date_ranges: [Date.new(2017, 7, 15)..Date.new(2017, 7, 30)], organisation_id: 1 - # visit calendars_path - # select '7', from: 'q_contains_date_3i' - # select 'juillet', from: 'q_contains_date_2i' - # select '2017', from: 'q_contains_date_1i' - # click_button 'filter_btn' - # expect(page).to have_content(july_calendar.short_name) - # expect(page).not_to have_content(calendars.first.short_name) - # select '18', from: 'q_contains_date_3i' - # select 'juillet', from: 'q_contains_date_2i' - # select '2017', from: 'q_contains_date_1i' - # click_button 'filter_btn' - # expect(page).to have_content(july_calendar.short_name) - # expect(page).not_to have_content(calendars.first.short_name) - # end - end - end - - describe 'show' do - it 'displays calendar' do - visit calendar_path(calendars.first) - expect(page).to have_content(calendars.first.name) - end - end -end diff --git a/spec/features/referentials_permissions_spec.rb b/spec/features/referentials_permissions_spec.rb index 0216eeeb0..c37dff5b9 100644 --- a/spec/features/referentials_permissions_spec.rb +++ b/spec/features/referentials_permissions_spec.rb @@ -31,7 +31,7 @@ describe "Referentials", :type => :feature do end it 'shows the delete button' do expected_href = referential_path(referential) - expect( page ).to have_css(%{a[href=#{expected_href.inspect}] span}, text: destroy_link_text) + expect( page ).to have_css(%{a[href=#{expected_href.inspect}]}, text: destroy_link_text) end end diff --git a/spec/helpers/table_builder_helper/column_spec.rb b/spec/helpers/table_builder_helper/column_spec.rb new file mode 100644 index 000000000..0f27703b2 --- /dev/null +++ b/spec/helpers/table_builder_helper/column_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe TableBuilderHelper::Column do + describe "#header_label" do + it "returns the column @name if present" do + expect( + TableBuilderHelper::Column.new( + name: 'ID Codif', + attribute: nil + ).header_label + ).to eq('ID Codif') + end + + it "returns the I18n translation of @key if @name not present" do + expect( + TableBuilderHelper::Column.new( + key: :phone, + attribute: 'phone' + ).header_label(Chouette::Company) + ).to eq('Numéro de téléphone') + end + end +end diff --git a/spec/helpers/table_builder_helper/custom_links_spec.rb b/spec/helpers/table_builder_helper/custom_links_spec.rb new file mode 100644 index 000000000..b64e97527 --- /dev/null +++ b/spec/helpers/table_builder_helper/custom_links_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe TableBuilderHelper::CustomLinks do + describe "#actions_after_policy_check" do + it "includes :show" do + referential = build_stubbed(:referential) + user_context = UserContext.new( + build_stubbed( + :user, + organisation: referential.organisation, + permissions: [ + 'boiv:read-offer' + ] + ), + referential: referential + ) + + expect( + TableBuilderHelper::CustomLinks.new( + referential, + user_context, + [:show] + ).actions_after_policy_check + ).to eq([:show]) + end + end +end diff --git a/spec/helpers/table_builder_helper_spec.rb b/spec/helpers/table_builder_helper_spec.rb new file mode 100644 index 000000000..8f4d98c88 --- /dev/null +++ b/spec/helpers/table_builder_helper_spec.rb @@ -0,0 +1,371 @@ +require 'htmlbeautifier' + +module TableBuilderHelper + include Pundit +end + +describe TableBuilderHelper, type: :helper do + describe "#table_builder_2" do + it "builds a table" do + referential = build_stubbed(:referential) + workbench = referential.workbench + + user_context = UserContext.new( + build_stubbed( + :user, + organisation: referential.organisation, + permissions: [ + 'referentials.create', + 'referentials.edit', + 'referentials.destroy' + ] + ), + referential: referential + ) + allow(helper).to receive(:current_user).and_return(user_context) + + referentials = [referential] + + allow(referentials).to receive(:model).and_return(Referential) + + allow(helper).to receive(:params).and_return({ + controller: 'workbenches', + action: 'show', + id: referentials[0].workbench.id + }) + + referentials = ModelDecorator.decorate( + referentials, + with: ReferentialDecorator + ) + + expected = <<-HTML +<table class="table has-filter has-search"> + <thead> + <tr> + <th> + <div class="checkbox"><input type="checkbox" name="0" id="0" value="all" /><label for="0"></label></div> + </th> + <th><a href="/workbenches/#{workbench.id}?direction=desc&sort=name">Nom<span class="orderers"><span class="fa fa-sort-asc active"></span><span class="fa fa-sort-desc "></span></span></a></th> + <th><a href="/workbenches/#{workbench.id}?direction=desc&sort=status">Etat<span class="orderers"><span class="fa fa-sort-asc active"></span><span class="fa fa-sort-desc "></span></span></a></th> + <th><a href="/workbenches/#{workbench.id}?direction=desc&sort=organisation">Organisation<span class="orderers"><span class="fa fa-sort-asc active"></span><span class="fa fa-sort-desc "></span></span></a></th> + <th><a href="/workbenches/#{workbench.id}?direction=desc&sort=validity_period">Période de validité englobante<span class="orderers"><span class="fa fa-sort-asc active"></span><span class="fa fa-sort-desc "></span></span></a></th> + <th><a href="/workbenches/#{workbench.id}?direction=desc&sort=lines">Lignes<span class="orderers"><span class="fa fa-sort-asc active"></span><span class="fa fa-sort-desc "></span></span></a></th> + <th><a href="/workbenches/#{workbench.id}?direction=desc&sort=created_at">Créé le<span class="orderers"><span class="fa fa-sort-asc active"></span><span class="fa fa-sort-desc "></span></span></a></th> + <th><a href="/workbenches/#{workbench.id}?direction=desc&sort=updated_at">Edité le<span class="orderers"><span class="fa fa-sort-asc active"></span><span class="fa fa-sort-desc "></span></span></a></th> + <th><a href="/workbenches/#{workbench.id}?direction=desc&sort=published_at">Intégré le<span class="orderers"><span class="fa fa-sort-asc active"></span><span class="fa fa-sort-desc "></span></span></a></th> + <th></th> + </tr> + </thead> + <tbody> + <tr> + <td> + <div class="checkbox"><input type="checkbox" name="#{referential.id}" id="#{referential.id}" value="#{referential.id}" /><label for="#{referential.id}"></label></div> + </td> + <td title="Voir"><a href="/referentials/#{referential.id}">#{referential.name}</a></td> + <td> + <div class='td-block'><span class='sb sb-lg sb-preparing'></span><span>En préparation</span></div> + </td> + <td>#{referential.organisation.name}</td> + <td>-</td> + <td>#{referential.lines.count}</td> + <td>#{I18n.localize(referential.created_at, format: :short)}</td> + <td>#{I18n.localize(referential.updated_at, format: :short)}</td> + <td></td> + <td class="actions"> + <div class="btn-group"> + <div class="btn dropdown-toggle" data-toggle="dropdown"><span class="fa fa-cog"></span></div> + <ul class="dropdown-menu"> + <li><a href="/referentials/#{referential.id}">Consulter</a></li> + <li><a href="/referentials/#{referential.id}/edit">Editer</a></li> + <li><a href="/referentials/#{referential.id}/time_tables">Calendriers</a></li> + <li><a href="/referentials/new?from=#{referential.id}">Dupliquer</a></li> + <li><a rel="nofollow" data-method="put" href="/referentials/#{referential.id}/archive">Conserver</a></li> + <li class="delete-action"><a data-confirm="Etes vous sûr de vouloir supprimer ce jeu de données ?" rel="nofollow" data-method="delete" href="/referentials/#{referential.id}"><span class="fa fa-trash"></span>Supprimer</a></li> + </ul> + </div> + </td> + </tr> + </tbody> +</table> + HTML + + html_str = helper.table_builder_2( + referentials, + [ + TableBuilderHelper::Column.new( + key: :name, + attribute: 'name' + ), + TableBuilderHelper::Column.new( + key: :status, + attribute: Proc.new do |w| + if w.archived? + ("<div class='td-block'><span class='fa fa-archive'></span><span>Conservé</span></div>").html_safe + else + ("<div class='td-block'><span class='sb sb-lg sb-preparing'></span><span>En préparation</span></div>").html_safe + end + end + ), + TableBuilderHelper::Column.new( + key: :organisation, + attribute: Proc.new {|w| w.organisation.name} + ), + TableBuilderHelper::Column.new( + key: :validity_period, + attribute: Proc.new do |w| + if w.validity_period.nil? + '-' + else + t( + 'validity_range', + debut: l(w.try(:validity_period).try(:begin), format: :short), + end: l(w.try(:validity_period).try(:end), format: :short) + ) + end + end + ), + TableBuilderHelper::Column.new( + key: :lines, + attribute: Proc.new {|w| w.lines.count} + ), + TableBuilderHelper::Column.new( + key: :created_at, + attribute: Proc.new {|w| l(w.created_at, format: :short)} + ), + TableBuilderHelper::Column.new( + key: :updated_at, + attribute: Proc.new {|w| l(w.updated_at, format: :short)} + ), + TableBuilderHelper::Column.new( + key: :published_at, + attribute: '' + ) + ], + selectable: true, + links: [:show, :edit], + cls: 'table has-filter has-search' + ) + + beautified_html = HtmlBeautifier.beautify(html_str, indent: ' ') + + expect(beautified_html).to eq(expected.chomp) + end + + it "can set a column as non-sortable" do + company = build_stubbed(:company) + line_referential = build_stubbed( + :line_referential, + companies: [company] + ) + referential = build_stubbed( + :referential, + line_referential: line_referential + ) + + user_context = UserContext.new( + build_stubbed( + :user, + organisation: referential.organisation, + permissions: [ + 'referentials.create', + 'referentials.edit', + 'referentials.destroy' + ] + ), + referential: referential + ) + allow(helper).to receive(:current_user).and_return(user_context) + allow(TableBuilderHelper::URL).to receive(:current_referential) + .and_return(referential) + + companies = [company] + + allow(companies).to receive(:model).and_return(Chouette::Company) + + allow(helper).to receive(:params).and_return({ + controller: 'referential_companies', + action: 'index', + referential_id: referential.id + }) + + companies = ModelDecorator.decorate( + companies, + with: CompanyDecorator + ) + + expected = <<-HTML +<table class="table has-search"> + <thead> + <tr> + <th>ID Codif</th> + <th><a href="/referentials/#{referential.id}/companies?direction=desc&sort=name">Nom<span class="orderers"><span class="fa fa-sort-asc active"></span><span class="fa fa-sort-desc "></span></span></a></th> + <th><a href="/referentials/#{referential.id}/companies?direction=desc&sort=phone">Numéro de téléphone<span class="orderers"><span class="fa fa-sort-asc active"></span><span class="fa fa-sort-desc "></span></span></a></th> + <th><a href="/referentials/#{referential.id}/companies?direction=desc&sort=email">Email<span class="orderers"><span class="fa fa-sort-asc active"></span><span class="fa fa-sort-desc "></span></span></a></th> + <th><a href="/referentials/#{referential.id}/companies?direction=desc&sort=url">Page web associée<span class="orderers"><span class="fa fa-sort-asc active"></span><span class="fa fa-sort-desc "></span></span></a></th> + <th></th> + </tr> + </thead> + <tbody> + <tr> + <td>#{company.objectid.local_id}</td> + <td title="Voir"><a href="/referentials/#{referential.id}/companies/#{company.id}">#{company.name}</a></td> + <td></td> + <td></td> + <td></td> + <td class="actions"> + <div class="btn-group"> + <div class="btn dropdown-toggle" data-toggle="dropdown"><span class="fa fa-cog"></span></div> + <ul class="dropdown-menu"> + <li><a href="/referentials/#{referential.id}/companies/#{company.id}">Consulter</a></li> + </ul> + </div> + </td> + </tr> + </tbody> +</table> + HTML + + html_str = helper.table_builder_2( + companies, + [ + TableBuilderHelper::Column.new( + name: 'ID Codif', + attribute: Proc.new { |n| n.try(:objectid).try(:local_id) }, + sortable: false + ), + TableBuilderHelper::Column.new( + key: :name, + attribute: 'name' + ), + TableBuilderHelper::Column.new( + key: :phone, + attribute: 'phone' + ), + TableBuilderHelper::Column.new( + key: :email, + attribute: 'email' + ), + TableBuilderHelper::Column.new( + key: :url, + attribute: 'url' + ), + ], + links: [:show, :edit, :delete], + cls: 'table has-search' + ) + + beautified_html = HtmlBeautifier.beautify(html_str, indent: ' ') + + expect(beautified_html).to eq(expected.chomp) + end + + it "can set all columns as non-sortable" do + company = build_stubbed(:company) + line_referential = build_stubbed( + :line_referential, + companies: [company] + ) + referential = build_stubbed( + :referential, + line_referential: line_referential + ) + + user_context = UserContext.new( + build_stubbed( + :user, + organisation: referential.organisation, + permissions: [ + 'referentials.create', + 'referentials.edit', + 'referentials.destroy' + ] + ), + referential: referential + ) + allow(helper).to receive(:current_user).and_return(user_context) + allow(TableBuilderHelper::URL).to receive(:current_referential) + .and_return(referential) + + companies = [company] + + allow(companies).to receive(:model).and_return(Chouette::Company) + + allow(helper).to receive(:params).and_return({ + controller: 'referential_companies', + action: 'index', + referential_id: referential.id + }) + + companies = ModelDecorator.decorate( + companies, + with: CompanyDecorator + ) + + expected = <<-HTML +<table class="table has-search"> + <thead> + <tr> + <th>ID Codif</th> + <th>Nom</th> + <th>Numéro de téléphone</th> + <th>Email</th> + <th>Page web associée</th> + <th></th> + </tr> + </thead> + <tbody> + <tr> + <td>#{company.objectid.local_id}</td> + <td title="Voir"><a href="/referentials/#{referential.id}/companies/#{company.id}">#{company.name}</a></td> + <td></td> + <td></td> + <td></td> + <td class="actions"> + <div class="btn-group"> + <div class="btn dropdown-toggle" data-toggle="dropdown"><span class="fa fa-cog"></span></div> + <ul class="dropdown-menu"> + <li><a href="/referentials/#{referential.id}/companies/#{company.id}">Consulter</a></li> + </ul> + </div> + </td> + </tr> + </tbody> +</table> + HTML + + html_str = helper.table_builder_2( + companies, + [ + TableBuilderHelper::Column.new( + name: 'ID Codif', + attribute: Proc.new { |n| n.try(:objectid).try(:local_id) } + ), + TableBuilderHelper::Column.new( + key: :name, + attribute: 'name' + ), + TableBuilderHelper::Column.new( + key: :phone, + attribute: 'phone' + ), + TableBuilderHelper::Column.new( + key: :email, + attribute: 'email' + ), + TableBuilderHelper::Column.new( + key: :url, + attribute: 'url' + ), + ], + sortable: false, + links: [:show, :edit, :delete], + cls: 'table has-search' + ) + + beautified_html = HtmlBeautifier.beautify(html_str, indent: ' ') + + expect(beautified_html).to eq(expected.chomp) + end + end +end diff --git a/spec/javascripts/spec_helper.coffee b/spec/javascripts/spec_helper.coffee new file mode 100644 index 000000000..9ff516885 --- /dev/null +++ b/spec/javascripts/spec_helper.coffee @@ -0,0 +1,32 @@ +# Teaspoon includes some support files, but you can use anything from your own support path too. +# require support/jasmine-jquery-1.7.0 +# require support/jasmine-jquery-2.0.0 +# require support/jasmine-jquery-2.1.0 +# require support/sinon +# require support/your-support-file +# +# PhantomJS (Teaspoons default driver) doesn't have support for Function.prototype.bind, which has caused confusion. +# Use this polyfill to avoid the confusion. +#= require support/phantomjs-shims +# +# You can require your own javascript files here. By default this will include everything in application, however you +# may get better load performance if you require the specific files that are being used in the spec that tests them. +#= require application +# +# Deferring execution +# If you're using CommonJS, RequireJS or some other asynchronous library you can defer execution. Call +# Teaspoon.execute() after everything has been loaded. Simple example of a timeout: +# +# Teaspoon.defer = true +# setTimeout(Teaspoon.execute, 1000) +# +# Matching files +# By default Teaspoon will look for files that match _spec.{js,js.coffee,.coffee}. Add a filename_spec.js file in your +# spec path and it'll be included in the default suite automatically. If you want to customize suites, check out the +# configuration in teaspoon_env.rb +# +# Manifest +# If you'd rather require your spec files manually (to control order for instance) you can disable the suite matcher in +# the configuration and use this file as a manifest. +# +# For more information: http://github.com/modeset/teaspoon diff --git a/spec/javascripts/vehicle_journeys/actions_spec.js b/spec/javascripts/vehicle_journeys/actions_spec.js index 19f65046f..d96baf8ef 100644 --- a/spec/javascripts/vehicle_journeys/actions_spec.js +++ b/spec/javascripts/vehicle_journeys/actions_spec.js @@ -188,11 +188,13 @@ 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 departureDelta = 0 const expectedAction = { type: 'DUPLICATE_VEHICLEJOURNEY', - data + data, + departureDelta } - expect(actions.duplicateVehicleJourney(data)).toEqual(expectedAction) + expect(actions.duplicateVehicleJourney(data, departureDelta)).toEqual(expectedAction) }) }) describe('when clicking on edit notes modal', () => { diff --git a/spec/javascripts/vehicle_journeys/reducers/filters_spec.js b/spec/javascripts/vehicle_journeys/reducers/filters_spec.js index 2ab88386a..0a6729c8b 100644 --- a/spec/javascripts/vehicle_journeys/reducers/filters_spec.js +++ b/spec/javascripts/vehicle_journeys/reducers/filters_spec.js @@ -31,7 +31,7 @@ describe('filters reducer', () => { vehicleJourney: {}, timetable: {}, withoutSchedule: true, - withoutTimeTable: false + withoutTimeTable: true }, queryString: '' } @@ -153,7 +153,7 @@ describe('filters reducer', () => { "&q%5Bvehicle_journey_at_stops_departure_time_gteq%5D=11%3A11", "&q%5Bvehicle_journey_at_stops_departure_time_lteq%5D=22%3A22", "&q%5Bvehicle_journey_without_departure_time%5D=true", - "&q%5Bvehicle_journey_without_time_table%5D=false" + "&q%5Bvehicle_journey_without_time_table%5D=true" ].join('') expect( diff --git a/spec/javascripts/vehicle_journeys/reducers/vehicle_journeys_spec.js b/spec/javascripts/vehicle_journeys/reducers/vehicle_journeys_spec.js index 23ebc3d9f..620e2ffdd 100644 --- a/spec/javascripts/vehicle_journeys/reducers/vehicle_journeys_spec.js +++ b/spec/javascripts/vehicle_journeys/reducers/vehicle_journeys_spec.js @@ -216,14 +216,15 @@ describe('vehicleJourneys reducer', () => { delta: 627, arrival_time : { hour: '12', - minute: '00' + minute: '01' }, departure_time : { hour: '22', - minute: '27' + minute: '28' }, stop_area_object_id : "FR:92024:ZDE:420553:STIF" }] + let departureDelta = 1 let fakeData = { duplicate_number: {value : 1}, additional_time: {value: '5'} @@ -234,7 +235,8 @@ describe('vehicleJourneys reducer', () => { expect( vjReducer(state, { type: 'DUPLICATE_VEHICLEJOURNEY', - data: fakeData + data: fakeData, + departureDelta }) ).toEqual([state[0], newVJ, state[1]]) }) diff --git a/spec/models/calendar/date_value_spec.rb b/spec/models/calendar/date_value_spec.rb new file mode 100644 index 000000000..70dca3cc6 --- /dev/null +++ b/spec/models/calendar/date_value_spec.rb @@ -0,0 +1,35 @@ +RSpec.describe Calendar::DateValue, type: :model do + describe 'DateValue' do + subject { date_value } + + def date_value(attributes = {}) + @__date_values__ ||= Hash.new + @__date_values__.fetch(attributes) do + @__date_values__[attributes] = Calendar::DateValue.new(attributes) + end + end + + it 'should support mark_for_destruction (required by cocoon)' do + date_value.mark_for_destruction + expect(date_value).to be_marked_for_destruction + end + + it 'should support _destroy attribute (required by coocon)' do + date_value._destroy = true + expect(date_value).to be_marked_for_destruction + end + + it 'should support new_record? (required by cocoon)' do + expect(Calendar::DateValue.new).to be_new_record + expect(date_value(id: 42)).not_to be_new_record + end + + it 'should cast value as date attribute' do + expect(date_value(value: '2017-01-03').value).to eq(Date.new(2017,01,03)) + end + + it 'validates presence' do + is_expected.to validate_presence_of(:value) + end + end +end diff --git a/spec/models/calendar/period_spec.rb b/spec/models/calendar/period_spec.rb new file mode 100644 index 000000000..233733cbf --- /dev/null +++ b/spec/models/calendar/period_spec.rb @@ -0,0 +1,68 @@ +RSpec.describe Calendar::Period, type: :model do + + + subject { period } + + def period(attributes = {}) + @__period__ ||= {} + @__period__.fetch(attributes){ + @__period__[attributes] = Calendar::Period.new(attributes) + } + end + + it 'should support mark_for_destruction (required by cocoon)' do + period.mark_for_destruction + expect(period).to be_marked_for_destruction + end + + it 'should support _destroy attribute (required by coocon)' do + period._destroy = true + expect(period).to be_marked_for_destruction + end + + it 'should support new_record? (required by cocoon)' do + expect(Calendar::Period.new).to be_new_record + expect(period(id: 42)).not_to be_new_record + end + + it 'should cast begin as date attribute' do + expect(period(begin: '2016-11-22').begin).to eq(Date.new(2016,11,22)) + end + + it 'should cast end as date attribute' do + expect(period(end: '2016-11-22').end).to eq(Date.new(2016,11,22)) + end + + it { is_expected.to validate_presence_of(:begin) } + it { is_expected.to validate_presence_of(:end) } + + it 'should validate that end is greather than or equlals to begin' do + expect(period(begin: '2016-11-21', end: '2016-11-22')).to be_valid + expect(period(begin: '2016-11-21', end: '2016-11-21')).to_not be_valid + expect(period(begin: '2016-11-22', end: '2016-11-21')).to_not be_valid + end + + describe 'intersect?' do + it 'should detect date in common with other date_ranges' do + november = period(begin: '2016-11-01', end: '2016-11-30') + mid_november_mid_december = period(begin: '2016-11-15', end: '2016-12-15') + expect(november.intersect?(mid_november_mid_december)).to be(true) + end + + it 'should not intersect when no date is in common' do + november = period(begin: '2016-11-01', end: '2016-11-30') + december = period(begin: '2016-12-01', end: '2016-12-31') + + expect(november.intersect?(december)).to be(false) + + january = period(begin: '2017-01-01', end: '2017-01-31') + expect(november.intersect?(december, january)).to be(false) + end + + it 'should not intersect itself' do + period = period(id: 42, begin: '2016-11-01', end: '2016-11-30') + expect(period.intersect?(period)).to be(false) + end + + end +end diff --git a/spec/models/calendar_spec.rb b/spec/models/calendar_spec.rb index 33d9676cd..cf7e4aa27 100644 --- a/spec/models/calendar_spec.rb +++ b/spec/models/calendar_spec.rb @@ -1,5 +1,3 @@ -require 'rails_helper' - RSpec.describe Calendar, :type => :model do it { should belong_to(:organisation) } @@ -23,134 +21,25 @@ RSpec.describe Calendar, :type => :model do describe 'validations' do it 'validates that dates and date_ranges do not overlap' do - calendar = build(:calendar, dates: [Date.today], date_ranges: [Date.today..Date.tomorrow]) - expect { - calendar.save! - }.to raise_error(ActiveRecord::RecordInvalid) + expect(build(:calendar, dates: [Date.today], date_ranges: [Date.today..Date.tomorrow])).to_not be_valid end it 'validates that there are no duplicates in dates' do - calendar = build(:calendar, dates: [Date.yesterday, Date.yesterday], date_ranges: [Date.today..Date.tomorrow]) - expect { - calendar.save! - }.to raise_error(ActiveRecord::RecordInvalid) + expect(build(:calendar, dates: [Date.yesterday, Date.yesterday], date_ranges: [Date.today..Date.tomorrow])).to_not be_valid end end - describe 'Period' do - - subject { period } - - def period(attributes = {}) - return @period if attributes.empty? and @period - Calendar::Period.new(attributes).tap do |period| - @period = period if attributes.empty? - end - end - - it 'should support mark_for_destruction (required by cocoon)' do - period.mark_for_destruction - expect(period).to be_marked_for_destruction - end - - it 'should support _destroy attribute (required by coocon)' do - period._destroy = true - expect(period).to be_marked_for_destruction - end - - it 'should support new_record? (required by cocoon)' do - expect(Calendar::Period.new).to be_new_record - expect(period(id: 42)).not_to be_new_record - end - - it 'should cast begin as date attribute' do - expect(period(begin: '2016-11-22').begin).to eq(Date.new(2016,11,22)) - end - - it 'should cast end as date attribute' do - expect(period(end: '2016-11-22').end).to eq(Date.new(2016,11,22)) - end - - it { is_expected.to validate_presence_of(:begin) } - it { is_expected.to validate_presence_of(:end) } - - it 'should validate that end is greather than or equlals to begin' do - expect(period(begin: '2016-11-21', end: '2016-11-22')).to be_valid - expect(period(begin: '2016-11-21', end: '2016-11-21')).to be_valid - expect(period(begin: '2016-11-22', end: '2016-11-21')).to_not be_valid - end - - describe 'intersect?' do - it 'should detect date in common with other date_ranges' do - november = period(begin: '2016-11-01', end: '2016-11-30') - mid_november_mid_december = period(begin: '2016-11-15', end: '2016-12-15') - expect(november.intersect?(mid_november_mid_december)).to be(true) - end - - it 'should not intersect when no date is in common' do - november = period(begin: '2016-11-01', end: '2016-11-30') - december = period(begin: '2016-12-01', end: '2016-12-31') - - expect(november.intersect?(december)).to be(false) - - january = period(begin: '2017-01-01', end: '2017-01-31') - expect(november.intersect?(december, january)).to be(false) - end - - it 'should not intersect itself' do - period = period(id: 42, begin: '2016-11-01', end: '2016-11-30') - expect(period.intersect?(period)).to be(false) - end - - end - end describe 'before_validation' do let(:calendar) { create(:calendar, date_ranges: []) } it 'shoud fill date_ranges with date ranges' do - expected_ranges = [ - Range.new(Date.today, Date.tomorrow) - ] - expected_ranges.each_with_index do |range, index| - calendar.date_ranges << Calendar::Period.from_range(index, range) - end + expected_range = Date.today..Date.tomorrow + calendar.date_ranges << expected_range calendar.valid? - expect(calendar.date_ranges.map { |period| period.begin..period.end }).to eq(expected_ranges) + expect(calendar.date_ranges.map { |period| period.begin..period.end }).to eq([expected_range]) end end - describe 'DateValue' do - subject { date_value } - - def date_value(attributes = {}) - return @date_value if attributes.empty? and @date_value - Calendar::DateValue.new(attributes).tap do |date_value| - @date_value = date_value if attributes.empty? - end - end - - it 'should support mark_for_destruction (required by cocoon)' do - date_value.mark_for_destruction - expect(date_value).to be_marked_for_destruction - end - - it 'should support _destroy attribute (required by coocon)' do - date_value._destroy = true - expect(date_value).to be_marked_for_destruction - end - - it 'should support new_record? (required by cocoon)' do - expect(Calendar::DateValue.new).to be_new_record - expect(date_value(id: 42)).not_to be_new_record - end - - it 'should cast value as date attribute' do - expect(date_value(value: '2017-01-03').value).to eq(Date.new(2017,01,03)) - end - - it { is_expected.to validate_presence_of(:value) } - end end - diff --git a/spec/models/chouette/journey_pattern_spec.rb b/spec/models/chouette/journey_pattern_spec.rb index 19b5060d2..aaf9a694f 100644 --- a/spec/models/chouette/journey_pattern_spec.rb +++ b/spec/models/chouette/journey_pattern_spec.rb @@ -2,6 +2,26 @@ require 'spec_helper' describe Chouette::JourneyPattern, :type => :model do + context 'validate minimum stop_points size' do + let(:journey_pattern) { create :journey_pattern } + let(:stop_points) { journey_pattern.stop_points } + + it 'should be valid if it has at least two sp' do + journey_pattern.stop_points.first(stop_points.size - 2).each do |sp| + journey_pattern.stop_points.delete(sp) + end + expect(journey_pattern).to be_valid + end + + it 'should not be valid if it has less then two sp' do + journey_pattern.stop_points.first(stop_points.size - 1).each do |sp| + journey_pattern.stop_points.delete(sp) + end + expect(journey_pattern).to_not be_valid + expect(journey_pattern.errors).to have_key(:stop_points) + end + end + describe "state_update" do def journey_pattern_to_state jp jp.attributes.slice('name', 'published_name', 'registration_number').tap do |item| @@ -24,12 +44,14 @@ describe Chouette::JourneyPattern, :type => :model do end it 'should attach checked stop_points' do - state['stop_points'].each{|sp| sp['checked'] = true} # Make sure journey_pattern has no stop_points - journey_pattern.stop_points.delete_all + state['stop_points'].each{|sp| sp['checked'] = false} + journey_pattern.state_stop_points_update(state) expect(journey_pattern.reload.stop_points).to be_empty + state['stop_points'].each{|sp| sp['checked'] = true} journey_pattern.state_stop_points_update(state) + expect(journey_pattern.reload.stop_points.count).to eq(5) end @@ -89,6 +111,63 @@ describe Chouette::JourneyPattern, :type => :model do expect(collection.first).to_not have_key('object_id') end + + it 'should create journey_pattern' do + new_state = journey_pattern_to_state(build(:journey_pattern, objectid: nil, route: route)) + Chouette::JourneyPattern.state_create_instance route, new_state + expect(new_state['object_id']).to be_truthy + expect(new_state['new_record']).to be_truthy + end + + it 'should delete journey_pattern' do + state['deletable'] = true + collection = [state] + expect { + Chouette::JourneyPattern.state_update route, collection + }.to change{Chouette::JourneyPattern.count}.from(1).to(0) + + expect(collection).to be_empty + end + + it 'should delete multiple journey_pattern' do + collection = 5.times.collect{journey_pattern_to_state(create(:journey_pattern, route: route))} + collection.map{|i| i['deletable'] = true} + + expect { + Chouette::JourneyPattern.state_update route, collection + }.to change{Chouette::JourneyPattern.count}.from(5).to(0) + end + + it 'should validate journey_pattern on update' do + journey_pattern.name = '' + collection = [state] + Chouette::JourneyPattern.state_update route, collection + expect(collection.first['errors']).to have_key(:name) + end + + it 'should validate journey_pattern on create' do + new_state = journey_pattern_to_state(build(:journey_pattern, name: '', objectid: nil, route: route)) + collection = [new_state] + expect { + Chouette::JourneyPattern.state_update route, collection + }.to_not change{Chouette::JourneyPattern.count} + + expect(collection.first['errors']).to have_key(:name) + expect(collection.first).to_not have_key('object_id') + end + + it 'should not save any journey_pattern of collection if one is invalid' do + journey_pattern.name = '' + valid_state = journey_pattern_to_state(build(:journey_pattern, objectid: nil, route: route)) + invalid_state = journey_pattern_to_state(journey_pattern) + collection = [valid_state, invalid_state] + + expect { + Chouette::JourneyPattern.state_update route, collection + }.to_not change{Chouette::JourneyPattern.count} + + expect(collection.first).to_not have_key('object_id') + end end describe "#stop_point_ids" do diff --git a/spec/models/chouette/time_table_spec.rb b/spec/models/chouette/time_table_spec.rb index 7a8863cb3..76c5def5c 100644 --- a/spec/models/chouette/time_table_spec.rb +++ b/spec/models/chouette/time_table_spec.rb @@ -2,18 +2,74 @@ require 'spec_helper' describe Chouette::TimeTable, :type => :model do subject { create(:time_table) } + let(:subject_periods_to_range) { subject.periods.map{|p| p.period_start..p.period_end } } it { is_expected.to validate_presence_of :comment } it { is_expected.to validate_uniqueness_of :objectid } - context "merge with calendar" do - let(:calendar) { create(:calendar) } + describe "#merge! with time_table" do + let(:another_tt) { create(:time_table) } + let(:another_tt_periods_to_range) { another_tt.periods.map{|p| p.period_start..p.period_end } } + + # Make sur we don't have overlapping periods or dates + before do + another_tt.periods.each do |p| + p.period_start = p.period_start + 1.year + p.period_end = p.period_end + 1.year + end + another_tt.dates.each{| d| d.date = d.date + 1.year } + end + + it 'should merge dates' do + subject.dates.clear + subject.merge!(another_tt) + expect(subject.dates.map(&:date)).to include(*another_tt.dates.map(&:date)) + end + + it 'should merge periods' do + subject.periods.clear + subject.merge!(another_tt) + + expect(subject_periods_to_range).to include(*another_tt_periods_to_range) + end + + it 'should not modify int_day_types' do + int_day_types = subject.int_day_types + subject.merge!(another_tt) + expect(subject.int_day_types).to eq int_day_types + end + + it 'should merge date in_out false' do + another_tt.dates.last.in_out = false + another_tt.save + + subject.merge!(another_tt) + expect(subject.dates.map(&:date)).to include(another_tt.dates.last.date) + end + end + + context "#merge! with calendar" do + let(:calendar) { create(:calendar, date_ranges: [Date.today + 1.year..Date.tomorrow + 1.year]) } - it 'should add calendar dates to time_table' do + it 'should merge calendar dates' do subject.dates.clear subject.merge!(calendar.convert_to_time_table) expect(subject.dates.map(&:date)).to include(*calendar.dates) end + + it 'should merge calendar periods with no periods in source' do + subject.periods.clear + another_tt = calendar.convert_to_time_table + subject.merge!(another_tt) + expect(subject_periods_to_range).to include(*calendar.date_ranges) + end + + it 'should add calendar periods with existing periods in source' do + another_tt = calendar.convert_to_time_table + subject.merge!(another_tt) + + expect(subject_periods_to_range).to include(*calendar.date_ranges) + end end describe "actualize" do @@ -981,47 +1037,6 @@ end end end - - describe "#merge!" do - context "timetables have periods with common day_types " do - before do - subject.periods.clear - subject.dates.clear - subject.periods << Chouette::TimeTablePeriod.new(:period_start => Date.new(2014,8,1), :period_end => Date.new(2014,8,5)) - subject.periods << Chouette::TimeTablePeriod.new(:period_start => Date.new(2014,6,30), :period_end => Date.new(2014,7,6)) - subject.dates << Chouette::TimeTableDate.new( :date => Date.new(2014,7,16), :in_out => true) - subject.int_day_types = 4|16|32|128 - another_tt = create(:time_table , :int_day_types => (4|16|64|128) ) - another_tt.periods.clear - another_tt.dates.clear - another_tt.periods << Chouette::TimeTablePeriod.new(:period_start => Date.new(2014,8,5), :period_end => Date.new(2014,8,12)) - another_tt.periods << Chouette::TimeTablePeriod.new(:period_start => Date.new(2014,7,15), :period_end => Date.new(2014,7,25)) - subject.merge! another_tt - subject.reload - end - it "should have merged periods" do - expect(subject.periods.size).to eq(3) - expect(subject.periods[0].period_start).to eq(Date.new(2014, 6, 30)) - expect(subject.periods[0].period_end).to eq(Date.new(2014, 7, 6)) - expect(subject.periods[1].period_start).to eq(Date.new(2014, 7, 15)) - expect(subject.periods[1].period_end).to eq(Date.new(2014, 7, 25)) - expect(subject.periods[2].period_start).to eq(Date.new(2014, 8, 1)) - expect(subject.periods[2].period_end).to eq(Date.new(2014, 8, 12)) - end - it "should not modify day_types" do - expect(subject.int_day_types).to eq(4|16|128) - end - it "should have dates for thursdays and fridays" do - expect(subject.dates.size).to eq(4) - expect(subject.dates[0].date).to eq(Date.new(2014,7,3)) - expect(subject.dates[1].date).to eq(Date.new(2014,7,18)) - expect(subject.dates[2].date).to eq(Date.new(2014,7,25)) - expect(subject.dates[3].date).to eq(Date.new(2014,8,8)) - end - end - - end - describe "#intersect!" do context "timetables have periods with common day_types " do before do diff --git a/spec/models/chouette/vehicle_journey_spec.rb b/spec/models/chouette/vehicle_journey_spec.rb index 8f9080b99..c78ef5b33 100644 --- a/spec/models/chouette/vehicle_journey_spec.rb +++ b/spec/models/chouette/vehicle_journey_spec.rb @@ -351,7 +351,7 @@ describe Chouette::VehicleJourney, :type => :model do end end - describe ".departure_time_between" do + describe ".where_departure_time_between" do it "selects vehicle journeys whose departure times are between the specified range" do journey_early = create( @@ -404,6 +404,35 @@ describe Chouette::VehicleJourney, :type => :model do .to_a ).to eq([journey]) end + + it "uses an inclusive range" do + journey_early = create( + :vehicle_journey, + stop_departure_time: '03:00:00' + ) + + route = journey_early.route + journey_pattern = journey_early.journey_pattern + + journey_late = create( + :vehicle_journey, + route: route, + journey_pattern: journey_pattern, + stop_departure_time: '04:00:00' + ) + + expect(route + .vehicle_journeys + .select('DISTINCT "vehicle_journeys".*') + .joins(' + LEFT JOIN "vehicle_journey_at_stops" + ON "vehicle_journey_at_stops"."vehicle_journey_id" = + "vehicle_journeys"."id" + ') + .where_departure_time_between('03:00', '04:00', allow_empty: true) + .to_a + ).to match_array([journey_early, journey_late]) + end end describe ".without_time_tables" do diff --git a/spec/models/clean_up_spec.rb b/spec/models/clean_up_spec.rb index 4b1bf4da9..ee88ca773 100644 --- a/spec/models/clean_up_spec.rb +++ b/spec/models/clean_up_spec.rb @@ -191,7 +191,7 @@ RSpec.describe CleanUp, :type => :model do context '#destroy_time_tables' do let!(:time_table) { create(:time_table) } - let(:cleaner) { create(:clean_up, date_type: :before) } + let(:cleaner) { create(:clean_up) } it 'should destroy all time_tables' do expect{cleaner.destroy_time_tables(Chouette::TimeTable.all)}.to change { @@ -199,11 +199,26 @@ RSpec.describe CleanUp, :type => :model do }.by(-1) end - it 'should destroy associated vehicle_journeys' do - create(:vehicle_journey, time_tables: [time_table]) - expect{cleaner.destroy_time_tables(Chouette::TimeTable.all)}.to change { - Chouette::VehicleJourney.count - }.by(-1) + it 'should destroy time_table vehicle_journey association' do + vj = create(:vehicle_journey, time_tables: [time_table]) + cleaner.destroy_time_tables(Chouette::TimeTable.all) + expect(vj.reload.time_tables).to be_empty + end + end + + context '#destroy_vehicle_journey_without_time_table' do + let(:cleaner) { create(:clean_up) } + + it 'should destroy vehicle_journey' do + vj = create(:vehicle_journey) + expect{cleaner.destroy_vehicle_journey_without_time_table + }.to change { Chouette::VehicleJourney.count }.by(-1) + end + + it 'should not destroy vehicle_journey with time_table' do + create(:vehicle_journey, time_tables: [create(:time_table)]) + expect{cleaner.destroy_vehicle_journey_without_time_table + }.to_not change { Chouette::VehicleJourney.count } end end diff --git a/spec/support/pundit/policies.rb b/spec/support/pundit/policies.rb index e18309226..56433b2ee 100644 --- a/spec/support/pundit/policies.rb +++ b/spec/support/pundit/policies.rb @@ -9,7 +9,7 @@ module Support end def create_user_context(user:, referential:) - OpenStruct.new(user: user, context: {referential: referential}) + UserContext.new(user, referential: referential) end def add_permissions(*permissions, for_user:) diff --git a/spec/views/lines/show.html.erb_spec.rb b/spec/views/lines/show.html.erb_spec.rb index 3a9efa0ce..7bc120f1a 100644 --- a/spec/views/lines/show.html.erb_spec.rb +++ b/spec/views/lines/show.html.erb_spec.rb @@ -3,7 +3,13 @@ require 'spec_helper' describe "/lines/show", :type => :view do assign_referential - let!(:line) { assign :line, create(:line) } + let!(:line) do + line = create(:line) + assign :line, line.decorate(context: { + line_referential: line.line_referential, + current_organisation: referential.organisation + }) + end let!(:line_referential) { assign :line_referential, line.line_referential } let!(:routes) { assign :routes, Array.new(2) { create(:route, :line => line) }.paginate } let!(:map) { assign(:map, double(:to_html => '<div id="map"/>'.html_safe)) } diff --git a/spec/views/time_tables/show.html.erb_spec.rb b/spec/views/time_tables/show.html.erb_spec.rb index f429f9dec..edfb3f3cc 100644 --- a/spec/views/time_tables/show.html.erb_spec.rb +++ b/spec/views/time_tables/show.html.erb_spec.rb @@ -3,7 +3,14 @@ require 'spec_helper' describe "/time_tables/show", :type => :view do assign_referential - let!(:time_table) { assign(:time_table, create(:time_table)) } + let!(:time_table) do + assign( + :time_table, + create(:time_table).decorate(context: { + referential: referential + }) + ) + end let!(:year) { assign(:year, Date.today.cwyear) } let!(:time_table_combination) {assign(:time_table_combination, TimeTableCombination.new)} |
