diff options
| author | Robert | 2017-06-27 14:25:35 +0200 | 
|---|---|---|
| committer | Robert | 2017-06-27 14:25:35 +0200 | 
| commit | f20df3c08dfec0e3dda68401204f7d49470119a7 (patch) | |
| tree | 4f27a34130d4ff044d48729f16d2dff154047ce5 | |
| parent | 970954938043d8d73c4457ee2d91e22c0e422e65 (diff) | |
| parent | eabd56ff9f2c7979192e54b4ae11673f1cc778c1 (diff) | |
| download | chouette-core-f20df3c08dfec0e3dda68401204f7d49470119a7.tar.bz2 | |
conflicts resolved
78 files changed, 1674 insertions, 351 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/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/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/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/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/controllers/calendars_controller.rb b/app/controllers/calendars_controller.rb index 415ebcaeb..23af4129b 100644 --- a/app/controllers/calendars_controller.rb +++ b/app/controllers/calendars_controller.rb @@ -5,6 +5,11 @@ class CalendarsController < BreadcrumbController    respond_to :html    respond_to :js, only: :index +  def show +    show! do +      @calendar = @calendar.decorate +    end +  end    private    def calendar_params 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..83762d83f 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 @@ -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 02349b90b..2462501bc 100644 --- a/app/models/calendar.rb +++ b/app/models/calendar.rb @@ -1,4 +1,7 @@  require 'range_ext' +require_relative 'calendar/date_value' +require_relative 'calendar/period' +  class Calendar < ActiveRecord::Base    belongs_to :organisation @@ -31,9 +34,11 @@ class Calendar < ActiveRecord::Base      end    end + +  ### Calendar::Period    # Required by coocon    def build_period -    Period.new +    Calendar::Period.new    end    def periods @@ -41,11 +46,9 @@ 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 @@ -78,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 @@ -101,11 +104,11 @@ class Calendar < ActiveRecord::Base    private :clear_periods -### dates +  ### Calendar::DateValue    # Required by coocon    def build_date_value -    DateValue.new +    Calendar::DateValue.new    end    def date_values @@ -114,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 @@ -148,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/calendar_date.rb b/app/models/calendar/calendar_date.rb deleted file mode 100644 index cfee95a25..000000000 --- a/app/models/calendar/calendar_date.rb +++ /dev/null @@ -1,29 +0,0 @@ -class Calendar -  class CalendarDate < ::Date - -    module IllegalDate -      attr_reader :year, :month, :day -      def to_s(*_args) -        "%d-%02d-%02d" % [year, month, day] -      end -    end - -    def self.new(*args) -      super(*args) -    rescue -      o = allocate() -      o.instance_exec do -        @illegal = true -        @year, @month, @day = args -        extend IllegalDate -      end -      o -    end - -    def self.from_date(date) -      new date.year, date.month, date.day -    end - -    def legal?; !!!@illegal end -  end -end diff --git a/app/models/calendar/date_value.rb b/app/models/calendar/date_value.rb index 709dc2c14..a4a405d43 100644 --- a/app/models/calendar/date_value.rb +++ b/app/models/calendar/date_value.rb @@ -1,33 +1,32 @@ -class Calendar::DateValue -  include ActiveAttr::Model +class Calendar < ActiveRecord::Base -  attribute :id, type: Integer -  attribute :value, type: Date +  class DateValue +    include ActiveAttr::Model -  validates_presence_of :value -  validate :validate_date -   -  def self.from_date(index, date) -    new id: index, value: Calendar::CalendarDate.from_date(date) -  end +    attribute :id, type: Integer +    attribute :value, type: Date -  # Stuff required for coocon -  def new_record? -    !persisted? -  end +    validates_presence_of :value -  def persisted? -    id.present? -  end +    def self.from_date(index, date) +      new id: index, value: date +    end -  def mark_for_destruction -    self._destroy = true -  end +    # Stuff required for coocon +    def new_record? +      !persisted? +    end + +    def persisted? +      id.present? +    end + +    def mark_for_destruction +      self._destroy = true +    end -  def validate_date -    errors.add(:value, I18n.t('activerecord.errors.models.calendar.attributes.dates.illegal_date', date: value.to_s)) unless value.try(:legal?) +    attribute :_destroy, type: Boolean +    alias_method :marked_for_destruction?, :_destroy    end -  attribute :_destroy, type: Boolean -  alias_method :marked_for_destruction?, :_destroy  end diff --git a/app/models/calendar/period.rb b/app/models/calendar/period.rb index eb1bb5370..bfde242f3 100644 --- a/app/models/calendar/period.rb +++ b/app/models/calendar/period.rb @@ -1,83 +1,63 @@ -class Calendar::Period -  include ActiveAttr::Model +class Calendar < ActiveRecord::Base +   +  class Period +    include ActiveAttr::Model -  attribute :id, type: Integer -  attribute :begin, type: Date -  attribute :end, type: Date +    attribute :id, type: Integer +    attribute :begin, type: Date +    attribute :end, type: Date -  validate :check_end_greather_than_begin -  validates_presence_of :begin, :end -  validate :validate_dates +    validates_presence_of :begin, :end +    validate :check_end_greather_than_begin -  def initialize(args={}) -    super -    self.begin = Calendar::CalendarDate.from_date(self.begin) if Date === self.begin -    self.end = Calendar::CalendarDate.from_date(self.end) if Date === self.end -  end - -  def check_end_greather_than_begin -    if self.begin and self.end and self.begin > self.end -      errors.add(:end, :invalid) +    def check_end_greather_than_begin +      if self.begin and self.end and self.begin > self.end +        errors.add(:end, :invalid) +      end      end -  end -  def self.from_range(index, range) -    new \ -      id:    index,  -      begin: Calendar::CalendarDate.from_date(range.begin),  -      end:   Calendar::CalendarDate.from_date(range.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 and self.end and self.begin <= self.end -      Range.new self.begin, self.end +    def range +      if self.begin and self.end and self.begin <= self.end +        Range.new self.begin, self.end +      end      end -  end -  def intersect?(*other) -    return false if range.nil? +    def intersect?(*other) +      return false if range.nil? -    other = other.flatten -    other = other.delete_if { |o| o.id == id } if id +      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? +      other.any? do |period| +        if other_range = period.range +          (range & other_range).present? +        end        end      end -  end -  def validate_dates -    validate_begin -    validate_end -  end - -  def validate_begin -    errors.add(:begin, I18n.t('activerecord.errors.models.calendar.attributes.dates.illegal_date', date: self.begin.to_s)) unless self.begin.try( :legal? ) -  end - -  def validate_end -    errors.add(:end, I18n.t('activerecord.errors.models.calendar.attributes.dates.illegal_date', date: self.end.to_s)) unless self.end.try( :legal? ) -  end +    def cover? date +      range.cover? date +    end -  def cover? date -    range.cover? date -  end +    # Stuff required for coocon +    def new_record? +      !persisted? +    end -  # Stuff required for coocon -  def new_record? -    !persisted? -  end +    def persisted? +      id.present? +    end -  def persisted? -    id.present? -  end +    def mark_for_destruction +      self._destroy = true +    end -  def mark_for_destruction -    self._destroy = true +    attribute :_destroy, type: Boolean +    alias_method :marked_for_destruction?, :_destroy    end - -  attribute :_destroy, type: Boolean -  alias_method :marked_for_destruction?, :_destroy  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/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/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/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/show.html.slim b/app/views/referentials/show.html.slim index 3c1e36302..dfa264c0b 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 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/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/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..0aedbdd62 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,7 +22,7 @@        = 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 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/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/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/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/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/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/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/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_spec.rb b/spec/models/calendar_spec.rb index 6a2b24011..a3da95aca 100644 --- a/spec/models/calendar_spec.rb +++ b/spec/models/calendar_spec.rb @@ -109,15 +109,11 @@ RSpec.describe Calendar, :type => :model 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 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/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/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)} | 
