diff options
Diffstat (limited to 'app')
162 files changed, 2466 insertions, 947 deletions
| diff --git a/app/assets/javascripts/forms.coffee b/app/assets/javascripts/forms.coffee index 12d82fef1..b7ae3c6ca 100644 --- a/app/assets/javascripts/forms.coffee +++ b/app/assets/javascripts/forms.coffee @@ -32,14 +32,25 @@ isEdge = !isIE && !!window.StyleMedia  @colorSelector = ->    $('.form-group .dropdown.color_selector').each -> -    selectedStatus = $(this).children('.dropdown-toggle').children('.fa-circle') - +    selectedStatusColor = $(this).children('.dropdown-toggle').children('.fa-circle') +    selectedStatusLabel = $(this).children('.dropdown-toggle') +    self = this      $(this).on 'click', "input[type='radio']", (e) ->        selectedValue = e.currentTarget.value +      selectedText = $(e.currentTarget).parent()[0].textContent +      if e.currentTarget.getAttribute("data-for") +        hidden = $("[name=\"#{e.currentTarget.getAttribute("data-for")}\"]") +        if selectedValue == '' -        $(selectedStatus).css('color', 'transparent') +        $(selectedStatusColor).css('color', 'transparent') +        $(selectedStatusLabel).contents().filter( -> this.nodeType == 3 ).filter(':first').text = "" +        hidden?.val ""        else -        $(selectedStatus).css('color', selectedValue) +        $(selectedStatusColor).css('color', selectedValue) +        $(selectedStatusLabel).contents().filter( -> this.nodeType == 3 ).first().replaceWith selectedText +        hidden?.val selectedValue +         +      $(self).find('.dropdown-toggle').click()  $ ->    togglableFilter() diff --git a/app/assets/javascripts/main_menu.coffee b/app/assets/javascripts/main_menu.coffee index a12c47576..e943f448a 100644 --- a/app/assets/javascripts/main_menu.coffee +++ b/app/assets/javascripts/main_menu.coffee @@ -1,12 +1,11 @@  $ -> -  link = [] +  stickyActions = []    ptitleCont = ""    $(document).on 'page:before-change', -> -    link = [] +    stickyActions = []      ptitleCont = "" -    $el = $('#main_nav')      # Opening/closing left-side menu    $el.find('.openMenu').on 'click', (e) -> @@ -24,34 +23,45 @@ $ ->      limit = 51      if $(window).scrollTop() >= limit -      if ($('.page-action .small').length > 0) -        data = $('.page-action .small')[0].innerHTML +      if stickyActions.length == 0 +        if ($('.page-action .small').length > 0) +          stickyActions.push +            content: [ +              $('.page-action .small'), +              $('.page-action .small').first().next() +              ] +            originalParent: $('.page-action .small').parent() + +        for action in $(".sticky-action, .sticky-actions") +          stickyActions.push +            class: "small", +            content: [$(action)] +            originalParent: $(action).parent() -      if ($(".page-title").length > 0) +      if $(".page-title").length > 0          ptitleCont = $(".page-title").html() -      stickyContent = '<div class="sticky-content">' -      stickyContent += '<div class="sticky-ptitle">' + ptitleCont + '</div>' -      stickyContent += '<div class="sticky-paction"><div class="small">' + data + '</div></div>' -      stickyContent += '</div>' +      stickyContent = $('<div class="sticky-content"></div>') +      stickyContent.append $("<div class='sticky-ptitle'>#{ptitleCont}</div>") +      stickyContent.append $('<div class="sticky-paction"></div>')        $('#main_nav').addClass 'sticky'        if $('#menu_top').find('.sticky-content').length == 0          if ptitleCont.length > 0            $('#menu_top').children('.menu-content').after(stickyContent) -        if link.length == 0 -          link = $('.page-action .small').next() - -        $('.sticky-paction .small').after(link) +        for item in stickyActions +          for child in item.content +            child.appendTo $('.sticky-paction')      else        $('#main_nav').removeClass 'sticky'        if $('#menu_top').find('.sticky-content').length > 0 -        if !$('.page-action').find('.formSubmitr').length -          $('.page-action .small').after(link) +        for item in stickyActions +          for child in item.content +            child.appendTo item.originalParent          $('.sticky-content').remove() -  sticker(); +  sticker()    # Sticky behavior    $(document).on 'scroll', sticker diff --git a/app/assets/javascripts/select2.coffee b/app/assets/javascripts/select2.coffee index 2e3884d7f..4cf5f42d0 100644 --- a/app/assets/javascripts/select2.coffee +++ b/app/assets/javascripts/select2.coffee @@ -9,24 +9,27 @@ bind_select2 = (el, cfg = {}) ->    target.select2 $.extend({}, default_cfg, cfg)  bind_select2_ajax = (el, cfg = {}) -> -  target = $(el) +  _this = $(el)    cfg =      ajax:        data: (params) -> -        q: -          "#{target.data('term')}": params.term -      url: target.data('url'), +        if _this.data('term') +          { q: "#{_this.data('term')}": params.term } +        else +          { q: params.term } +      url: _this.data('url'),        dataType: 'json',        delay: 125,        processResults: (data, params) -> results: data -    minimumInputLength: 1 -    placeholder: target.data('select2ed-placeholder')      templateResult: (item) ->        item.text      templateSelection: (item) ->        item.text      escapeMarkup: (markup) ->        markup +    initSelection : (item, callback) -> +      if _this.data('initvalue') +        callback(_this.data('initvalue'))    bind_select2(el, cfg) @@ -40,7 +43,5 @@ bind_select2_ajax = (el, cfg = {}) ->    $('select.form-control.tags').each ->      bind_select2(this, {tags: true}) - -  $ ->    select_2() diff --git a/app/assets/stylesheets/_layout.sass b/app/assets/stylesheets/_layout.sass index b6b91b2a5..340467e77 100644 --- a/app/assets/stylesheets/_layout.sass +++ b/app/assets/stylesheets/_layout.sass @@ -28,6 +28,7 @@ body    // width: 75%    border-bottom: 1px solid rgba($blue, 0.5)    margin: 30px auto 45px auto +  clear: both  .content_header    font-size: 2.2rem diff --git a/app/assets/stylesheets/base/_config.sass b/app/assets/stylesheets/base/_config.sass index 65444479f..ec1c43e7f 100644 --- a/app/assets/stylesheets/base/_config.sass +++ b/app/assets/stylesheets/base/_config.sass @@ -16,6 +16,7 @@ $blue: #007fbb  $darkgrey: #4b4b4b  $grey: #a4a4a4 +$lightgrey: rgba($grey, 0.15)  $green: #70b12b  $red: #da2f36 diff --git a/app/assets/stylesheets/components/_breadcrumb.sass b/app/assets/stylesheets/components/_breadcrumb.sass index 62f167eb4..1b30ca42b 100644 --- a/app/assets/stylesheets/components/_breadcrumb.sass +++ b/app/assets/stylesheets/components/_breadcrumb.sass @@ -1,3 +1,6 @@  .breadcrumbs +  white-space: nowrap +  text-overflow: ellipsis +  overflow: hidden    a -    color: white
\ No newline at end of file +    color: white diff --git a/app/assets/stylesheets/components/_buttons.sass b/app/assets/stylesheets/components/_buttons.sass index a59699383..a649a07ef 100644 --- a/app/assets/stylesheets/components/_buttons.sass +++ b/app/assets/stylesheets/components/_buttons.sass @@ -165,6 +165,13 @@ table, .table              .fa:first-child                margin-right: 0.5em +          & + li.delete-action +            > a, > button +              margin-top: 0 +              &:before +                display: none + +    &.table-2entries .t2e-item      > .th        position: relative diff --git a/app/assets/stylesheets/components/_color_selector.sass b/app/assets/stylesheets/components/_color_selector.sass new file mode 100644 index 000000000..07bfa0c80 --- /dev/null +++ b/app/assets/stylesheets/components/_color_selector.sass @@ -0,0 +1,21 @@ +select.color_selector +  option[value='#9B9B9B'] +    background-color: #9B9B9B +  option[value='#FFA070'] +    background-color: #FFA070 +  option[value='#C67300'] +    background-color: #C67300 +  option[value='#7F551B'] +    background-color: #7F551B +  option[value='#41CCE3'] +    background-color: #41CCE3 +  option[value='#09B09C'] +    background-color: #09B09C +  option[value='#3655D7'] +    background-color: #3655D7 +  option[value='#6321A0'] +    background-color: #6321A0 +  option[value='#E796C6'] +    background-color: #E796C6 +  option[value='#DD2DAA'] +    background-color: #DD2DAA
\ No newline at end of file diff --git a/app/assets/stylesheets/components/_forms.sass b/app/assets/stylesheets/components/_forms.sass index 9a363ab97..47faf19b1 100644 --- a/app/assets/stylesheets/components/_forms.sass +++ b/app/assets/stylesheets/components/_forms.sass @@ -229,6 +229,13 @@ $cbx-size-xs: 15px      &[type='checkbox']:checked + label:before        background-color: $blue +    &[type='checkbox']:disabled + label +      &:before +        border-color: $grey +        background-color: $lightgrey +        cursor: not-allowed +      &:after +        display: none  // Table adjustments  table, .table    .td, td, .th, th diff --git a/app/assets/stylesheets/components/_lists.sass b/app/assets/stylesheets/components/_lists.sass index d8f83d72b..3cce20021 100644 --- a/app/assets/stylesheets/components/_lists.sass +++ b/app/assets/stylesheets/components/_lists.sass @@ -54,3 +54,8 @@ $dlWidth: 40%    // Definition    .dl-def      width: 100% - $dlWidth + +    ul +      list-style: none +      padding-left: 0 +      margin-bottom: 0
\ No newline at end of file diff --git a/app/assets/stylesheets/components/_main_nav.sass b/app/assets/stylesheets/components/_main_nav.sass index fdbf5836a..f102c4617 100644 --- a/app/assets/stylesheets/components/_main_nav.sass +++ b/app/assets/stylesheets/components/_main_nav.sass @@ -17,6 +17,9 @@ $menuW: 300px      line-height: $menuH      padding-left: 10px      opacity: 0.6 +    > a +      color: rgba(#fff, 0.9) +      text-decoration: none    #menu_left      position: absolute @@ -240,6 +243,9 @@ $menuW: 300px            left: 0            top: 13px +        & > .menu-item +          max-width: 75% +                    .menu-item            padding: 0 10px @@ -321,8 +327,11 @@ $menuW: 300px        height: $menuH * 2        transition: 0.1s -    #menu_top > .menu-content > .menu-item-group -      display: none +    #menu_top > .menu-content +      & > .menu-item +        max-width: 90% +      & > .menu-item-group +        display: none      .sticky-content        height: $menuH @@ -342,16 +351,16 @@ $menuW: 300px              white-space: nowrap              max-height: 1.1em              margin: 0 -1.4em 0 0 -            padding: 0 1.4em 0 0 +            padding: 0 1.4em 5px 0              overflow: hidden              &:before                content: '[...]'                font-size: 0.65em                position: absolute -              z-index: 5 +              z-index: 6                right: 0 -              bottom: 4px +              bottom: 9px              &:after                content: '' diff --git a/app/assets/stylesheets/components/_panels.sass b/app/assets/stylesheets/components/_panels.sass index e9f615081..ab25d8184 100644 --- a/app/assets/stylesheets/components/_panels.sass +++ b/app/assets/stylesheets/components/_panels.sass @@ -34,6 +34,7 @@          a            text-decoration: none            color: $blue +          text-transform: capitalize            &:hover, &:focus              color: $darkblue diff --git a/app/assets/stylesheets/components/_referentials.sass b/app/assets/stylesheets/components/_referentials.sass new file mode 100644 index 000000000..0bbb18f2b --- /dev/null +++ b/app/assets/stylesheets/components/_referentials.sass @@ -0,0 +1,4 @@ +#referential_form +	.metadatas-errors +		margin-top: -20px +		margin-bottom: 20px diff --git a/app/assets/stylesheets/components/_tables.sass b/app/assets/stylesheets/components/_tables.sass index 178ec2f36..119a38c07 100644 --- a/app/assets/stylesheets/components/_tables.sass +++ b/app/assets/stylesheets/components/_tables.sass @@ -211,6 +211,18 @@            top: 50%            margin-top: -8px +      .zdlp +        background: url( image-path('map/zdlp.png') ) no-repeat left 50% +        padding-left: 30px + +      .lda +        background: url( image-path('map/lda.png') ) no-repeat left 50% +        padding-left: 30px + +      .gdl +        background: url( image-path('map/lda.png') ) no-repeat left 50% +        padding-left: 30px +  // select_toolbox  .select_toolbox diff --git a/app/assets/stylesheets/modules/import_messages.sass b/app/assets/stylesheets/modules/import_messages.sass index e5666cbcd..6a088dc06 100644 --- a/app/assets/stylesheets/modules/import_messages.sass +++ b/app/assets/stylesheets/modules/import_messages.sass @@ -2,4 +2,42 @@    .status_icon      padding-right: 20px    h1 -    padding-bottom: 20px  +    padding-bottom: 20px + + +.import_message-list +  padding-bottom: 20px + +  .import_messages-head +    display: block +    font-size: 1.8rem +    font-weight: 700 +    border-bottom: 2px solid #4b4b4b +    padding: 5px 15px 6px 15px + +  dl +    dd, dt +      display: inline-block +      letter-spacing: normal +      word-spacing: normal +      text-rendering: auto +      vertical-align: top +      padding: 5px 15px 6px 15px + +    dt +      position: relative +      width: 7% +      font-weight: 700 + +      &:before +        content: "" +        display: block +        position: absolute +        z-index: 1 +        top: 0 +        left: 0 +        width: 250% +        border-bottom: 1px solid rgba(164, 164, 164, 0.5) + +    dd +      width: 93%
\ No newline at end of file diff --git a/app/assets/stylesheets/typography/_sboiv.sass b/app/assets/stylesheets/typography/_sboiv.sass index 6c82a5739..f0943f843 100644 --- a/app/assets/stylesheets/typography/_sboiv.sass +++ b/app/assets/stylesheets/typography/_sboiv.sass @@ -52,6 +52,8 @@    &.sb-5x      font-size: 5em +  &.sb-strong +    font-weight: bold  .sb-ZDLR:before    content: '\e904' @@ -80,7 +82,7 @@  .sb-network:before    content: '\e90c' -.sb-compliance-check-set:before +.sb-compliance_check_set:before, .sb-import_resource:before    content: '\e90d'  .sb-OAT:before @@ -89,7 +91,7 @@  .sb-OAS:before    content: '\e90f' -.sb-calendar:before +.sb-calendar:before, .sb-purchase_window:before    content: '\e910'  .sb-journey_pattern:before @@ -104,7 +106,7 @@  .sb-LDA:before    content: '\e914' -.sb-referential:before +.sb-referential:before, .sb-workbench:before    content: '\e915'  .sb-compliance_control_set:before diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 97f5548ae..474277da1 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,6 +1,7 @@  class ApplicationController < ActionController::Base    include PaperTrailSupport    include Pundit +  include FeatureChecker    rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized diff --git a/app/controllers/autocomplete_purchase_windows_controller.rb b/app/controllers/autocomplete_purchase_windows_controller.rb new file mode 100644 index 000000000..70dc5a346 --- /dev/null +++ b/app/controllers/autocomplete_purchase_windows_controller.rb @@ -0,0 +1,12 @@ +class AutocompletePurchaseWindowsController < ChouetteController +  respond_to :json, :only => [:index] + +  requires_feature :purchase_windows + +  include ReferentialSupport + +  protected +  def collection +    @purchase_windows = referential.purchase_windows.search(params[:q]).result.paginate(page: params[:page]) +  end +end diff --git a/app/controllers/autocomplete_stop_areas_controller.rb b/app/controllers/autocomplete_stop_areas_controller.rb index 233012028..d82fa316a 100644 --- a/app/controllers/autocomplete_stop_areas_controller.rb +++ b/app/controllers/autocomplete_stop_areas_controller.rb @@ -17,13 +17,20 @@ class AutocompleteStopAreasController < ChouetteController        scope = scope.possible_parents if relation_parent?        scope = scope.possible_parents if relation_children?      end +    if search_scope.present? +      scope = StopAreaPolicy::Scope.new(current_user, scope).search_scope(search_scope) +    end      args = [].tap{|arg| 4.times{arg << "%#{params[:q]}%"}} -    @stop_areas = scope.where("name ILIKE ? OR city_name ILIKE ? OR registration_number ILIKE ? OR objectid ILIKE ?", *args).limit(50) +    @stop_areas = scope.where("unaccent(name) ILIKE unaccent(?) OR unaccent(city_name) ILIKE unaccent(?) OR registration_number ILIKE ? OR objectid ILIKE ?", *args).limit(50)      @stop_areas    end    def target_type? -    params.has_key?( :target_type) +    params.has_key?(:target_type) +  end + +  def search_scope +    params[:scope]    end    def relation_parent? diff --git a/app/controllers/calendars_controller.rb b/app/controllers/calendars_controller.rb index 2ed10a111..4a752f2b9 100644 --- a/app/controllers/calendars_controller.rb +++ b/app/controllers/calendars_controller.rb @@ -19,7 +19,7 @@ class CalendarsController < ChouetteController    private    def calendar_params -    permitted_params = [:id, :name, :short_name, periods_attributes: [:id, :begin, :end, :_destroy], date_values_attributes: [:id, :value, :_destroy]] +    permitted_params = [:id, :name, :short_name, :shared, periods_attributes: [:id, :begin, :end, :_destroy], date_values_attributes: [:id, :value, :_destroy]]      permitted_params << :shared if policy(Calendar).share?      params.require(:calendar).permit(*permitted_params)    end diff --git a/app/controllers/companies_controller.rb b/app/controllers/companies_controller.rb index 931d846c5..f84252920 100644 --- a/app/controllers/companies_controller.rb +++ b/app/controllers/companies_controller.rb @@ -61,6 +61,10 @@ class CompaniesController < ChouetteController    alias_method :current_referential, :line_referential    helper_method :current_referential +  def begin_of_association_chain +    current_organisation +  end +    def company_params      params.require(:company).permit( :objectid, :object_version, :name, :short_name, :organizational_unit, :operating_department_name, :code, :phone, :fax, :email, :registration_number, :url, :time_zone )    end diff --git a/app/controllers/concerns/activatable.rb b/app/controllers/concerns/activatable.rb new file mode 100644 index 000000000..1a34551a9 --- /dev/null +++ b/app/controllers/concerns/activatable.rb @@ -0,0 +1,11 @@ +module Activatable +  extend ActiveSupport::Concern + +  %w(activate deactivate).each do |action| +    define_method action do +      authorize resource, "#{action}?" +      resource.send "#{action}!" +      redirect_to request.referer || [current_referential, resource] +    end +  end +end diff --git a/app/controllers/concerns/feature_checker.rb b/app/controllers/concerns/feature_checker.rb new file mode 100644 index 000000000..9ca5ed0a7 --- /dev/null +++ b/app/controllers/concerns/feature_checker.rb @@ -0,0 +1,42 @@ +# Check availability of optional features +# +# In your controller, use : +# +#   requires_feature :test +#   requires_feature :test, only: [:show] +# +# In your view, use : +# +#   has_feature? :test +# +module FeatureChecker +  extend ActiveSupport::Concern + +  module ClassMethods +    def requires_feature(feature, options = {}) +      before_action options do +        check_feature! feature +      end +    end +  end + +  included do +    helper_method :has_feature? +  end + +  protected + +  def has_feature?(*features) +    features.all? do |feature| +      current_organisation.has_feature? feature +    end +  end + +  def check_feature!(*features) +    unless has_feature?(*features) +      raise NotAuthorizedError, "Feature not autorized" +    end +  end + +  class NotAuthorizedError < StandardError; end +end diff --git a/app/controllers/group_of_lines_controller.rb b/app/controllers/group_of_lines_controller.rb index 5762108dc..46d9d077f 100644 --- a/app/controllers/group_of_lines_controller.rb +++ b/app/controllers/group_of_lines_controller.rb @@ -42,7 +42,6 @@ class GroupOfLinesController < ChouetteController      end    end -    protected    def filtered_group_of_lines_maps @@ -70,6 +69,10 @@ class GroupOfLinesController < ChouetteController    alias_method :line_referential, :parent +  def begin_of_association_chain +    current_organisation +  end +    private    def group_of_line_params diff --git a/app/controllers/journey_patterns_collections_controller.rb b/app/controllers/journey_patterns_collections_controller.rb index 736fb1441..5fe78766c 100644 --- a/app/controllers/journey_patterns_collections_controller.rb +++ b/app/controllers/journey_patterns_collections_controller.rb @@ -49,6 +49,7 @@ class JourneyPatternsCollectionsController < ChouetteController    end    def user_permissions +    @features = Hash[*current_organisation.features.map{|f| [f, true]}.flatten].to_json      policy = policy(:journey_pattern)      @perms =        %w{create destroy update}.inject({}) do | permissions, action | diff --git a/app/controllers/line_referentials_controller.rb b/app/controllers/line_referentials_controller.rb index 39c2cdb89..03dab3f8f 100644 --- a/app/controllers/line_referentials_controller.rb +++ b/app/controllers/line_referentials_controller.rb @@ -3,6 +3,7 @@ class LineReferentialsController < ChouetteController    defaults :resource_class => LineReferential    def sync +    authorize resource, :synchronize?      @sync = resource.line_referential_syncs.build      if @sync.save        flash[:notice] = t('notice.line_referential_sync.created') diff --git a/app/controllers/lines_controller.rb b/app/controllers/lines_controller.rb index 2f0ef1542..7041a3a26 100644 --- a/app/controllers/lines_controller.rb +++ b/app/controllers/lines_controller.rb @@ -1,6 +1,8 @@  class LinesController < ChouetteController    include ApplicationHelper +  include Activatable    include PolicyChecker +    defaults :resource_class => Chouette::Line    respond_to :html    respond_to :xml @@ -112,6 +114,10 @@ class LinesController < ChouetteController    alias_method :current_referential, :line_referential    helper_method :current_referential +  def begin_of_association_chain +    current_organisation +  end +    def line_params      params.require(:line).permit(        :transport_mode, @@ -135,6 +141,8 @@ class LinesController < ChouetteController        :color,        :text_color,        :stable_id, +      :transport_submode, +      :secondary_company_ids => [],        footnotes_attributes: [:code, :label, :_destroy, :id]      )    end diff --git a/app/controllers/networks_controller.rb b/app/controllers/networks_controller.rb index 494d1e69f..79a5eb97b 100644 --- a/app/controllers/networks_controller.rb +++ b/app/controllers/networks_controller.rb @@ -71,6 +71,10 @@ class NetworksController < ChouetteController    alias_method :current_referential, :line_referential    helper_method :current_referential +  def begin_of_association_chain +    current_organisation +  end +    def network_params      params.require(:network).permit(:objectid, :object_version, :version_date, :description, :name, :registration_number, :source_name, :source_type_name, :source_identifier, :comment )    end diff --git a/app/controllers/purchase_windows_controller.rb b/app/controllers/purchase_windows_controller.rb new file mode 100644 index 000000000..04b5736bb --- /dev/null +++ b/app/controllers/purchase_windows_controller.rb @@ -0,0 +1,75 @@ +class PurchaseWindowsController < ChouetteController +  include ReferentialSupport +  include RansackDateFilter +  include PolicyChecker +  before_action :ransack_contains_date, only: [:index] +  defaults :resource_class => Chouette::PurchaseWindow, collection_name: 'purchase_windows', instance_name: 'purchase_window' +  belongs_to :referential + +  requires_feature :purchase_windows + +  def index +    index! do +      @purchase_windows = decorate_purchase_windows(@purchase_windows) +    end +  end + +  def show +    show! do +      @purchase_window = @purchase_window.decorate(context: { +        referential: @referential +      }) +    end +  end + +  protected + +  def create_resource(purchase_window) +    purchase_window.referential = @referential +    super +  end + +  private + +  def purchase_window_params +    params.require(:purchase_window).permit(:id, :name, :color, :referential_id, periods_attributes: [:id, :begin, :end, :_destroy]) +  end + +  def decorate_purchase_windows(purchase_windows) +    ModelDecorator.decorate( +      purchase_windows, +      with: PurchaseWindowDecorator, +      context: { +        referential: @referential +        } +      ) +  end + +   def sort_column +    Chouette::PurchaseWindow.column_names.include?(params[:sort]) ? params[:sort] : 'name' +  end + +  def sort_direction +    %w[asc desc].include?(params[:direction]) ?  params[:direction] : 'asc' +  end + +  def collection +    return @purchase_windows if @purchase_windows +    @q = Chouette::PurchaseWindow.ransack(params[:q]) + +    purchase_windows = @q.result +    purchase_windows = purchase_windows.order(sort_column + ' ' + sort_direction) if sort_column && sort_direction +    @purchase_windows = purchase_windows.paginate(page: params[:page]) +  end + +  def ransack_contains_date +    date =[] +    if params[:q] && params[:q]['contains_date(1i)'].present? +      ['contains_date(1i)', 'contains_date(2i)', 'contains_date(3i)'].each do |key| +        date << params[:q][key].to_i +        params[:q].delete(key) +      end +      params[:q]['contains_date'] = @date = Date.new(*date) rescue nil +    end +  end +end diff --git a/app/controllers/referential_companies_controller.rb b/app/controllers/referential_companies_controller.rb index ca1ff67db..7e65a72cf 100644 --- a/app/controllers/referential_companies_controller.rb +++ b/app/controllers/referential_companies_controller.rb @@ -35,7 +35,8 @@ class ReferentialCompaniesController < ChouetteController    def collection      scope = referential.line_referential.companies      if params[:line_id] -      scope = referential.line_referential.lines.find(params[:line_id]).companies +      line_scope = referential.line_referential.lines.find(params[:line_id]).companies +      scope = line_scope if line_scope.exists?      end      @q = scope.search(params[:q]) diff --git a/app/controllers/referential_vehicle_journeys_controller.rb b/app/controllers/referential_vehicle_journeys_controller.rb new file mode 100644 index 000000000..ad08699a5 --- /dev/null +++ b/app/controllers/referential_vehicle_journeys_controller.rb @@ -0,0 +1,17 @@ +# +# Browse all VehicleJourneys of the Referential +# +class ReferentialVehicleJourneysController < ChouetteController +  include ReferentialSupport +  defaults :resource_class => Chouette::VehicleJourney, collection_name: :vehicle_journeys + +  requires_feature :referential_vehicle_journeys + +  private + +  def collection +    @q ||= end_of_association_chain.ransack(params[:q]) +    @vehicle_journeys ||= @q.result.includes(:vehicle_journey_at_stops).paginate page: params[:page], per_page: 10 +  end + +end diff --git a/app/controllers/referentials_controller.rb b/app/controllers/referentials_controller.rb index 227651a59..83e3bc56a 100644 --- a/app/controllers/referentials_controller.rb +++ b/app/controllers/referentials_controller.rb @@ -13,11 +13,16 @@ class ReferentialsController < ChouetteController    end    def create -    create! do |format| -      build_referential - -      if !!@referential.created_from_id -        format.html { redirect_to workbench_path(@referential.workbench) } +    create! do |success, failure| +      success.html do +        if @referential.created_from_id.present? +          flash[:notice] = t('notice.referentials.duplicate') +        end +        redirect_to workbench_path(@referential.workbench) +      end +      failure.html do +        Rails.logger.info "Can't create Referential : #{@referential.errors.inspect}" +        render :new        end      end    end @@ -60,7 +65,7 @@ class ReferentialsController < ChouetteController    def validate      ComplianceControlSetCopyWorker.perform_async(params[:compliance_control_set], params[:id]) -    flash[:notice] = I18n.t("referentials.operation_in_progress") +    flash[:notice] = t('notice.referentials.validate')      redirect_to(referential_path)    end diff --git a/app/controllers/stop_area_referentials_controller.rb b/app/controllers/stop_area_referentials_controller.rb index 85541230d..f2d375e49 100644 --- a/app/controllers/stop_area_referentials_controller.rb +++ b/app/controllers/stop_area_referentials_controller.rb @@ -2,6 +2,7 @@ class StopAreaReferentialsController < ChouetteController    defaults :resource_class => StopAreaReferential    def sync +    authorize resource, :synchronize?      @sync = resource.stop_area_referential_syncs.build      if @sync.save        flash[:notice] = t('notice.stop_area_referential_sync.created') diff --git a/app/controllers/stop_areas_controller.rb b/app/controllers/stop_areas_controller.rb index 133518324..5243ce56c 100644 --- a/app/controllers/stop_areas_controller.rb +++ b/app/controllers/stop_areas_controller.rb @@ -1,5 +1,6 @@  class StopAreasController < ChouetteController    include ApplicationHelper +  include Activatable    defaults :resource_class => Chouette::StopArea @@ -13,10 +14,12 @@ class StopAreasController < ChouetteController    respond_to :html, :kml, :xml, :json    respond_to :js, :only => :index -  # def complete -  #   @stop_areas = line.stop_areas -  #   render :layout => false -  # end +  def autocomplete +    scope = stop_area_referential.stop_areas.where(deleted_at: nil) +    args  = [].tap{|arg| 4.times{arg << "%#{params[:q]}%"}} +    @stop_areas = scope.where("unaccent(name) ILIKE unaccent(?) OR unaccent(city_name) ILIKE unaccent(?) OR registration_number ILIKE ? OR objectid ILIKE ?", *args).limit(50) +    @stop_areas +  end    def select_parent      @stop_area = stop_area @@ -154,6 +157,10 @@ class StopAreasController < ChouetteController      end    end +  def begin_of_association_chain +    current_organisation +  end +    private    def sort_column @@ -171,7 +178,35 @@ class StopAreasController < ChouetteController    helper_method :current_referential    def stop_area_params -    params.require(:stop_area).permit( :routing_stop_ids, :routing_line_ids, :children_ids, :stop_area_type, :parent_id, :objectid, :object_version, :name, :comment, :area_type, :registration_number, :nearest_topic_name, :fare_code, :longitude, :latitude, :long_lat_type, :country_code, :street_name, :zip_code, :city_name, :mobility_restricted_suitability, :stairs_availability, :lift_availability, :int_user_needs, :coordinates, :url, :time_zone ) +    params.require(:stop_area).permit( +      :area_type, +      :children_ids, +      :city_name, +      :comment, +      :coordinates, +      :country_code, +      :fare_code, +      :int_user_needs, +      :latitude, +      :lift_availability, +      :long_lat_type, +      :longitude, +      :mobility_restricted_suitability, +      :name, +      :nearest_topic_name, +      :object_version, +      :objectid, +      :parent_id, +      :registration_number, +      :routing_line_ids, +      :routing_stop_ids, +      :stairs_availability, +      :street_name, +      :time_zone, +      :url, +      :waiting_time, +      :zip_code, +    )    end  end diff --git a/app/controllers/vehicle_journeys_controller.rb b/app/controllers/vehicle_journeys_controller.rb index c941aeae4..c03db0c7f 100644 --- a/app/controllers/vehicle_journeys_controller.rb +++ b/app/controllers/vehicle_journeys_controller.rb @@ -40,43 +40,46 @@ class VehicleJourneysController < ChouetteController    end    def index -    @stop_points_list = [] -    route.stop_points.each do |sp| -      @stop_points_list << { -        :id => sp.stop_area.id, -        :route_id => sp.try(:route_id), -        :object_id => sp.try(:objectid), -        :position => sp.try(:position), -        :for_boarding => sp.try(:for_boarding), -        :for_alighting => sp.try(:for_alighting), -        :name => sp.stop_area.try(:name), -        :zip_code => sp.stop_area.try(:zip_code), -        :city_name => sp.stop_area.try(:city_name), -        :comment => sp.stop_area.try(:comment), -        :area_type => sp.stop_area.try(:area_type), -        :registration_number => sp.stop_area.try(:registration_number), -        :nearest_topic_name => sp.stop_area.try(:nearest_topic_name), -        :fare_code => sp.stop_area.try(:fare_code), -        :longitude => sp.stop_area.try(:longitude), -        :latitude => sp.stop_area.try(:latitude), -        :long_lat_type => sp.stop_area.try(:long_lat_type), -        :country_code => sp.stop_area.try(:country_code), -        :street_name => sp.stop_area.try(:street_name) -      } -    end - -    @transport_mode = route.line['transport_mode'] -    @transport_submode = route.line['transport_submode'] - -    if params[:jp] -      @jp_origin  = Chouette::JourneyPattern.find_by(objectid: params[:jp]) -      @jp_origin_stop_points = @jp_origin.stop_points -    end - -    index! do +    index! do |format|        if collection.out_of_bounds?          redirect_to params.merge(:page => 1)        end +      format.json do +        @vehicle_journeys = @vehicle_journeys.includes({stop_points: :stop_area}) +      end +      format.html do +        @stop_points_list = [] +        @stop_points_list = route.stop_points.includes(:stop_area).map do |sp| +          { +            :id => sp.stop_area.id, +            :route_id => sp.try(:route_id), +            :object_id => sp.try(:objectid), +            :position => sp.try(:position), +            :for_boarding => sp.try(:for_boarding), +            :for_alighting => sp.try(:for_alighting), +            :name => sp.stop_area.try(:name), +            :zip_code => sp.stop_area.try(:zip_code), +            :city_name => sp.stop_area.try(:city_name), +            :comment => sp.stop_area.try(:comment), +            :area_type => sp.stop_area.try(:area_type), +            :registration_number => sp.stop_area.try(:registration_number), +            :nearest_topic_name => sp.stop_area.try(:nearest_topic_name), +            :fare_code => sp.stop_area.try(:fare_code), +            :longitude => sp.stop_area.try(:longitude), +            :latitude => sp.stop_area.try(:latitude), +            :long_lat_type => sp.stop_area.try(:long_lat_type), +            :country_code => sp.stop_area.try(:country_code), +            :street_name => sp.stop_area.try(:street_name) +          } +        end +        @transport_mode = route.line['transport_mode'] +        @transport_submode = route.line['transport_submode'] + +        if params[:jp] +          @jp_origin  = Chouette::JourneyPattern.find_by(objectid: params[:jp]) +          @jp_origin_stop_points = @jp_origin.stop_points +        end +      end      end    end @@ -92,13 +95,15 @@ class VehicleJourneysController < ChouetteController      scope = maybe_filter_by_departure_time(scope)      scope = maybe_filter_out_journeys_with_time_tables(scope) -    @q = scope.search filtered_ransack_params +    @vehicle_journeys ||= begin +      @q = scope.search filtered_ransack_params -    @ppage = 20 -    @vehicle_journeys = @q.result.paginate(:page => params[:page], :per_page => @ppage) -    @footnotes = route.line.footnotes.to_json -    @matrix    = resource_class.matrix(@vehicle_journeys) -    @vehicle_journeys +      @ppage = 20 +      vehicle_journeys = @q.result.paginate(:page => params[:page], :per_page => @ppage) +      @footnotes = route.line.footnotes.to_json +      @matrix    = resource_class.matrix(vehicle_journeys) +      vehicle_journeys +    end    end    def maybe_filter_by_departure_time(scope) @@ -159,6 +164,7 @@ class VehicleJourneysController < ChouetteController    end    def user_permissions +    @features = Hash[*current_organisation.features.map{|f| [f, true]}.flatten].to_json      policy = policy(:vehicle_journey)      @perms =        %w{create destroy update}.inject({}) do | permissions, action | diff --git a/app/decorators/company_decorator.rb b/app/decorators/company_decorator.rb index 9416c73ae..50b82d276 100644 --- a/app/decorators/company_decorator.rb +++ b/app/decorators/company_decorator.rb @@ -18,13 +18,6 @@ class CompanyDecorator < Draper::Decorator    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[:referential]) -      ) -    end -      if h.policy(object).update?        links << Link.new(          content: h.t('companies.actions.edit'), @@ -37,7 +30,7 @@ class CompanyDecorator < Draper::Decorator      if h.policy(object).destroy?        links << Link.new( -        content: t('companies.actions.destroy'), +        content: h.destroy_link_content('companies.actions.destroy'),          href: h.line_referential_company_path(            context[:referential],            object diff --git a/app/decorators/compliance_check_set_decorator.rb b/app/decorators/compliance_check_set_decorator.rb index 096596b19..c58e14d88 100644 --- a/app/decorators/compliance_check_set_decorator.rb +++ b/app/decorators/compliance_check_set_decorator.rb @@ -3,15 +3,6 @@ class ComplianceCheckSetDecorator < Draper::Decorator    def action_links      links = [] - -    links << Link.new( -      content: h.destroy_link_content, -      href: h.workbench_compliance_check_sets_path(object.id), -      method: :delete, -      data: {confirm: h.t('imports.actions.destroy_confirm')} -    ) - -    links    end    def lines_status diff --git a/app/decorators/line_decorator.rb b/app/decorators/line_decorator.rb index ede670cbd..9c0cf7292 100644 --- a/app/decorators/line_decorator.rb +++ b/app/decorators/line_decorator.rb @@ -18,7 +18,8 @@ class LineDecorator < Draper::Decorator      links << Link.new(        content: h.t('lines.actions.show_company'), -      href: [context[:line_referential], object.company] +      href: [context[:line_referential], object.company], +      disabled: object.company.nil?      )      if h.policy(Chouette::Line).create? && @@ -41,6 +42,26 @@ class LineDecorator < Draper::Decorator        )      end +    if h.policy(object).deactivate? +      links << Link.new( +        content: h.deactivate_link_content('lines.actions.deactivate'), +        href: h.deactivate_line_referential_line_path(context[:line_referential], object), +        method: :put, +        data: {confirm: h.t('lines.actions.deactivate_confirm')}, +        extra_class: "delete-action" +      ) +    end + +    if h.policy(object).activate? +      links << Link.new( +        content: h.activate_link_content('lines.actions.activate'), +        href: h.activate_line_referential_line_path(context[:line_referential], object), +        method: :put, +        data: {confirm: h.t('lines.actions.activate_confirm')}, +        extra_class: "delete-action" +      ) +    end +      if h.policy(object).destroy?        links << Link.new(          content: h.destroy_link_content('lines.actions.destroy'), diff --git a/app/decorators/purchase_window_decorator.rb b/app/decorators/purchase_window_decorator.rb new file mode 100644 index 000000000..646fdea0d --- /dev/null +++ b/app/decorators/purchase_window_decorator.rb @@ -0,0 +1,34 @@ +class PurchaseWindowDecorator < Draper::Decorator +  decorates Chouette::PurchaseWindow +  delegate_all + +  def action_links +    policy = h.policy(object) +    links = [] + +    if policy.update? +      links << Link.new( +        content: I18n.t('actions.edit'), +        href: h.edit_referential_purchase_window_path(context[:referential].id, object) +      ) +    end + +    if policy.destroy? +      links << Link.new( +        content: I18n.t('actions.destroy'), +        href: h.referential_purchase_window_path(context[:referential].id, object), +        method: :delete, +        data: { confirm: h.t('purchase_windows.actions.destroy_confirm') } +      ) +    end + +    links +  end + +  def bounding_dates +    unless object.date_ranges.empty? +      object.date_ranges.map(&:min).min..object.date_ranges.map(&:max).max +    end +  end + +end diff --git a/app/decorators/referential_decorator.rb b/app/decorators/referential_decorator.rb index 4103790aa..d75ad1050 100644 --- a/app/decorators/referential_decorator.rb +++ b/app/decorators/referential_decorator.rb @@ -3,12 +3,26 @@ class ReferentialDecorator < Draper::Decorator    def action_links      policy = h.policy(object) -    links = [ -      Link.new( -        content: h.t('time_tables.index.title'), -        href: h.referential_time_tables_path(object) +    links = [] + +    if has_feature?(:referential_vehicle_journeys) +      links << Link.new( +        content: h.t('referential_vehicle_journeys.index.title'), +        href: h.referential_vehicle_journeys_path(object) +      ) +    end + +    if has_feature?(:purchase_windows) +      links << Link.new( +        content: h.t('purchase_windows.index.title'), +        href: h.referential_purchase_windows_path(object)        ) -    ] +    end + +    links << Link.new( +      content: h.t('time_tables.index.title'), +      href: h.referential_time_tables_path(object) +    )      if policy.clone?        links << Link.new( @@ -63,4 +77,12 @@ class ReferentialDecorator < Draper::Decorator      links    end + +  private + +  # TODO move to a base Decorator (ApplicationDecorator) +  def has_feature?(*features) +    h.has_feature?(*features) rescue false +  end +  end diff --git a/app/decorators/referential_line_decorator.rb b/app/decorators/referential_line_decorator.rb index 55acf7ed9..dceb3e2a9 100644 --- a/app/decorators/referential_line_decorator.rb +++ b/app/decorators/referential_line_decorator.rb @@ -24,30 +24,6 @@ class ReferentialLineDecorator < Draper::Decorator        )      ) -    if h.policy(Chouette::Line).create? && -        context[:referential].organisation == context[:current_organisation] -      links << Link.new( -        content: h.t('actions.new'), -        href: h.new_referential_line_path(context[:referential]) -      ) -    end - -    if h.policy(object).update? -      links << Link.new( -        content: h.t('actions.edit'), -        href: h.edit_referential_line_path(context[:referential], object) -      ) -    end - -    if h.policy(object).destroy? -      links << Link.new( -        content: h.destroy_link_content('actions.destroy'), -        href: h.referential_line_path(context[:referential], object), -        method: :delete, -        data: { confirm: t('lines.actions.destroy_confirm') } -      ) -    end -      if !object.hub_restricted? ||          (object.hub_restricted? && object.routes.size < 2)        if h.policy(Chouette::Route).create? && diff --git a/app/decorators/route_decorator.rb b/app/decorators/route_decorator.rb index 510c941a3..ec7f0d6aa 100644 --- a/app/decorators/route_decorator.rb +++ b/app/decorators/route_decorator.rb @@ -13,7 +13,7 @@ class RouteDecorator < Draper::Decorator      if object.stop_points.any?        links << Link.new( -        content: h.t('journey_patterns.index.title'), +        content: h.t('journey_patterns.actions.index'),          href: [            context[:referential],            context[:line], diff --git a/app/decorators/stop_area_decorator.rb b/app/decorators/stop_area_decorator.rb index 4e777292d..32f6e1d2b 100644 --- a/app/decorators/stop_area_decorator.rb +++ b/app/decorators/stop_area_decorator.rb @@ -3,21 +3,12 @@ class StopAreaDecorator < Draper::Decorator    delegate_all -  def action_links(stop_area = nil) -    links = [] +  def common_action_links(stop_area = nil) +    top_links, bottom_links = [], []      stop_area ||= object -    if h.policy(Chouette::StopArea).new? -      links << Link.new( -        content: h.t('stop_areas.actions.new'), -        href: h.new_stop_area_referential_stop_area_path( -          stop_area.stop_area_referential -        ) -      ) -    end -      if h.policy(stop_area).update? -      links << Link.new( +      top_links << Link.new(          content: h.t('stop_areas.actions.edit'),          href: h.edit_stop_area_referential_stop_area_path(            stop_area.stop_area_referential, @@ -27,7 +18,7 @@ class StopAreaDecorator < Draper::Decorator      end      if h.policy(stop_area).destroy? -      links << Link.new( +      bottom_links << Link.new(          content: h.destroy_link_content('stop_areas.actions.destroy'),          href: h.stop_area_referential_stop_area_path(            stop_area.stop_area_referential, @@ -38,6 +29,40 @@ class StopAreaDecorator < Draper::Decorator        )      end -    links +    [top_links, bottom_links] +  end + +  def action_links(stop_area = nil) +    stop_area ||= object +    top_links, bottom_links = common_action_links(stop_area) +    links = [] + +    if h.policy(object).deactivate? +      links << Link.new( +        content: h.deactivate_link_content('stop_areas.actions.deactivate'), +        href: h.deactivate_stop_area_referential_stop_area_path(stop_area.stop_area_referential, object), +        method: :put, +        data: {confirm: h.t('stop_areas.actions.deactivate_confirm')}, +        extra_class: "delete-action" +      ) +    end + +    if h.policy(object).activate? +      links << Link.new( +        content: h.activate_link_content('stop_areas.actions.activate'), +        href: h.activate_stop_area_referential_stop_area_path(stop_area.stop_area_referential, object), +        method: :put, +        data: {confirm: h.t('stop_areas.actions.activate_confirm')}, +        extra_class: "delete-action" +      ) +    end + +    top_links + links + bottom_links    end + +  def waiting_time_text +    return '-' if [nil, 0].include? waiting_time +    h.t('stop_areas.waiting_time_format', value: waiting_time) +  end +  end diff --git a/app/decorators/stop_point_decorator.rb b/app/decorators/stop_point_decorator.rb index 196d6d490..27e1a7058 100644 --- a/app/decorators/stop_point_decorator.rb +++ b/app/decorators/stop_point_decorator.rb @@ -4,6 +4,6 @@ class StopPointDecorator < StopAreaDecorator    delegate_all    def action_links -    super(object.stop_area) +    common_action_links(object.stop_area).flatten    end  end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index d2cdaaa20..713542ff4 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -3,24 +3,38 @@ module ApplicationHelper    include NewapplicationHelper +  def array_to_html_list items +    content_tag :ul do +      items.each do |item| +        concat content_tag :li, item +      end +    end +  end +    def page_header_title(object)      # Unwrap from decorator, we want to know the object model name      object = object.object if object.try(:object)      local  = "#{object.model_name.name.underscore.pluralize}.#{params[:action]}.title"      if object.try(:name) -      t(local, name: object.name) +      t(local, name: object.name || object.id)      else        t(local)      end    end    def page_header_meta(object) -    info = t('last_update', time: l(object.updated_at, format: :short)) -    if object.try(:versions) -      author = object.versions.try(:last).try(:whodunnit) || t('default_whodunnit') -      info   = "#{info} <br/> #{t('whodunnit', author: author)}" +    out = "" +    display = true +    display = policy(object).synchronize? if policy(object).respond_to?(:synchronize?) rescue false +    if display +      info = t('last_update', time: l(object.updated_at, format: :short)) +      if object.try(:versions) +        author = object.versions.try(:last).try(:whodunnit) || t('default_whodunnit') +        info   = "#{info} <br/> #{t('whodunnit', author: author)}" +      end +      out += content_tag :div, info.html_safe, class: 'small last-update'      end -    content_tag :div, info.html_safe, class: 'small' +    out.html_safe    end    def page_header_content_for(object) diff --git a/app/helpers/common_helpers.rb b/app/helpers/common_helpers.rb deleted file mode 100644 index 29cabddac..000000000 --- a/app/helpers/common_helpers.rb +++ /dev/null @@ -1,26 +0,0 @@ - -module CommonHelpers -  # TODO: Needs refactoring, but does not seem to be under test -  # so let us refactor this **after** test coverage. -  def access_links_pairs(access_links) -    hpairs = Hash.new -    pairs = Array.new -    access_links.each do |link| -      key = pair_key(link) -      pair = nil -      if (hpairs.has_key? key) -        pair = hpairs[key] -      else -        pair = AccessLinkPair.new -        pairs << pair -        hpairs[key] = pair -      end -      if (link.link_orientation_type == "access_point_to_stop_area") -        pair.from_access_point = link -      else -        pair.to_access_point = link -      end -    end -    pairs -  end -end diff --git a/app/helpers/compliance_control_sets_helper.rb b/app/helpers/compliance_control_sets_helper.rb index bb2a72623..57e6d9608 100644 --- a/app/helpers/compliance_control_sets_helper.rb +++ b/app/helpers/compliance_control_sets_helper.rb @@ -4,12 +4,12 @@ module ComplianceControlSetsHelper      [current_organisation, Organisation.find_by_name("STIF")].uniq    end -  def flotted_links ccs_id = @compliance_control_set +  def floating_links ccs_id      links = [new_control(ccs_id), new_block(ccs_id)] -    unless links.all? &:nil? -      content_tag :div, class: 'select_toolbox' do +    if links.any? +      content_tag :div, class: 'select_toolbox', id: 'floating-links' do          content_tag :ul do -          links.collect {|link| concat content_tag(:li, link, class: 'st_action with_text') unless link.nil?}  +          links.collect {|link| concat content_tag(:li, link, class: 'st_action with_text') if link}           end        end      end @@ -21,8 +21,6 @@ module ComplianceControlSetsHelper          concat content_tag :span, nil, class: 'fa fa-plus'          concat content_tag :span, t('compliance_control_sets.actions.add_compliance_control')        end -    else -      nil      end    end @@ -32,8 +30,83 @@ module ComplianceControlSetsHelper          concat content_tag :span, nil, class: 'fa fa-plus'          concat content_tag :span,t('compliance_control_sets.actions.add_compliance_control_block')        end    -    else -      nil      end    end -end
\ No newline at end of file + +  def render_compliance_control_block(block=nil) +    content_tag :div, class: 'row' do +      content_tag :div, class: 'col-lg-12' do +        content_tag :h2 do +          concat transport_mode_text(block) +          concat dropdown(block) if block +        end +      end +    end +  end + +  def dropdown(block) +    dropdown_button = content_tag :div, class: 'btn dropdown-toggle', "data-toggle": "dropdown" do +      content_tag :div, nil, class: 'span fa fa-cog' +    end + +    dropdown_menu = content_tag :ul, class: 'dropdown-menu' do +      link_1 = content_tag :li do +        link_to t('compliance_control_sets.actions.edit'), edit_compliance_control_set_compliance_control_block_path(@compliance_control_set.id, block.id) +      end +      link_2 = content_tag :li do +        link_to t('compliance_control_sets.actions.destroy'), compliance_control_set_compliance_control_block_path(@compliance_control_set.id, block.id), :method => :delete, :data => {:confirm =>  t('compliance_control_sets.actions.destroy_confirm')} +      end +      link_1 + link_2 +    end + +    content_tag :div, class: 'btn-group' do +      dropdown_button + dropdown_menu +    end + +  end + +  def render_compliance_controls(compliance_controls) +    content_tag :div, class: 'row' do +      content_tag :div, class: 'col-lg-12' do +        compliance_controls.try(:any?) ? render_table_builder(compliance_controls) : render_no_controls +      end +    end + +  end + +  def render_table_builder(compliance_controls) +    table = content_tag :div, class: 'select_table' do +      table_builder_2 compliance_controls, +        [ +          TableBuilderHelper::Column.new( +            key: :code, +            attribute: 'code' +          ), +          TableBuilderHelper::Column.new( +            key: :name, +            attribute: 'name', +            link_to: lambda do |compliance_control| +              compliance_control_set_compliance_control_path(@compliance_control_set, compliance_control) +            end +          ), +          TableBuilderHelper::Column.new( +            key: :criticity, +            attribute: 'criticity' +          ), +          TableBuilderHelper::Column.new( +            key: :comment, +            attribute: 'comment' +          ), +      ], +      sortable: true, +      cls: 'table has-filter has-search', +      model: ComplianceControl +    end +    metas = content_tag :div, I18n.t('compliance_control_blocks.metas.control', count: compliance_controls.count), class: 'pull-right' +    table + metas +  end + +  def render_no_controls +    content_tag :div, I18n.t('compliance_control_blocks.metas.control.zero'), class: 'alert alert-warning' +  end +end diff --git a/app/helpers/links_helper.rb b/app/helpers/links_helper.rb index 4fb7a797d..088415dc3 100644 --- a/app/helpers/links_helper.rb +++ b/app/helpers/links_helper.rb @@ -1,5 +1,18 @@  module LinksHelper +  def custom_link_content(translation_key, klass, extra_class: nil) +    klass = ["fa", "fa-#{klass}", "mr-xs", extra_class].compact.join(" ") +    content_tag(:span, nil, class: klass) + t(translation_key) +  end +    def destroy_link_content(translation_key = 'actions.destroy') -    content_tag(:span, nil, class: 'fa fa-trash mr-xs') + t(translation_key) +    custom_link_content translation_key, 'trash' +  end + +  def deactivate_link_content(translation_key = 'actions.deactivate') +    custom_link_content translation_key, 'power-off', extra_class: "text-danger" +  end + +  def activate_link_content(translation_key = 'actions.activate') +    custom_link_content translation_key, 'power-off', extra_class: "text-success"    end  end diff --git a/app/helpers/multiple_selection_toolbox_helper.rb b/app/helpers/multiple_selection_toolbox_helper.rb index 85294af6d..e0a1d2dd4 100644 --- a/app/helpers/multiple_selection_toolbox_helper.rb +++ b/app/helpers/multiple_selection_toolbox_helper.rb @@ -1,7 +1,11 @@  module MultipleSelectionToolboxHelper    # Box of links that floats at the bottom right of the page -  def multiple_selection_toolbox(actions) +  # c.f. https://projects.af83.io/issues/5206 +  # #5206 method too long +  def multiple_selection_toolbox(actions, collection_name:)      links = content_tag :ul do +       +      # #5206 `if params[:controller]` mieux passer comme parametre si besoin        delete_path = nil        if params[:controller] = 'workbenches' @@ -15,6 +19,7 @@ module MultipleSelectionToolboxHelper              method: :delete,              data: {                path: delete_path, +              # #5206 Missing Translations                confirm: 'Etes-vous sûr(e) de vouloir effectuer cette action ?'              },              title: t("actions.#{action}") @@ -33,7 +38,9 @@ module MultipleSelectionToolboxHelper        class: 'info-msg'      ) -    content_tag :div, '', class: 'select_toolbox noselect' do +    content_tag :div, '',  +      class: 'select_toolbox noselect', +      id: "selected-#{collection_name}-action-box" do        links + label      end    end diff --git a/app/helpers/table_builder_helper.rb b/app/helpers/table_builder_helper.rb index 37f01ce0d..dede51920 100644 --- a/app/helpers/table_builder_helper.rb +++ b/app/helpers/table_builder_helper.rb @@ -95,6 +95,18 @@ module TableBuilderHelper        class: cls    end +  def self.item_row_class_name collection +    if collection.respond_to?(:model) +      model_name = collection.model.name +    elsif collection.respond_to?(:first) +      model_name = collection.first.class.name +    else +      model_name = "item" +    end + +    model_name.split("::").last.parameterize +  end +    private    def thead(collection, columns, sortable, selectable, has_links, overhead, model ) @@ -187,86 +199,92 @@ module TableBuilderHelper      end    end -  def tbody(collection, columns, selectable, links, overhead) -    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) +  def tr item, columns, selectable, links, overhead, model_name +    klass = "#{model_name}-#{item.id}" +    content_tag :tr, class: klass do +      bcont = [] +      if selectable +        disabled = selectable.respond_to?(:call) && !selectable.call(item) +        bcont << content_tag( +          :td, +          checkbox(id_name: item.try(:id), value: item.try(:id), disabled: disabled) +        ) +      end -            if column.linkable? -              path = column.link_to(item) -              link = link_to(value, path) +      columns.each do |column| +        value = column.value(item) -              if overhead.empty? -                bcont << content_tag(:td, link, title: 'Voir') +        if column.linkable? +          path = column.link_to(item) +          link = link_to(value, path) -              else -                i = columns.index(column) - -                if overhead[i].blank? -                  if (i > 0) && (overhead[i - 1][:width] > 1) -                    clsArrayAlt = overhead[i - 1][:cls].split +          if overhead.empty? +            bcont << content_tag(:td, link, title: 'Voir') -                    bcont << content_tag(:td, link, title: 'Voir', class: td_cls(clsArrayAlt)) +          else +            i = columns.index(column) -                  else -                    bcont << content_tag(:td, link, title: 'Voir') -                  end +            if overhead[i].blank? +              if (i > 0) && (overhead[i - 1][:width] > 1) +                clsArrayAlt = overhead[i - 1][:cls].split -                else -                  clsArray = overhead[columns.index(column)][:cls].split +                bcont << content_tag(:td, link, title: 'Voir', class: td_cls(clsArrayAlt)) -                  bcont << content_tag(:td, link, title: 'Voir', class: td_cls(clsArray)) -                end +              else +                bcont << content_tag(:td, link, title: 'Voir')                end              else -              if overhead.empty? -                bcont << content_tag(:td, value) +              clsArray = overhead[columns.index(column)][:cls].split -              else -                i = columns.index(column) +              bcont << content_tag(:td, link, title: 'Voir', class: td_cls(clsArray)) +            end +          end -                if overhead[i].blank? -                  if (i > 0) && (overhead[i - 1][:width] > 1) -                    clsArrayAlt = overhead[i - 1][:cls].split +        else +          if overhead.empty? +            bcont << content_tag(:td, value) -                    bcont << content_tag(:td, value, class: td_cls(clsArrayAlt)) +          else +            i = columns.index(column) -                  else -                    bcont << content_tag(:td, value) -                  end +            if overhead[i].blank? +              if (i > 0) && (overhead[i - 1][:width] > 1) +                clsArrayAlt = overhead[i - 1][:cls].split -                else -                  clsArray = overhead[i][:cls].split +                bcont << content_tag(:td, value, class: td_cls(clsArrayAlt)) -                  bcont << content_tag(:td, value, class: td_cls(clsArray)) -                end +              else +                bcont << content_tag(:td, value)                end + +            else +              clsArray = overhead[i][:cls].split + +              bcont << content_tag(:td, value, class: td_cls(clsArray))              end            end +        end +      end -          if links.any? || item.try(:action_links).try(:any?) -            bcont << content_tag( -              :td, -              build_links(item, links), -              class: 'actions' -            ) -          end +      if links.any? || item.try(:action_links).try(:any?) +        bcont << content_tag( +          :td, +          build_links(item, links), +          class: 'actions' +        ) +      end -          bcont.join.html_safe -        end +      bcont.join.html_safe +    end +  end + +  def tbody(collection, columns, selectable, links, overhead) +    model_name = TableBuilderHelper.item_row_class_name collection + +    content_tag :tbody do +      collection.map do |item| +        tr item, columns, selectable, links, overhead, model_name        end.join.html_safe      end    end @@ -341,14 +359,20 @@ module TableBuilderHelper      end    end -  def checkbox(id_name:, value:) +  def checkbox(id_name:, value:, disabled: false)      content_tag :div, '', class: 'checkbox' do -      check_box_tag(id_name, value).concat( +      check_box_tag(id_name, value, nil, disabled: disabled).concat(          content_tag(:label, '', for: id_name)        )      end    end +    def gear_menu_link(link) +    klass = [] +    klass << link.extra_class if link.extra_class +    klass << 'delete-action' if link.method == :delete +    klass << 'disabled' if link.disabled +      content_tag(        :li,        link_to( @@ -358,7 +382,7 @@ module TableBuilderHelper        ) do          link.content        end, -      class: ('delete-action' if link.method == :delete) +      class: (klass.join(' ') if klass.present?)      )    end diff --git a/app/helpers/table_builder_helper/url.rb b/app/helpers/table_builder_helper/url.rb index a53ac5620..28f1ade76 100644 --- a/app/helpers/table_builder_helper/url.rb +++ b/app/helpers/table_builder_helper/url.rb @@ -10,7 +10,7 @@ module TableBuilderHelper            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) +          polymorph_url << item if item.respond_to?(:stop_points) || item.is_a?(Chouette::TimeTable) || item.is_a?(Chouette::PurchaseWindow)          elsif item.respond_to? :referential            if item.respond_to? :workbench              polymorph_url << item.workbench diff --git a/app/inputs/color_select_input.rb b/app/inputs/color_select_input.rb new file mode 100644 index 000000000..f92c80a22 --- /dev/null +++ b/app/inputs/color_select_input.rb @@ -0,0 +1,44 @@ +class ColorSelectInput < SimpleForm::Inputs::CollectionInput +  enable :placeholder + +  def input(wrapper_options = {}) +    selected_color = object.send(attribute_name) +    label = if selected_color +      collection.find{|i| i.is_a?(Enumerable) && i.last == selected_color}.try(:first) +    end + +    out = @builder.hidden_field attribute_name, value: selected_color +    tag_name = ActionView::Helpers::Tags::Base.new( ActiveModel::Naming.param_key(object), attribute_name, :dummy ).send(:tag_name) +    select = <<-eos +  <div class="dropdown color_selector"> +    <button type='button' class="btn btn-default dropdown-toggle" data-toggle='dropdown' aria-haspopup='true' aria-expanded='true' +      ><span +        class='fa fa-circle mr-xs' +        style='color: #{selected_color == nil ? 'transparent' : selected_color}' +        > +      </span> +      #{label} +      <span class='caret'></span> +    </button> + +    <div class="form-group dropdown-menu" aria-labelledby='dpdwn_color'> +    eos + +    collection.each do |color| +      name = nil +      name, color = color if color.is_a?(Enumerable) +      select += <<-eos +        <span class="radio" key=#{color} > +          <label> +            <input type='radio' class='color_selector' value='#{color}' data-for='#{tag_name}'/> +            <span class='fa fa-circle mr-xs' style='color: #{color == nil ? 'transparent' : color}'></span> +            #{name} +          </label> +        </span> +      eos +    end +    select += "</div></div>" + +    out + select.html_safe +  end +end diff --git a/app/javascript/date_filters/index.js b/app/javascript/date_filters/index.js index ee892a7fe..432166008 100644 --- a/app/javascript/date_filters/index.js +++ b/app/javascript/date_filters/index.js @@ -3,6 +3,7 @@ import complianceControlSetDF from './compliance_control_set'  import complianceCheckSetDF from './compliance_check_set'  import timetableDF from './time_table'  import importDF from './import' +import purchaseWindowDF from './purchase_window'  import workbenchDF from './workbench'  const DateFilters = { @@ -11,6 +12,7 @@ const DateFilters = {    complianceControlSetDF,    importDF,    timetableDF, +  purchaseWindowDF,    workbenchDF  } diff --git a/app/javascript/date_filters/purchase_window.js b/app/javascript/date_filters/purchase_window.js new file mode 100644 index 000000000..2c46b6d52 --- /dev/null +++ b/app/javascript/date_filters/purchase_window.js @@ -0,0 +1,5 @@ +import DateFilter from '../helpers/date_filters' + +const purchaseWindowDF = new DateFilter("purchase_window_filter_btn", "Tous les champs du filtre de date doivent être remplis", "q_contains_date_NUMi") + +export default purchaseWindowDF
\ No newline at end of file diff --git a/app/javascript/packs/vehicle_journeys/index.js b/app/javascript/packs/vehicle_journeys/index.js index 38431af1d..53c5d5417 100644 --- a/app/javascript/packs/vehicle_journeys/index.js +++ b/app/javascript/packs/vehicle_journeys/index.js @@ -23,6 +23,7 @@ var initialState = {    filters: {      selectedJourneyPatterns : selectedJP,      policy: window.perms, +    features: window.features,      toggleArrivals: false,      queryString: '',      query: { @@ -54,7 +55,7 @@ var initialState = {    },    status: { -    fetchSuccess: true, +    fetchSuccess: false,      isFetching: false    },    vehicleJourneys: [], @@ -99,4 +100,4 @@ render(      <App />    </Provider>,    document.getElementById('vehicle_journeys_wip') -)
\ No newline at end of file +) diff --git a/app/javascript/routes/components/BSelect2.js b/app/javascript/routes/components/BSelect2.js index 340d9df95..0d8d7787f 100644 --- a/app/javascript/routes/components/BSelect2.js +++ b/app/javascript/routes/components/BSelect2.js @@ -96,17 +96,26 @@ class BSelect2 extends Component{              data: function(params) {                return {                  q: params.term, -                target_type: 'zdep' +                scope: 'route_editor'                };              },              processResults: function(data, params) {                return { -                results: data.map( -                  item => _.assign( -                    {}, -                    item, -                    { text: item.name + ", " + item.zip_code + " " + item.short_city_name + " <small><em>(" + item.user_objectid + ")</em></small>" } -                  ) +                 results: data.map( +                  function(item) { +                      var text = item.name; +                      if (item.zip_code || item.short_city_name) { +                          text += "," +                      } +                      if (item.zip_code) { +                          text += ` ${item.zip_code}` +                      } +                      if (item.short_city_name) { +                          text += ` ${item.short_city_name}` +                      } +                      text += ` <small><em>(${item.area_type.toUpperCase()}, ${item.user_objectid})</em></small>`; +                      return _.assign({}, item, { text: text }); +                  }                  )                };              }, diff --git a/app/javascript/routes/form_helper.js b/app/javascript/routes/form_helper.js index 8a3277234..865722fb6 100644 --- a/app/javascript/routes/form_helper.js +++ b/app/javascript/routes/form_helper.js @@ -7,14 +7,14 @@ const formHelper = {      input.setAttribute('name', formatedName)      input.setAttribute('value', value)      form.appendChild(input) -  },  +  },    addError: (ids) => {      ids.forEach((id) => {        if (!$(id).parents('.form-group').hasClass('has-error')) {          $(id).parents('.form-group').addClass('has-error')          $(id).parent().append(`<span class='help-block small'>${'doit être rempli(e)'}</span>`)        } -    })  +    })    },    cleanInputs: (ids) => {      ids.forEach((id) =>{ @@ -28,21 +28,22 @@ const formHelper = {      ids.forEach(id => {        $(id).val() == "" ? blankInputs.push(id) : filledInputs.push(id)      }) -     +      if (filledInputs.length > 0) formHelper.cleanInputs(filledInputs) -    if (blankInputs.length > 0) formHelper.addError(blankInputs)   +    if (blankInputs.length > 0) formHelper.addError(blankInputs)    },    handleStopPoints: (event, state) => {      if (state.stopPoints.length >= 2) {        state.stopPoints.map((stopPoint, i) => {          formHelper.addInput('id', stopPoint.stoppoint_id ? stopPoint.stoppoint_id : '', i)          formHelper.addInput('stop_area_id', stopPoint.stoparea_id, i) -        formHelper.addInput('position', i, i) +        formHelper.addInput('position', stopPoint.index, i)          formHelper.addInput('for_boarding', stopPoint.for_boarding, i)          formHelper.addInput('for_alighting', stopPoint.for_alighting, i)        }) -    if ($('.alert.alert-danger').length > 0) $('.alert.alert-danger').remove() -    } else { +      if ($('.alert.alert-danger').length > 0) $('.alert.alert-danger').remove() +    } +    else {        event.preventDefault()        let msg = "L'itinéraire doit comporter au moins deux arrêts"        if ($('.alert.alert-danger').length == 0) { @@ -52,4 +53,4 @@ const formHelper = {    }  } -export default formHelper
\ No newline at end of file +export default formHelper diff --git a/app/javascript/routes/reducers/stopPoints.js b/app/javascript/routes/reducers/stopPoints.js index eeec06327..0b42b504f 100644 --- a/app/javascript/routes/reducers/stopPoints.js +++ b/app/javascript/routes/reducers/stopPoints.js @@ -38,15 +38,15 @@ const stopPoints = (state = [], action) => {      case 'MOVE_STOP_UP':        return [          ...state.slice(0, action.index - 1), -        _.assign({}, state[action.index], { stoppoint_id: state[action.index - 1].stoppoint_id }), -        _.assign({}, state[action.index - 1], { stoppoint_id: state[action.index].stoppoint_id }), +        _.assign({}, state[action.index], { index: action.index - 1 }), +        _.assign({}, state[action.index - 1], { index: action.index }),          ...state.slice(action.index + 1)        ]      case 'MOVE_STOP_DOWN':        return [          ...state.slice(0, action.index), -        _.assign({}, state[action.index + 1], { stoppoint_id: state[action.index].stoppoint_id }), -        _.assign({}, state[action.index], { stoppoint_id: state[action.index + 1].stoppoint_id }), +        _.assign({}, state[action.index + 1], { index: action.index }), +        _.assign({}, state[action.index], { index: action.index + 1 }),          ...state.slice(action.index + 2)        ]      case 'DELETE_STOP': @@ -141,4 +141,4 @@ const stopPoints = (state = [], action) => {    }  } -export default stopPoints
\ No newline at end of file +export default stopPoints diff --git a/app/javascript/vehicle_journeys/actions/index.js b/app/javascript/vehicle_journeys/actions/index.js index ddb54d615..40c8006f1 100644 --- a/app/javascript/vehicle_journeys/actions/index.js +++ b/app/javascript/vehicle_journeys/actions/index.js @@ -84,7 +84,8 @@ const actions = {      selectedItem:{        id: selectedTT.id,        comment: selectedTT.comment, -      objectid: selectedTT.objectid +      objectid: selectedTT.objectid, +      color: selectedTT.color      }    }),    addSelectedTimetable: () => ({ @@ -99,6 +100,31 @@ const actions = {      vehicleJourneys,      timetables    }), +  openPurchaseWindowsEditModal : (vehicleJourneys) => ({ +    type : 'EDIT_PURCHASE_WINDOWS_VEHICLEJOURNEY_MODAL', +    vehicleJourneys +  }), +  selectPurchaseWindowsModal: (selectedWindow) =>({ +    type: 'SELECT_PURCHASE_WINDOW_MODAL', +    selectedItem:{ +      id: selectedWindow.id, +      name: selectedWindow.name, +      color: selectedWindow.color, +      objectid: selectedWindow.objectid +    } +  }), +  addSelectedPurchaseWindow: () => ({ +    type: 'ADD_SELECTED_PURCHASE_WINDOW' +  }), +  deletePurchaseWindowsModal : (purchaseWindow) => ({ +    type : 'DELETE_PURCHASE_WINDOW_MODAL', +    purchaseWindow +  }), +  editVehicleJourneyPurchaseWindows : (vehicleJourneys, purchase_windows) => ({ +    type: 'EDIT_VEHICLEJOURNEYS_PURCHASE_WINDOWS', +    vehicleJourneys, +    purchase_windows +  }),    openShiftModal : () => ({      type : 'SHIFT_VEHICLEJOURNEY_MODAL'    }), @@ -163,25 +189,17 @@ const actions = {      $(target).parent().removeClass('has-error').children('.help-block').remove()    },    validateFields : (fields) => { -    const test = [] - -    Object.keys(fields).map(function(key) { -      test.push(fields[key].validity.valid) +    let valid = true +    Object.keys(fields).forEach((key) => { +      let field = fields[key] +      if(field.validity && !field.validity.valid){ +        valid = false +        $(field).parent().addClass('has-error').children('.help-block').remove() +        $(field).parent().append("<span class='small help-block'>" + field.validationMessage + "</span>") +      }      }) -    if(test.indexOf(false) >= 0) { -      // Form is invalid -      test.map(function(item, i) { -        if(item == false) { -          const k = Object.keys(fields)[i] -          $(fields[k]).parent().addClass('has-error').children('.help-block').remove() -          $(fields[k]).parent().append("<span class='small help-block'>" + fields[k].validationMessage + "</span>") -        } -      }) -      return false -    } else { -      // Form is valid -      return true -    } + +    return valid    },    toggleArrivals : () => ({      type: 'TOGGLE_ARRIVALS', @@ -313,6 +331,7 @@ const actions = {            let val            for (val of json.vehicle_journeys){              var timeTables = [] +            var purchaseWindows = []              let tt              for (tt of val.time_tables){                timeTables.push({ @@ -322,26 +341,35 @@ const actions = {                  color: tt.color                })              } +            if(val.purchase_windows){ +              for (tt of val.purchase_windows){ +                purchaseWindows.push({ +                  objectid: tt.objectid, +                  name: tt.name, +                  id: tt.id, +                  color: tt.color +                }) +              } +            }              let vjasWithDelta = val.vehicle_journey_at_stops.map((vjas, i) => {                actions.fillEmptyFields(vjas)                return actions.getDelta(vjas)              }) -            vehicleJourneys.push({ -              journey_pattern: val.journey_pattern, -              published_journey_name: val.published_journey_name, -              objectid: val.objectid, -              short_id: val.short_id, -              footnotes: val.footnotes, -              time_tables: timeTables, -              vehicle_journey_at_stops: vjasWithDelta, -              deletable: false, -              selected: false, -              published_journey_name: val.published_journey_name || 'non renseigné', -              published_journey_identifier: val.published_journey_identifier || 'non renseigné', -              company: val.company || 'non renseigné', -              transport_mode: val.route.line.transport_mode || 'undefined', -              transport_submode: val.route.line.transport_submode || 'undefined' -            }) + +            vehicleJourneys.push( +              _.assign({}, val, { +                time_tables: timeTables, +                purchase_windows: purchaseWindows, +                vehicle_journey_at_stops: vjasWithDelta, +                deletable: false, +                selected: false, +                published_journey_name: val.published_journey_name || 'non renseigné', +                published_journey_identifier: val.published_journey_identifier || 'non renseigné', +                company: val.company || {name: 'non renseigné'}, +                transport_mode: val.route.line.transport_mode || 'undefined', +                transport_submode: val.route.line.transport_submode || 'undefined' +              }) +            )            }            window.currentItemsLength = vehicleJourneys.length            dispatch(actions.receiveVehicleJourneys(vehicleJourneys)) @@ -439,6 +467,20 @@ const actions = {      vjas.delta = delta      return vjas    }, +  adjustSchedule: (action, schedule) => { +    // we enforce that the departure time remains after the arrival time +    actions.getDelta(schedule) +    if(schedule.delta < 0){ +      if(action.isDeparture){ +        schedule.arrival_time = schedule.departure_time +      } +      else{ +        schedule.departure_time = schedule.arrival_time +      } +      actions.getDelta(schedule) +    } +    return schedule +  },    getShiftedSchedule: ({departure_time, arrival_time}, additional_time) => {      // We create dummy dates objects to manipulate time more easily      let departureDT = new Date (Date.UTC(2017, 2, 1, parseInt(departure_time.hour), parseInt(departure_time.minute))) @@ -457,10 +499,6 @@ const actions = {          minute: actions.simplePad(newArrivalDT.getUTCMinutes())        }      } -  }, -  escapeWildcardCharacters(search) { -    let newSearch = search.replace(/^_/, "\\_") -    return newSearch.replace(/^%/, "\\%")    }  } diff --git a/app/javascript/vehicle_journeys/components/Filters.js b/app/javascript/vehicle_journeys/components/Filters.js index db6707520..3bc4f7ff7 100644 --- a/app/javascript/vehicle_journeys/components/Filters.js +++ b/app/javascript/vehicle_journeys/components/Filters.js @@ -33,6 +33,7 @@ export default function Filters({filters, pagination, onFilter, onResetFilters,                  onSelect2Timetable={onSelect2Timetable}                  hasRoute={true}                  chunkURL={("/autocomplete_time_tables.json?route_id=" + String(window.route_id))} +                searchKey={"comment_or_objectid_cont_any"}                  filters={filters}                  isFilter={true}                  /> @@ -165,4 +166,4 @@ Filters.propTypes = {    onSelect2Timetable: PropTypes.func.isRequired,    onSelect2JourneyPattern: PropTypes.func.isRequired,    onSelect2VehicleJourney: PropTypes.func.isRequired -}
\ No newline at end of file +} diff --git a/app/javascript/vehicle_journeys/components/SaveVehicleJourneys.js b/app/javascript/vehicle_journeys/components/SaveVehicleJourneys.js index e8c27f92e..8bab5baa9 100644 --- a/app/javascript/vehicle_journeys/components/SaveVehicleJourneys.js +++ b/app/javascript/vehicle_journeys/components/SaveVehicleJourneys.js @@ -14,16 +14,27 @@ export default class SaveVehicleJourneys extends Component{          <div className='row mt-md'>            <div className='col-lg-12 text-right'>              <form className='vehicle_journeys formSubmitr ml-xs' onSubmit={e => {e.preventDefault()}}> -              <button -                className='btn btn-default' -                type='button' -                onClick={e => { -                  e.preventDefault() -                  this.props.editMode ? this.props.onSubmitVehicleJourneys(this.props.dispatch, this.props.vehicleJourneys) : this.props.onEnterEditMode() -                }} -              > -                {this.props.editMode ? "Valider" : "Editer"} -              </button> +              <div className="btn-group sticky-actions"> +                <button +                  className={'btn ' + (this.props.editMode ? 'btn-success' : 'btn-default') + (this.props.status.fetchSuccess ? '' : ' disabled')} +                  type='button' +                  onClick={e => { +                    e.preventDefault() +                    this.props.editMode ? this.props.onSubmitVehicleJourneys(this.props.dispatch, this.props.vehicleJourneys) : this.props.onEnterEditMode() +                  }} +                > +                  {this.props.editMode ? "Valider" : "Editer"} +                </button> +                {this.props.editMode && <button +                    className='btn btn-default' +                    type='button' +                    onClick={e => { +                      e.preventDefault() +                      this.props.onExitEditMode() +                    }} +                  > Annuler </button> +                } +              </div>              </form>            </div>          </div> @@ -38,5 +49,6 @@ SaveVehicleJourneys.propTypes = {    status: PropTypes.object.isRequired,    filters: PropTypes.object.isRequired,    onEnterEditMode: PropTypes.func.isRequired, +  onExitEditMode: PropTypes.func.isRequired,    onSubmitVehicleJourneys: PropTypes.func.isRequired -}
\ No newline at end of file +} diff --git a/app/javascript/vehicle_journeys/components/Tools.js b/app/javascript/vehicle_journeys/components/Tools.js index 7621dfc10..d6e04f00e 100644 --- a/app/javascript/vehicle_journeys/components/Tools.js +++ b/app/javascript/vehicle_journeys/components/Tools.js @@ -7,6 +7,7 @@ import DuplicateVehicleJourney from '../containers/tools/DuplicateVehicleJourney  import EditVehicleJourney from '../containers/tools/EditVehicleJourney'  import NotesEditVehicleJourney from '../containers/tools/NotesEditVehicleJourney'  import TimetablesEditVehicleJourney from '../containers/tools/TimetablesEditVehicleJourney' +import PurchaseWindowsEditVehicleJourney from '../containers/tools/PurchaseWindowsEditVehicleJourney'  export default class Tools extends Component { @@ -17,7 +18,12 @@ export default class Tools extends Component {    hasPolicy(key) {      // Check if the user has the policy to disable or not the action -    return this.props.filters.policy[`vehicle_journeys.${key}`]  +    return this.props.filters.policy[`vehicle_journeys.${key}`] +  } + +  hasFeature(key) { +    // Check if the organisation has the given feature +    return this.props.filters.features[key]    }    render() { @@ -25,13 +31,16 @@ export default class Tools extends Component {      return (        <div className='select_toolbox'>          <ul> -          <AddVehicleJourney disabled={this.hasPolicy("create") && !editMode} /> -          <DuplicateVehicleJourney disabled={this.hasPolicy("create") && this.hasPolicy("update") && !editMode}/> -          <ShiftVehicleJourney disabled={this.hasPolicy("update") && !editMode}/> +          <AddVehicleJourney disabled={!this.hasPolicy("create") || !editMode} /> +          <DuplicateVehicleJourney disabled={!this.hasPolicy("create") || !this.hasPolicy("update") || !editMode}/> +          <ShiftVehicleJourney disabled={!this.hasPolicy("update") || !editMode}/>            <EditVehicleJourney disabled={!this.hasPolicy("update")}/>            <TimetablesEditVehicleJourney disabled={!this.hasPolicy("update")}/> +          { this.hasFeature('purchase_windows') && +            <PurchaseWindowsEditVehicleJourney disabled={!this.hasPolicy("update")}/> +          }            <NotesEditVehicleJourney disabled={!this.hasPolicy("update")}/> -          <DeleteVehicleJourneys disabled={this.hasPolicy("destroy") && !editMode}/> +          <DeleteVehicleJourneys disabled={!this.hasPolicy("destroy") || !editMode}/>          </ul>          <span className='info-msg'>{actions.getSelected(vehicleJourneys).length} course(s) sélectionnée(s)</span> @@ -45,4 +54,4 @@ Tools.propTypes = {    vehicleJourneys : PropTypes.array.isRequired,    onCancelSelection: PropTypes.func.isRequired,    filters: PropTypes.object.isRequired -}
\ No newline at end of file +} diff --git a/app/javascript/vehicle_journeys/components/VehicleJourney.js b/app/javascript/vehicle_journeys/components/VehicleJourney.js index 8fb4b8a7e..5f6281487 100644 --- a/app/javascript/vehicle_journeys/components/VehicleJourney.js +++ b/app/javascript/vehicle_journeys/components/VehicleJourney.js @@ -17,12 +17,25 @@ export default class VehicleJourney extends Component {      return bool    } +  hasFeature(key) { +    return this.props.filters.features[key] +  } +    timeTableURL(tt) {      let refURL = window.location.pathname.split('/', 3).join('/')      let ttURL = refURL + '/time_tables/' + tt.id      return ( -      <a href={ttURL} title='Voir le calendrier'><span className='fa fa-calendar' style={{color: (tt.color ? tt.color : '')}}></span></a> +      <a href={ttURL} title='Voir le calendrier'><span className='fa fa-calendar' style={{ color: (tt.color ? tt.color : '#4B4B4B')}}></span></a> +    ) +  } + +  purchaseWindowURL(tt) { +    let refURL = window.location.pathname.split('/', 3).join('/') +    let ttURL = refURL + '/purchase_windows/' + tt.id + +    return ( +      <a href={ttURL} title='Voir le calendrier commercial'><span className='fa fa-calendar' style={{color: (tt.color ? tt.color : '')}}></span></a>      )    } @@ -44,19 +57,33 @@ export default class VehicleJourney extends Component {    render() {      this.previousCity = undefined -    let {time_tables} = this.props.value +    let {time_tables, purchase_windows} = this.props.value      return (        <div className={'t2e-item' + (this.props.value.deletable ? ' disabled' : '') + (this.props.value.errors ? ' has-error': '')}> -        <div className='th'> -          <div className='strong mb-xs'>{this.props.value.objectid ? this.props.value.short_id : '-'}</div> -          <div>{this.props.value.journey_pattern.short_id}</div> +        <div +          className='th' +          onClick={(e) => +            ($(e.target).parents("a").length == 0) && this.props.onSelectVehicleJourney(this.props.index) +          } +          > +          <div className='strong mb-xs'>{this.props.value.short_id || '-'}</div> +          <div>{this.props.value.published_journey_name && this.props.value.published_journey_name != "non renseigné" ? this.props.value.published_journey_name : '-'}</div> +          <div>{this.props.value.journey_pattern.short_id || '-'}</div>            <div>              {time_tables.slice(0,3).map((tt, i)=>                <span key={i} className='vj_tt'>{this.timeTableURL(tt)}</span>              )}              {time_tables.length > 3 && <span className='vj_tt'> + {time_tables.length - 3}</span>}            </div> +          { this.hasFeature('purchase_windows') && +            <div> +              {purchase_windows.slice(0,3).map((tt, i)=> +                <span key={i} className='vj_tt'>{this.purchaseWindowURL(tt)}</span> +              )} +              {purchase_windows.length > 3 && <span className='vj_tt'> + {purchase_windows.length - 3}</span>} +            </div> +          }            <div className={(this.props.value.deletable ? 'disabled ' : '') + 'checkbox'}>              <input                id={this.props.index} @@ -83,6 +110,7 @@ export default class VehicleJourney extends Component {                        className='form-control'                        disabled={this.isDisabled(this.props.value.deletable, vj.dummy) || this.props.filters.policy['vehicle_journeys.update'] == false}                        readOnly={!this.props.editMode && !vj.dummy} +                      disabled={!this.props.editMode && !vj.dummy}                        onChange={(e) => {this.props.onUpdateTime(e, i, this.props.index, 'hour', false, false)}}                        value={vj.arrival_time['hour']}                        /> @@ -94,6 +122,7 @@ export default class VehicleJourney extends Component {                        className='form-control'                        disabled={this.isDisabled(this.props.value.deletable, vj.dummy) || this.props.filters.policy['vehicle_journeys.update'] == false}                        readOnly={!this.props.editMode && !vj.dummy} +                      disabled={!this.props.editMode && !vj.dummy}                        onChange={(e) => {this.props.onUpdateTime(e, i, this.props.index, 'minute', false, false)}}                        value={vj.arrival_time['minute']}                        /> @@ -114,6 +143,7 @@ export default class VehicleJourney extends Component {                        className='form-control'                        disabled={this.isDisabled(this.props.value.deletable, vj.dummy) || this.props.filters.policy['vehicle_journeys.update'] == false}                        readOnly={!this.props.editMode && !vj.dummy} +                      disabled={!this.props.editMode && !vj.dummy}                        onChange={(e) => {this.props.onUpdateTime(e, i, this.props.index, 'hour', true, this.props.filters.toggleArrivals)}}                        value={vj.departure_time['hour']}                        /> @@ -125,6 +155,7 @@ export default class VehicleJourney extends Component {                        className='form-control'                        disabled={this.isDisabled(this.props.value.deletable, vj.dummy) || this.props.filters.policy['vehicle_journeys.update'] == false}                        readOnly={!this.props.editMode && !vj.dummy} +                      disabled={!this.props.editMode && !vj.dummy}                        onChange={(e) => {this.props.onUpdateTime(e, i, this.props.index, "minute", true,  this.props.filters.toggleArrivals)}}                        value={vj.departure_time['minute']}                        /> @@ -144,4 +175,4 @@ VehicleJourney.propTypes = {    index: PropTypes.number.isRequired,    onUpdateTime: PropTypes.func.isRequired,    onSelectVehicleJourney: PropTypes.func.isRequired -}
\ No newline at end of file +} diff --git a/app/javascript/vehicle_journeys/components/VehicleJourneys.js b/app/javascript/vehicle_journeys/components/VehicleJourneys.js index 6bce9766b..dc480d6b4 100644 --- a/app/javascript/vehicle_journeys/components/VehicleJourneys.js +++ b/app/javascript/vehicle_journeys/components/VehicleJourneys.js @@ -12,6 +12,10 @@ export default class VehicleJourneys extends Component {      this.props.onLoadFirstPage(this.props.filters)    } +  hasFeature(key) { +    return this.props.filters.features[key] +  } +    componentDidUpdate(prevProps, prevState) {      if(this.props.status.isFetching == false){        $('.table-2entries').each(function() { @@ -111,8 +115,10 @@ export default class VehicleJourneys extends Component {                <div className='t2e-head w20'>                  <div className='th'>                    <div className='strong mb-xs'>ID course</div> +                  <div>Nom course</div>                    <div>ID mission</div>                    <div>Calendriers</div> +                  { this.hasFeature('purchase_windows') && <div>Calendriers Commerciaux</div> }                  </div>                  {this.props.stopPointsList.map((sp, i) =>{                    return ( @@ -132,6 +138,7 @@ export default class VehicleJourneys extends Component {                        index={index}                        editMode={this.props.editMode}                        filters={this.props.filters} +                      features={this.props.features}                        onUpdateTime={this.props.onUpdateTime}                        onSelectVehicleJourney={this.props.onSelectVehicleJourney}                        /> @@ -153,4 +160,4 @@ VehicleJourneys.propTypes = {    onLoadFirstPage: PropTypes.func.isRequired,    onUpdateTime: PropTypes.func.isRequired,    onSelectVehicleJourney: PropTypes.func.isRequired -}
\ No newline at end of file +} diff --git a/app/javascript/vehicle_journeys/components/tools/CreateModal.js b/app/javascript/vehicle_journeys/components/tools/CreateModal.js index 2bffebdf6..cd593cdff 100644 --- a/app/javascript/vehicle_journeys/components/tools/CreateModal.js +++ b/app/javascript/vehicle_journeys/components/tools/CreateModal.js @@ -9,7 +9,7 @@ export default class CreateModal extends Component {    }    handleSubmit() { -    if(actions.validateFields(this.refs) == true && this.props.modal.modalProps.selectedJPModal) { +    if (actions.validateFields(...this.refs, $('.vjCreateSelectJP')[0]) && this.props.modal.modalProps.selectedJPModal) {        this.props.onAddVehicleJourney(this.refs, this.props.modal.modalProps.selectedJPModal, this.props.stopPointsList, this.props.modal.modalProps.selectedCompany)        this.props.onModalClose()        $('#NewVehicleJourneyModal').modal('hide') @@ -61,7 +61,7 @@ export default class CreateModal extends Component {                              <div className='form-group'>                                <label className='control-label'>Nom du transporteur</label>                                <CompanySelect2 -                                company = {undefined} +                                company = {this.props.modal.modalProps.vehicleJourney && this.props.modal.modalProps.vehicleJourney.company || undefined}                                  onSelect2Company = {(e) => this.props.onSelect2Company(e)}                                />                              </div> @@ -130,4 +130,4 @@ CreateModal.propTypes = {    onAddVehicleJourney: PropTypes.func.isRequired,    onSelect2JourneyPattern: PropTypes.func.isRequired,    disabled: PropTypes.bool.isRequired -}
\ No newline at end of file +} diff --git a/app/javascript/vehicle_journeys/components/tools/EditVehicleJourney.js b/app/javascript/vehicle_journeys/components/tools/EditVehicleJourney.js index 7d91896eb..f8d6add03 100644 --- a/app/javascript/vehicle_journeys/components/tools/EditVehicleJourney.js +++ b/app/javascript/vehicle_journeys/components/tools/EditVehicleJourney.js @@ -97,6 +97,7 @@ export default class EditVehicleJourney extends Component {                              <div className='form-group'>                                <label className='control-label'>Transporteur</label>                                <CompanySelect2 +                                editModal={this.props.modal.type == "edit"}                                  editMode={this.props.editMode}                                  company = {this.props.modal.modalProps.vehicleJourney.company}                                  onSelect2Company = {(e) => this.props.onSelect2Company(e)} diff --git a/app/javascript/vehicle_journeys/components/tools/PurchaseWindowsEditVehicleJourney.js b/app/javascript/vehicle_journeys/components/tools/PurchaseWindowsEditVehicleJourney.js new file mode 100644 index 000000000..d61c7a34b --- /dev/null +++ b/app/javascript/vehicle_journeys/components/tools/PurchaseWindowsEditVehicleJourney.js @@ -0,0 +1,152 @@ +import React, { PropTypes, Component } from 'react' +import actions from '../../actions' +import TimetableSelect2 from './select2s/TimetableSelect2' + +export default class PurchaseWindowsEditVehicleJourney extends Component { +  constructor(props) { +    super(props) +    this.handleSubmit = this.handleSubmit.bind(this) +    this.purchaseWindowURL = this.purchaseWindowURL.bind(this) +  } + +  handleSubmit() { +    this.props.onShoppingWindowsEditVehicleJourney(this.props.modal.modalProps.vehicleJourneys, this.props.modal.modalProps.purchase_windows) +    this.props.onModalClose() +    $('#PurchaseWindowsEditVehicleJourneyModal').modal('hide') +  } + +  purchaseWindowURL(tt) { +    let refURL = window.location.pathname.split('/', 3).join('/') +    return refURL + '/purchase_windows/' + tt.id +  } + +  render() { +    if(this.props.status.isFetching == true) { +      return false +    } +    if(this.props.status.fetchSuccess == true) { +      return ( +        <li className='st_action'> +          <button +            type='button' +            disabled={(actions.getSelected(this.props.vehicleJourneys).length < 1 || this.props.disabled)} +            data-toggle='modal' +            data-target='#PurchaseWindowsEditVehicleJourneyModal' +            onClick={() => this.props.onOpenCalendarsEditModal(actions.getSelected(this.props.vehicleJourneys))} +            title='Calendriers commerciaux' +          > +            <span className='sb sb-purchase_window sb-strong'></span> +          </button> + +          <div className={ 'modal fade ' + ((this.props.modal.type == 'duplicate') ? 'in' : '') } id='PurchaseWindowsEditVehicleJourneyModal'> +            <div className='modal-container'> +              <div className='modal-dialog'> +                <div className='modal-content'> +                  <div className='modal-header'> +                    <h4 className='modal-title'>Calendriers commerciaux associés</h4> +                    <span type="button" className="close modal-close" data-dismiss="modal">×</span> +                  </div> + +                  {(this.props.modal.type == 'purchase_windows_edit') && ( +                    <form> +                      <div className='modal-body'> +                        <div className='row'> +                          <div className='col-lg-12'> +                            <div className='subform'> +                              <div className='nested-head'> +                                <div className='wrapper'> +                                  <div> +                                    <div className='form-group'> +                                      <label className='control-label'>{this.props.modal.modalProps.purchase_windows.length == 0 ? "Aucun calendrier commercial associé" : "Calendriers commerciaux associés"}</label> +                                    </div> +                                  </div> +                                  <div></div> +                                </div> +                              </div> +                              {this.props.modal.modalProps.purchase_windows.map((tt, i) => +                                <div className='nested-fields' key={i}> +                                  <div className='wrapper'> +                                    <div> <a href={this.purchaseWindowURL(tt)} target="_blank"> +                                      <span className="fa fa-circle mr-xs" style={{color: tt.color}}></span> +                                      {tt.name} +                                    </a> </div> +                                    { +                                      this.props.editMode && +                                      <div> +                                        <a +                                          href='#' +                                          title='Supprimer' +                                          className='fa fa-trash remove_fields' +                                          style={{ height: 'auto', lineHeight: 'normal' }} +                                          onClick={(e) => { +                                            e.preventDefault() +                                            this.props.onDeleteCalendarModal(tt) +                                          }} +                                        ></a> +                                      </div> +                                    } +                                  </div> +                                </div> +                              )} +                              { +                                this.props.editMode && +                                <div className='nested-fields'> +                                  <div className='wrapper'> +                                    <div> +                                      <TimetableSelect2 +                                        onSelect2Timetable={this.props.onSelect2Timetable} +                                        chunkURL={'/autocomplete_purchase_windows.json'} +                                        searchKey={"name_or_objectid_cont_any"} +                                        isFilter={false} +                                      /> +                                    </div> +                                  </div> +                                </div> +                              } +                            </div> +                          </div> +                        </div> +                      </div> +                      { +                        this.props.editMode && +                        <div className='modal-footer'> +                          <button +                            className='btn btn-link' +                            data-dismiss='modal' +                            type='button' +                            onClick={this.props.onModalClose} +                          > +                            Annuler +                          </button> +                          <button +                            className='btn btn-primary' +                            type='button' +                            onClick={this.handleSubmit} +                          > +                            Valider +                          </button> +                        </div> +                      } +                    </form> +                  )} + +                </div> +              </div> +            </div> +          </div> +        </li> +      ) +    } else { +      return false +    } +  } +} + +PurchaseWindowsEditVehicleJourney.propTypes = { +  onOpenCalendarsEditModal: PropTypes.func.isRequired, +  onModalClose: PropTypes.func.isRequired, +  onShoppingWindowsEditVehicleJourney: PropTypes.func.isRequired, +  onDeleteCalendarModal: PropTypes.func.isRequired, +  onSelect2Timetable: PropTypes.func.isRequired, +  disabled: PropTypes.bool.isRequired +} diff --git a/app/javascript/vehicle_journeys/components/tools/TimetablesEditVehicleJourney.js b/app/javascript/vehicle_journeys/components/tools/TimetablesEditVehicleJourney.js index fef3cdcc9..fdaa5aeed 100644 --- a/app/javascript/vehicle_journeys/components/tools/TimetablesEditVehicleJourney.js +++ b/app/javascript/vehicle_journeys/components/tools/TimetablesEditVehicleJourney.js @@ -29,10 +29,11 @@ export default class TimetablesEditVehicleJourney extends Component {          <li className='st_action'>            <button              type='button' -            disabled={(actions.getSelected(this.props.vehicleJourneys).length != 1 || this.props.disabled)} +            disabled={(actions.getSelected(this.props.vehicleJourneys).length < 1 || this.props.disabled)}              data-toggle='modal'              data-target='#CalendarsEditVehicleJourneyModal'              onClick={() => this.props.onOpenCalendarsEditModal(actions.getSelected(this.props.vehicleJourneys))} +            title='Calendriers'            >              <span className='fa fa-calendar'></span>            </button> @@ -56,7 +57,7 @@ export default class TimetablesEditVehicleJourney extends Component {                                  <div className='wrapper'>                                    <div>                                      <div className='form-group'> -                                      <label className='control-label'>Calendriers associés</label> +                                      <label className='control-label'>{this.props.modal.modalProps.timetables.length == 0 ? "Aucun calendrier associé" : "Calendriers associés"}</label>                                      </div>                                    </div>                                    <div></div> @@ -65,9 +66,14 @@ export default class TimetablesEditVehicleJourney extends Component {                                {this.props.modal.modalProps.timetables.map((tt, i) =>                                  <div className='nested-fields' key={i}>                                    <div className='wrapper'> -                                    <div> <a href={this.timeTableURL(tt)} target="_blank">{tt.comment}</a> </div> +                                    <div> +                                      <a href={this.timeTableURL(tt)} target="_blank"> +                                        <span className="fa fa-circle mr-xs" style={{color: tt.color || 'black'}}></span> +                                        {tt.comment} +                                      </a> +                                    </div>                                      { -                                      this.props.editMode &&  +                                      this.props.editMode &&                                        <div>                                          <a                                            href='#' @@ -85,13 +91,14 @@ export default class TimetablesEditVehicleJourney extends Component {                                  </div>                                )}                                { -                                this.props.editMode &&  +                                this.props.editMode &&                                  <div className='nested-fields'>                                    <div className='wrapper'>                                      <div>                                        <TimetableSelect2                                          onSelect2Timetable={this.props.onSelect2Timetable}                                          chunkURL={'/autocomplete_time_tables.json'} +                                        searchKey={"comment_or_objectid_cont_any"}                                          isFilter={false}                                        />                                      </div> @@ -103,7 +110,7 @@ export default class TimetablesEditVehicleJourney extends Component {                          </div>                        </div>                        { -                        this.props.editMode &&  +                        this.props.editMode &&                          <div className='modal-footer'>                            <button                              className='btn btn-link' @@ -144,4 +151,4 @@ TimetablesEditVehicleJourney.propTypes = {    onDeleteCalendarModal: PropTypes.func.isRequired,    onSelect2Timetable: PropTypes.func.isRequired,    disabled: PropTypes.bool.isRequired -}
\ No newline at end of file +} diff --git a/app/javascript/vehicle_journeys/components/tools/select2s/CompanySelect2.js b/app/javascript/vehicle_journeys/components/tools/select2s/CompanySelect2.js index 0697e9141..79ba8f094 100644 --- a/app/javascript/vehicle_journeys/components/tools/select2s/CompanySelect2.js +++ b/app/javascript/vehicle_journeys/components/tools/select2s/CompanySelect2.js @@ -21,7 +21,7 @@ export default class BSelect4 extends Component {          value={(this.props.company) ? this.props.company.name : undefined}          onSelect={(e) => this.props.onSelect2Company(e) }          onUnselect={() => this.props.onUnselect2Company()} -        disabled={!this.props.editMode} +        disabled={!this.props.editMode && this.props.editModal}          multiple={false}          ref='company_id'          options={{ @@ -36,7 +36,7 @@ export default class BSelect4 extends Component {              delay: '500',              data: function(params) {                return { -                q: { name_cont: actions.escapeWildcardCharacters(params.term)}, +                q: { name_cont: params.term},                };              },              processResults: function(data, params) { diff --git a/app/javascript/vehicle_journeys/components/tools/select2s/MissionSelect2.js b/app/javascript/vehicle_journeys/components/tools/select2s/MissionSelect2.js index 5b4ae564c..fa847886c 100644 --- a/app/javascript/vehicle_journeys/components/tools/select2s/MissionSelect2.js +++ b/app/javascript/vehicle_journeys/components/tools/select2s/MissionSelect2.js @@ -21,6 +21,8 @@ export default class BSelect4 extends Component {          onSelect={(e) => this.props.onSelect2JourneyPattern(e)}          multiple={false}          ref='journey_pattern_id' +        className={!this.props.isFilter ? "vjCreateSelectJP" : null} +        required={!this.props.isFilter}          options={{            allowClear: false,            theme: 'bootstrap', @@ -33,7 +35,7 @@ export default class BSelect4 extends Component {              delay: '500',              data: function(params) {                return { -                q: { published_name_or_objectid_or_registration_number_cont: actions.escapeWildcardCharacters(params.term)}, +                q: { published_name_or_objectid_or_registration_number_cont: params.term},                };              },              processResults: function(data, params) { diff --git a/app/javascript/vehicle_journeys/components/tools/select2s/TimetableSelect2.js b/app/javascript/vehicle_journeys/components/tools/select2s/TimetableSelect2.js index a90a9f7b8..eb8651be2 100644 --- a/app/javascript/vehicle_journeys/components/tools/select2s/TimetableSelect2.js +++ b/app/javascript/vehicle_journeys/components/tools/select2s/TimetableSelect2.js @@ -31,12 +31,10 @@ export default class BSelect4 extends Component {              url: origin + path + this.props.chunkURL,              dataType: 'json',              delay: '500', -            data: function(params) { -              return { -                q: { -                  comment_or_objectid_cont_any: actions.escapeWildcardCharacters(params.term) -                } -              }; +            data: (params) => { +              let q = {} +              q[this.props.searchKey] = params.term +              return {q}              },              processResults: function(data, params) {                return { @@ -44,7 +42,7 @@ export default class BSelect4 extends Component {                    item => _.assign(                      {},                      item, -                    {text: '<strong>' + "<span class='fa fa-circle' style='color:" + (item.color ? item.color : '#4B4B4B') + "'></span> " + item.comment + ' - ' + item.short_id + '</strong><br/><small>' + (item.day_types ? item.day_types.match(/[A-Z]?[a-z]+/g).join(', ') : "") + '</small>'} +                    {text: '<strong>' + "<span class='fa fa-circle' style='color:" + (item.color ? item.color : '#4B4B4B') + "'></span> " + (item.comment || item.name) + ' - ' + item.short_id + '</strong><br/><small>' + (item.day_types ? item.day_types.match(/[A-Z]?[a-z]+/g).join(', ') : "") + '</small>'}                    )                  )                }; @@ -62,4 +60,4 @@ export default class BSelect4 extends Component {  const formatRepo = (props) => {    if(props.text) return props.text -}
\ No newline at end of file +} diff --git a/app/javascript/vehicle_journeys/components/tools/select2s/VJSelect2.js b/app/javascript/vehicle_journeys/components/tools/select2s/VJSelect2.js index 37628fce0..b063abeca 100644 --- a/app/javascript/vehicle_journeys/components/tools/select2s/VJSelect2.js +++ b/app/javascript/vehicle_journeys/components/tools/select2s/VJSelect2.js @@ -33,7 +33,7 @@ export default class BSelect4b extends Component {              delay: '500',              data: function(params) {                return { -                q: { objectid_cont: actions.escapeWildcardCharacters(params.term)}, +                q: { objectid_cont: params.term},                };              },              processResults: function(data, params) { diff --git a/app/javascript/vehicle_journeys/containers/SaveVehicleJourneys.js b/app/javascript/vehicle_journeys/containers/SaveVehicleJourneys.js index 18f9e994e..f5f879ed8 100644 --- a/app/javascript/vehicle_journeys/containers/SaveVehicleJourneys.js +++ b/app/javascript/vehicle_journeys/containers/SaveVehicleJourneys.js @@ -17,6 +17,10 @@ const mapDispatchToProps = (dispatch) => {      onEnterEditMode: () => {        dispatch(actions.enterEditMode())      }, +    onExitEditMode: () => { +      dispatch(actions.cancelSelection()) +      dispatch(actions.exitEditMode()) +    },      onSubmitVehicleJourneys: (next, state) => {        actions.submitVehicleJourneys(dispatch, state, next)      } diff --git a/app/javascript/vehicle_journeys/containers/tools/PurchaseWindowsEditVehicleJourney.js b/app/javascript/vehicle_journeys/containers/tools/PurchaseWindowsEditVehicleJourney.js new file mode 100644 index 000000000..3fef44489 --- /dev/null +++ b/app/javascript/vehicle_journeys/containers/tools/PurchaseWindowsEditVehicleJourney.js @@ -0,0 +1,38 @@ +import actions from '../../actions' +import { connect } from 'react-redux' +import PurchaseWindowsEditVehicleJourneyComponent from '../../components/tools/PurchaseWindowsEditVehicleJourney' + +const mapStateToProps = (state, ownProps) => { +  return { +    editMode: state.editMode, +    modal: state.modal, +    vehicleJourneys: state.vehicleJourneys, +    status: state.status, +    disabled: ownProps.disabled +  } +} + +const mapDispatchToProps = (dispatch) => { +  return { +    onModalClose: () =>{ +      dispatch(actions.closeModal()) +    }, +    onOpenCalendarsEditModal: (vehicleJourneys) =>{ +      dispatch(actions.openPurchaseWindowsEditModal(vehicleJourneys)) +    }, +    onDeleteCalendarModal: (timetable) => { +      dispatch(actions.deletePurchaseWindowsModal(timetable)) +    }, +    onShoppingWindowsEditVehicleJourney: (vehicleJourneys, timetables) =>{ +      dispatch(actions.editVehicleJourneyPurchaseWindows(vehicleJourneys, timetables)) +    }, +    onSelect2Timetable: (e) =>{ +      dispatch(actions.selectPurchaseWindowsModal(e.params.data)) +      dispatch(actions.addSelectedPurchaseWindow()) +    } +  } +} + +const PurchaseWindowsEditVehicleJourney = connect(mapStateToProps, mapDispatchToProps)(PurchaseWindowsEditVehicleJourneyComponent) + +export default PurchaseWindowsEditVehicleJourney diff --git a/app/javascript/vehicle_journeys/reducers/modal.js b/app/javascript/vehicle_journeys/reducers/modal.js index 57f54a144..eae3314e8 100644 --- a/app/javascript/vehicle_journeys/reducers/modal.js +++ b/app/javascript/vehicle_journeys/reducers/modal.js @@ -1,6 +1,6 @@  import _ from 'lodash' -let vehicleJourneysModal, newModalProps +let vehicleJourneysModal, newModalProps, vehicleJourney  export default function modal(state = {}, action) {    switch (action.type) { @@ -40,7 +40,6 @@ export default function modal(state = {}, action) {      case 'EDIT_CALENDARS_VEHICLEJOURNEY_MODAL':        vehicleJourneysModal = JSON.parse(JSON.stringify(action.vehicleJourneys))        let uniqTimetables = [] -      let timetable = {}        vehicleJourneysModal.map((vj, i) => {          vj.time_tables.map((tt, j) =>{            if(!(_.find(uniqTimetables, tt))){ @@ -56,15 +55,38 @@ export default function modal(state = {}, action) {          },          confirmModal: {}        } +    case 'EDIT_PURCHASE_WINDOWS_VEHICLEJOURNEY_MODAL': +      var vehicleJourneys = JSON.parse(JSON.stringify(action.vehicleJourneys)) +      let uniqPurchaseWindows = [] +      vehicleJourneys.map((vj, i) => { +        vj.purchase_windows.map((pw, j) =>{ +          if(!(_.find(uniqPurchaseWindows, pw))){ +            uniqPurchaseWindows.push(pw) +          } +        }) +      }) +      return { +        type: 'purchase_windows_edit', +        modalProps: { +          vehicleJourneys: vehicleJourneys, +          purchase_windows: uniqPurchaseWindows +        }, +        confirmModal: {} +      }      case 'SELECT_CP_EDIT_MODAL': -      newModalProps = _.assign({}, state.modalProps, {selectedCompany : action.selectedItem}) +      vehicleJourney =  _.assign({}, state.modalProps.vehicleJourney, {company: action.selectedItem}) +      newModalProps = _.assign({}, state.modalProps, {vehicleJourney})        return _.assign({}, state, {modalProps: newModalProps})      case 'UNSELECT_CP_EDIT_MODAL': -      newModalProps = _.assign({}, state.modalProps, {selectedCompany : undefined}) +      vehicleJourney =  _.assign({}, state.modalProps.vehicleJourney, {company: undefined}) +      newModalProps = _.assign({}, state.modalProps, {vehicleJourney})        return _.assign({}, state, {modalProps: newModalProps})      case 'SELECT_TT_CALENDAR_MODAL':        newModalProps = _.assign({}, state.modalProps, {selectedTimetable : action.selectedItem})        return _.assign({}, state, {modalProps: newModalProps}) +    case 'SELECT_PURCHASE_WINDOW_MODAL': +      newModalProps = _.assign({}, state.modalProps, {selectedPurchaseWindow : action.selectedItem}) +      return _.assign({}, state, {modalProps: newModalProps})      case 'ADD_SELECTED_TIMETABLE':        if(state.modalProps.selectedTimetable){          newModalProps = JSON.parse(JSON.stringify(state.modalProps)) @@ -73,6 +95,14 @@ export default function modal(state = {}, action) {          }          return _.assign({}, state, {modalProps: newModalProps})        } +    case 'ADD_SELECTED_PURCHASE_WINDOW': +      if(state.modalProps.selectedPurchaseWindow){ +        newModalProps = JSON.parse(JSON.stringify(state.modalProps)) +        if (!_.find(newModalProps.purchase_windows, newModalProps.selectedPurchaseWindow)){ +          newModalProps.purchase_windows.push(newModalProps.selectedPurchaseWindow) +        } +        return _.assign({}, state, {modalProps: newModalProps}) +      }      case 'DELETE_CALENDAR_MODAL':        newModalProps = JSON.parse(JSON.stringify(state.modalProps))        let timetablesModal = state.modalProps.timetables.slice(0) @@ -92,6 +122,25 @@ export default function modal(state = {}, action) {        newModalProps.vehicleJourneys = vehicleJourneysModal        newModalProps.timetables = timetablesModal        return _.assign({}, state, {modalProps: newModalProps}) +    case 'DELETE_PURCHASE_WINDOW_MODAL': +        newModalProps = JSON.parse(JSON.stringify(state.modalProps)) +        let purchase_windows = state.modalProps.purchase_windows.slice(0) +        purchase_windows.map((tt, i) =>{ +          if(tt == action.purchaseWindow){ +            purchase_windows.splice(i, 1) +          } +        }) +        vehicleJourneysModal = state.modalProps.vehicleJourneys.slice(0) +        vehicleJourneysModal.map((vj) =>{ +          vj.purchase_windows.map((tt, i) =>{ +            if (_.isEqual(tt, action.purchaseWindow)){ +              vj.purchase_windows.splice(i, 1) +            } +          }) +        }) +        newModalProps.vehicleJourneys = vehicleJourneysModal +        newModalProps.purchase_windows = purchase_windows +        return _.assign({}, state, {modalProps: newModalProps})      case 'CREATE_VEHICLEJOURNEY_MODAL':        let selectedJP = {}        if (window.jpOrigin){ @@ -135,4 +184,4 @@ export default function modal(state = {}, action) {      default:        return state    } -}
\ No newline at end of file +} diff --git a/app/javascript/vehicle_journeys/reducers/vehicleJourneys.js b/app/javascript/vehicle_journeys/reducers/vehicleJourneys.js index 775fefdca..d057bf704 100644 --- a/app/javascript/vehicle_journeys/reducers/vehicleJourneys.js +++ b/app/javascript/vehicle_journeys/reducers/vehicleJourneys.js @@ -38,8 +38,10 @@ const vehicleJourney= (state = {}, action, keep) => {          published_journey_name: action.data.published_journey_name.value,          published_journey_identifier: action.data.published_journey_identifier.value,          objectid: '', +        short_id: '',          footnotes: [],          time_tables: [], +        purchase_windows: [],          vehicle_journey_at_stops: pristineVjasList,          selected: false,          deletable: false, @@ -78,18 +80,12 @@ const vehicleJourney= (state = {}, action, keep) => {            if (action.isDeparture){              newSchedule.departure_time[action.timeUnit] = actions.pad(action.val, action.timeUnit)              if(!action.isArrivalsToggled) -              newSchedule.arrival_time[action.timeUnit] = actions.pad(action.val, action.timeUnit) -            newSchedule = actions.getDelta(newSchedule) -            if(newSchedule.delta < 0){ -              return vjas -            } +              newSchedule.arrival_time[action.timeUnit] = newSchedule.departure_time[action.timeUnit] +            newSchedule = actions.adjustSchedule(action, newSchedule)              return _.assign({}, state.vehicle_journey_at_stops[action.subIndex], {arrival_time: newSchedule.arrival_time, departure_time: newSchedule.departure_time, delta: newSchedule.delta})            }else{              newSchedule.arrival_time[action.timeUnit] = actions.pad(action.val, action.timeUnit) -            newSchedule = actions.getDelta(newSchedule) -            if(newSchedule.delta < 0){ -              return vjas -            } +            newSchedule = actions.adjustSchedule(action, newSchedule)              return _.assign({}, state.vehicle_journey_at_stops[action.subIndex],  {arrival_time: newSchedule.arrival_time, departure_time: newSchedule.departure_time, delta: newSchedule.delta})            }          }else{ @@ -160,6 +156,21 @@ export default function vehicleJourneys(state = [], action) {            return vj          }        }) +      case 'EDIT_VEHICLEJOURNEYS_PURCHASE_WINDOWS': +        let newWindows = JSON.parse(JSON.stringify(action.purchase_windows)) +        return state.map((vj,i) =>{ +          if(vj.selected){ +            let updatedVJ = _.assign({}, vj) +            action.vehicleJourneys.map((vjm, j) =>{ +              if(vj.objectid == vjm.objectid){ +                updatedVJ.purchase_windows = newWindows +              } +            }) +            return updatedVJ +          }else{ +            return vj +          } +        })      case 'SHIFT_VEHICLEJOURNEY':        return state.map((vj, i) => {          if (vj.selected){ @@ -224,4 +235,4 @@ export default function vehicleJourneys(state = [], action) {      default:        return state    } -}
\ No newline at end of file +} diff --git a/app/models/calendar.rb b/app/models/calendar.rb index b2e73929f..34ed51374 100644 --- a/app/models/calendar.rb +++ b/app/models/calendar.rb @@ -3,22 +3,19 @@ require_relative 'calendar/date_value'  require_relative 'calendar/period'  class Calendar < ActiveRecord::Base +  include DateSupport +  include PeriodSupport +    has_paper_trail    belongs_to :organisation -  has_many :time_tables    validates_presence_of :name, :short_name, :organisation    validates_uniqueness_of :short_name -  after_initialize :init_dates_and_date_ranges +  has_many :time_tables    scope :contains_date, ->(date) { where('date ? = any (dates) OR date ? <@ any (date_ranges)', date, date) } -  def init_dates_and_date_ranges -    self.dates ||= [] -    self.date_ranges ||= [] -  end -    def self.ransackable_scopes(auth_object = nil)      [:contains_date]    end @@ -35,144 +32,4 @@ class Calendar < ActiveRecord::Base      end    end - -  ### Calendar::Period -  # Required by coocon -  def build_period -    Calendar::Period.new -  end - -  def periods -    @periods ||= init_periods -  end - -  def init_periods -    (date_ranges || []) -      .each_with_index -      .map( &Calendar::Period.method(:from_range) ) -  end -  private :init_periods - -  validate :validate_periods - -  def validate_periods -    periods_are_valid = periods.all?(&:valid?) - -    periods.each do |period| -      if period.intersect?(periods) -        period.errors.add(:base, I18n.t('calendars.errors.overlapped_periods')) -        periods_are_valid = false -      end -    end - -    unless periods_are_valid -      errors.add(:periods, :invalid) -    end -  end - -  def flatten_date_array attributes, key -    date_int = %w(1 2 3).map {|e| attributes["#{key}(#{e}i)"].to_i } -    Date.new(*date_int) -  end - -  def periods_attributes=(attributes = {}) -    @periods = [] -    attributes.each do |index, period_attribute| -      # Convert date_select to date -      ['begin', 'end'].map do |attr| -        period_attribute[attr] = flatten_date_array(period_attribute, attr) -      end -      period = Calendar::Period.new(period_attribute.merge(id: index)) -      @periods << period unless period.marked_for_destruction? -    end - -    date_ranges_will_change! -  end - -  before_validation :fill_date_ranges - -  def fill_date_ranges -    if @periods -      self.date_ranges = @periods.map(&:range).compact.sort_by(&:begin) -    end -  end - -  after_save :clear_periods - -  def clear_periods -    @periods = nil -  end - -  private :clear_periods - -  ### Calendar::DateValue - -  # Required by coocon -  def build_date_value -    Calendar::DateValue.new -  end - -  def date_values -    @date_values ||= init_date_values -  end - -  def init_date_values -    if dates -      dates.each_with_index.map { |d, index| Calendar::DateValue.from_date(index, d) } -    else -      [] -    end -  end -  private :init_date_values - -  validate :validate_date_values - -  def validate_date_values -    date_values_are_valid = date_values.all?(&:valid?) - -    date_values.each do |date_value| -      if date_values.count { |d| d.value == date_value.value } > 1 -        date_value.errors.add(:base, I18n.t('activerecord.errors.models.calendar.attributes.dates.date_in_dates')) -        date_values_are_valid = false -      end -      date_ranges.each do |date_range| -        if date_range.cover? date_value.value -          date_value.errors.add(:base, I18n.t('activerecord.errors.models.calendar.attributes.dates.date_in_date_ranges')) -          date_values_are_valid = false -        end -      end -    end - -    unless date_values_are_valid -      errors.add(:date_values, :invalid) -    end -  end - -  def date_values_attributes=(attributes = {}) -    @date_values = [] -    attributes.each do |index, date_value_attribute| -      date_value_attribute['value'] = flatten_date_array(date_value_attribute, 'value') -      date_value = Calendar::DateValue.new(date_value_attribute.merge(id: index)) -      @date_values << date_value unless date_value.marked_for_destruction? -    end - -    dates_will_change! -  end - -  before_validation :fill_dates - -  def fill_dates -    if @date_values -      self.dates = @date_values.map(&:value).compact.sort -    end -  end - -  after_save :clear_date_values - -  def clear_date_values -    @date_values = nil -  end - -  private :clear_date_values -  end diff --git a/app/models/calendar/period.rb b/app/models/calendar/period.rb index 1c423dfcc..56ab722fe 100644 --- a/app/models/calendar/period.rb +++ b/app/models/calendar/period.rb @@ -1,5 +1,5 @@  class Calendar < ActiveRecord::Base -   +    class Period      include ActiveAttr::Model diff --git a/app/models/chouette/area_type.rb b/app/models/chouette/area_type.rb new file mode 100644 index 000000000..43d96b391 --- /dev/null +++ b/app/models/chouette/area_type.rb @@ -0,0 +1,42 @@ +class Chouette::AreaType +  include Comparable + +  ALL = %i(zdep zder zdlp zdlr lda gdl).freeze + +  @@all = ALL +  mattr_accessor :all + +  def self.all=(values) +    @@all = ALL & values +    reset_caches! +  end + +  @@instances = {} +  def self.find(code) +    code = code.to_sym +    @@instances[code] ||= new(code) if ALL.include? code +  end + +  def self.reset_caches! +    @@instances = {} +    @@options = nil +  end + +  def self.options +    @@options ||= all.map { |c| find(c) }.map { |t| [ t.label, t.code ] } +  end + +  attr_reader :code +  def initialize(code) +    @code = code +  end + +  def <=>(other) +    all.index(code) <=> all.index(other.code) +  end + +  def label +    I18n.translate code, scope: 'area_types.label' +  end + +end diff --git a/app/models/chouette/line.rb b/app/models/chouette/line.rb index 784e3f5b9..2d776e94b 100644 --- a/app/models/chouette/line.rb +++ b/app/models/chouette/line.rb @@ -72,12 +72,23 @@ module Chouette      end      def display_name -      [self.get_objectid.local_id, number, name, company.try(:name)].compact.join(' - ') +      [self.get_objectid.short_id, number, name, company.try(:name)].compact.join(' - ')      end      def companies        line_referential.companies.where(id: ([company_id] + Array(secondary_company_ids)).compact)      end +    def deactivate! +      update_attribute :deactivated, true +    end + +    def activate! +      update_attribute :deactivated, false +    end + +    def activated? +      !deactivated +    end    end  end diff --git a/app/models/chouette/purchase_window.rb b/app/models/chouette/purchase_window.rb new file mode 100644 index 000000000..e89a0ec7f --- /dev/null +++ b/app/models/chouette/purchase_window.rb @@ -0,0 +1,36 @@ +require 'range_ext' +require_relative '../calendar/period' + +module Chouette +  class PurchaseWindow < Chouette::TridentActiveRecord +    # include ChecksumSupport +    include ObjectidSupport +    include PeriodSupport +    extend Enumerize +    enumerize :color, in: %w(#9B9B9B #FFA070 #C67300 #7F551B #41CCE3 #09B09C #3655D7 #6321A0 #E796C6 #DD2DAA) + +    has_paper_trail +    belongs_to :referential +    has_and_belongs_to_many :vehicle_journeys, :class_name => 'Chouette::VehicleJourney' + +    validates_presence_of :name, :referential + +    scope :contains_date, ->(date) { where('date ? <@ any (date_ranges)', date) } +     +    def self.ransackable_scopes(auth_object = nil) +      [:contains_date] +    end + +    def self.colors_i18n +      Hash[*color.values.map{|c| [I18n.t("enumerize.purchase_window.color.#{c[1..-1]}"), c]}.flatten] +    end + +    def local_id +      "IBOO-#{self.referential.id}-#{self.id}" +    end + +    # def checksum_attributes +    # end + +  end +end diff --git a/app/models/chouette/stop_area.rb b/app/models/chouette/stop_area.rb index cc7170728..4f1359ff8 100644 --- a/app/models/chouette/stop_area.rb +++ b/app/models/chouette/stop_area.rb @@ -9,7 +9,7 @@ module Chouette      include ObjectidSupport      extend Enumerize -    enumerize :area_type, in: %i(zdep zder zdlp zdlr lda) +    enumerize :area_type, in: Chouette::AreaType::ALL      with_options dependent: :destroy do |assoc|        assoc.has_many :stop_points @@ -39,11 +39,21 @@ module Chouette      validates_format_of :coordinates, :with => %r{\A *-?(0?[0-9](\.[0-9]*)?|[0-8][0-9](\.[0-9]*)?|90(\.[0]*)?) *\, *-?(0?[0-9]?[0-9](\.[0-9]*)?|1[0-7][0-9](\.[0-9]*)?|180(\.[0]*)?) *\Z}, :allow_nil => true, :allow_blank => true      validates_format_of :url, :with => %r{\Ahttps?:\/\/([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?\Z}, :allow_nil => true, :allow_blank => true +    validates_numericality_of :waiting_time, greater_than_or_equal_to: 0, only_integer: true, if: :waiting_time +    validate :parent_area_type_must_be_greater +      def self.nullable_attributes        [:registration_number, :street_name, :country_code, :fare_code,        :nearest_topic_name, :comment, :long_lat_type, :zip_code, :city_name, :url, :time_zone]      end +    def parent_area_type_must_be_greater +      return unless self.parent +      if Chouette::AreaType.find(self.area_type) >= Chouette::AreaType.find(self.parent.area_type) +        errors.add(:parent_id, I18n.t('stop_areas.errors.parent_area_type', area_type: self.parent.area_type)) +      end +    end +      after_update :clean_invalid_access_links      before_save :coordinates_to_lat_lng @@ -72,6 +82,10 @@ module Chouette        end      end +    def full_name +      "#{name} #{zip_code} #{city_name} - #{user_objectid}" +    end +      def user_objectid        if objectid =~ /^.*:([0-9A-Za-z_-]+):STIF$/          $1 @@ -196,10 +210,12 @@ module Chouette        GeoRuby::SimpleFeatures::Envelope.from_coordinates coordinates      end +    # DEPRECATED use StopArea#area_type      def stop_area_type        area_type ? area_type : " "      end +    # DEPRECATED use StopArea#area_type      def stop_area_type=(stop_area_type)        self.area_type = (stop_area_type ? stop_area_type.camelcase : nil)      end @@ -324,5 +340,20 @@ module Chouette        end      end +    def activated? +      deleted_at.nil? +    end + +    def deactivated? +      !activated? +    end + +    def activate! +      update_attribute :deleted_at, nil +    end + +    def deactivate! +      update_attribute :deleted_at, Time.now +    end    end  end diff --git a/app/models/chouette/vehicle_journey.rb b/app/models/chouette/vehicle_journey.rb index 247c30668..983bf5363 100644 --- a/app/models/chouette/vehicle_journey.rb +++ b/app/models/chouette/vehicle_journey.rb @@ -1,3 +1,4 @@ +# coding: utf-8  module Chouette    class VehicleJourney < Chouette::TridentActiveRecord      has_paper_trail @@ -23,6 +24,7 @@ module Chouette      belongs_to :journey_pattern      has_and_belongs_to_many :footnotes, :class_name => 'Chouette::Footnote' +    has_and_belongs_to_many :purchase_windows, :class_name => 'Chouette::PurchaseWindow'      validates_presence_of :route      validates_presence_of :journey_pattern @@ -139,7 +141,7 @@ module Chouette      end      def update_has_and_belongs_to_many_from_state item -      ['time_tables', 'footnotes'].each do |assos| +      ['time_tables', 'footnotes', 'purchase_windows'].each do |assos|          saved = self.send(assos).map(&:id)          (saved - item[assos].map{|t| t['id']}).each do |id| @@ -241,11 +243,9 @@ module Chouette      end      def self.matrix(vehicle_journeys) -      {}.tap do |hash| -        vehicle_journeys.map{ |vj| -          vj.vehicle_journey_at_stops.map{ |vjas |hash[ "#{vj.id}-#{vjas.stop_point_id}"] = vjas } -        } -      end +      Hash[*VehicleJourneyAtStop.where(vehicle_journey_id: vehicle_journeys.pluck(:id)).map do |vjas| +        [ "#{vjas.vehicle_journey_id}-#{vjas.stop_point_id}", vjas] +      end.flatten]      end      def self.with_stops diff --git a/app/models/chouette/vehicle_journey_at_stop.rb b/app/models/chouette/vehicle_journey_at_stop.rb index 6f0119e74..6b3c1e7de 100644 --- a/app/models/chouette/vehicle_journey_at_stop.rb +++ b/app/models/chouette/vehicle_journey_at_stop.rb @@ -75,5 +75,14 @@ module Chouette          attrs << self.arrival_day_offset.to_s        end      end + +    def departure +      departure_time.utc.strftime "%H:%M" if departure_time +    end + +    def arrival +      arrival_time.utc.strftime "%H:%M" if arrival_time +    end +    end -end
\ No newline at end of file +end diff --git a/app/models/compliance_check_block.rb b/app/models/compliance_check_block.rb index 05240b428..059547e1b 100644 --- a/app/models/compliance_check_block.rb +++ b/app/models/compliance_check_block.rb @@ -6,8 +6,8 @@ class ComplianceCheckBlock < ActiveRecord::Base    has_many :compliance_checks -  hstore_accessor :condition_attributes, -    transport_mode: :string, -    transport_submode: :string +  store_accessor :condition_attributes, +    :transport_mode, +    :transport_submode  end diff --git a/app/models/compliance_control.rb b/app/models/compliance_control.rb index 65e22643d..2bde5b95a 100644 --- a/app/models/compliance_control.rb +++ b/app/models/compliance_control.rb @@ -6,7 +6,7 @@ class ComplianceControl < ActiveRecord::Base      def prerequisite; I18n.t('compliance_controls.metas.no_prerequisite'); end      def predicate; I18n.t("compliance_controls.#{self.name.underscore}.description") end      def dynamic_attributes -      hstore_metadata_for_control_attributes.keys +      stored_attributes[:control_attributes] || []      end      def policy_class @@ -39,7 +39,6 @@ class ComplianceControl < ActiveRecord::Base    belongs_to :compliance_control_block    enumerize :criticity, in: criticities, scope: true, default: :warning -  hstore_accessor :control_attributes, {}    validates :criticity, presence: true    validates :name, presence: true diff --git a/app/models/compliance_control_block.rb b/app/models/compliance_control_block.rb index cfcdfd1a6..d7d84fd06 100644 --- a/app/models/compliance_control_block.rb +++ b/app/models/compliance_control_block.rb @@ -5,11 +5,15 @@ class ComplianceControlBlock < ActiveRecord::Base    belongs_to :compliance_control_set    has_many :compliance_controls, dependent: :destroy -  hstore_accessor :condition_attributes, -    transport_mode: :string, -    transport_submode: :string +  store_accessor :condition_attributes, +    :transport_mode, +    :transport_submode    validates :transport_mode, presence: true    validates :compliance_control_set, presence: true +  def name +    ApplicationController.helpers.transport_mode_text(self) +  end +  end diff --git a/app/models/concerns/date_support.rb b/app/models/concerns/date_support.rb new file mode 100644 index 000000000..fbfe19af1 --- /dev/null +++ b/app/models/concerns/date_support.rb @@ -0,0 +1,80 @@ +module DateSupport +  extend ActiveSupport::Concern + +  included do +    after_initialize :init_dates + +    def init_dates +      self.dates ||= [] +    end + +    ### Calendar::DateValue +    # Required by coocon +    def build_date_value +      Calendar::DateValue.new +    end + +    def date_values +      @date_values ||= init_date_values +    end + +    def init_date_values +      if dates +        dates.each_with_index.map { |d, index| Calendar::DateValue.from_date(index, d) } +      else +        [] +      end +    end +    private :init_date_values + +    validate :validate_date_values + +    def validate_date_values +      date_values_are_valid = date_values.all?(&:valid?) + +      date_values.each do |date_value| +        if date_values.count { |d| d.value == date_value.value } > 1 +          date_value.errors.add(:base, I18n.t('activerecord.errors.models.calendar.attributes.dates.date_in_dates')) +          date_values_are_valid = false +        end +        date_ranges.each do |date_range| +          if date_range.cover? date_value.value +            date_value.errors.add(:base, I18n.t('activerecord.errors.models.calendar.attributes.dates.date_in_date_ranges')) +            date_values_are_valid = false +          end +        end +      end + +      unless date_values_are_valid +        errors.add(:date_values, :invalid) +      end +    end + +    def date_values_attributes=(attributes = {}) +      @date_values = [] +      attributes.each do |index, date_value_attribute| +        date_value_attribute['value'] = flatten_date_array(date_value_attribute, 'value') +        date_value = Calendar::DateValue.new(date_value_attribute.merge(id: index)) +        @date_values << date_value unless date_value.marked_for_destruction? +      end + +      dates_will_change! +    end + +    before_validation :fill_dates + +    def fill_dates +      if @date_values +        self.dates = @date_values.map(&:value).compact.sort +      end +    end + +    after_save :clear_date_values + +    def clear_date_values +      @date_values = nil +    end + +    private :clear_date_values +  end +end
\ No newline at end of file diff --git a/app/models/concerns/period_support.rb b/app/models/concerns/period_support.rb new file mode 100644 index 000000000..e17451fe4 --- /dev/null +++ b/app/models/concerns/period_support.rb @@ -0,0 +1,80 @@ +module PeriodSupport +  extend ActiveSupport::Concern + +  included do +    after_initialize :init_date_ranges + +    def init_date_ranges +      self.date_ranges ||= [] +    end + +    ### Calendar::Period +    # Required by coocon +    def build_period +      Calendar::Period.new +    end + +    def periods +      @periods ||= init_periods +    end + +    def init_periods +      (date_ranges || []) +        .each_with_index +        .map( &Calendar::Period.method(:from_range) ) +    end +    private :init_periods + +    validate :validate_periods + +    def validate_periods +      periods_are_valid = periods.all?(&:valid?) + +      periods.each do |period| +        if period.intersect?(periods) +          period.errors.add(:base, I18n.t('calendars.errors.overlapped_periods')) +          periods_are_valid = false +        end +      end + +      unless periods_are_valid +        errors.add(:periods, :invalid) +      end +    end + +    def flatten_date_array attributes, key +      date_int = %w(1 2 3).map {|e| attributes["#{key}(#{e}i)"].to_i } +      Date.new(*date_int) +    end + +    def periods_attributes=(attributes = {}) +      @periods = [] +      attributes.each do |index, period_attribute| +        # Convert date_select to date +        ['begin', 'end'].map do |attr| +          period_attribute[attr] = flatten_date_array(period_attribute, attr) +        end +        period = Calendar::Period.new(period_attribute.merge(id: index)) +        @periods << period unless period.marked_for_destruction? +      end + +      date_ranges_will_change! +    end + +    before_validation :fill_date_ranges + +    def fill_date_ranges +      if @periods +        self.date_ranges = @periods.map(&:range).compact.sort_by(&:begin) +      end +    end + +    after_save :clear_periods + +    def clear_periods +      @periods = nil +    end + +    private :clear_periods +  end +end diff --git a/app/models/generic_attribute_control/min_max.rb b/app/models/generic_attribute_control/min_max.rb index ab6f546a7..1c429b9a4 100644 --- a/app/models/generic_attribute_control/min_max.rb +++ b/app/models/generic_attribute_control/min_max.rb @@ -1,6 +1,6 @@  module GenericAttributeControl    class MinMax < ComplianceControl -    hstore_accessor :control_attributes, minimum: :integer, maximum: :integer, target: :string +    store_accessor :control_attributes, :minimum, :maximum, :target      validates :minimum, numericality: true, allow_nil: true      validates :maximum, numericality: true, allow_nil: true diff --git a/app/models/generic_attribute_control/pattern.rb b/app/models/generic_attribute_control/pattern.rb index 3a4a55d5c..7fc008e28 100644 --- a/app/models/generic_attribute_control/pattern.rb +++ b/app/models/generic_attribute_control/pattern.rb @@ -1,6 +1,6 @@  module GenericAttributeControl    class Pattern < ComplianceControl -    hstore_accessor :control_attributes, pattern: :string, target: :string +    store_accessor :control_attributes, :pattern, :target      validates :target, presence: true      validates :pattern, presence: true diff --git a/app/models/generic_attribute_control/uniqueness.rb b/app/models/generic_attribute_control/uniqueness.rb index f707c944b..82b5c0892 100644 --- a/app/models/generic_attribute_control/uniqueness.rb +++ b/app/models/generic_attribute_control/uniqueness.rb @@ -1,6 +1,6 @@  module GenericAttributeControl    class Uniqueness < ComplianceControl -    hstore_accessor :control_attributes, target: :string +    store_accessor :control_attributes, :target      validates :target, presence: true diff --git a/app/models/import.rb b/app/models/import.rb index 20e7f2d8a..049a65f40 100644 --- a/app/models/import.rb +++ b/app/models/import.rb @@ -14,12 +14,11 @@ class Import < ActiveRecord::Base     end    extend Enumerize -  enumerize :status, in: %i(new pending successful warning failed running aborted canceled), scope: true, default: :new +  enumerize :status, in: %w(new pending successful warning failed running aborted canceled), scope: true, default: :new    validates :name, presence: true    validates :file, presence: true    validates_presence_of :workbench, :creator -  validates_format_of :file, with: %r{\.zip\z}i, message: I18n.t('activerecord.errors.models.import.attributes.file.wrong_file_extension')    before_create :initialize_fields @@ -28,15 +27,19 @@ class Import < ActiveRecord::Base    end    def children_succeedeed -    children.with_status(:successful).count +    children.with_status(:successful, :warning).count    end -  def self.failing_statuses -    symbols_with_indifferent_access(%i(failed aborted canceled)) +  def self.launched_statuses +    %w(new pending) +  end + +  def self.failed_statuses +    %w(failed aborted canceled)    end    def self.finished_statuses -    symbols_with_indifferent_access(%i(successful failed warning aborted canceled)) +    %w(successful failed warning aborted canceled)    end    def notify_parent @@ -52,35 +55,25 @@ class Import < ActiveRecord::Base    end    def update_status -    status_count = children.group(:status).count -    children_finished_count = children_failed_count = children_count = 0 - -    status_count.each do |status, count| -      if self.class.failing_statuses.include?(status) -        children_failed_count += count -      end -      if self.class.finished_statuses.include?(status) -        children_finished_count += count -      end -      children_count += count -    end - -    attributes = { -      current_step: children_finished_count -    } -      status = -      if children_failed_count > 0 +      if children.where(status: self.class.failed_statuses).count > 0          'failed' -      elsif status_count['successful'] == children_count +      elsif children.where(status: "warning").count > 0 +        'warning' +      elsif children.where(status: "successful").count == children.count          'successful'        end +    attributes = { +      current_step: children.count, +      status: status +    } +      if self.class.finished_statuses.include?(status)        attributes[:ended_at] = Time.now      end -    update attributes.merge(status: status) +    update attributes    end    def update_referentials @@ -97,7 +90,4 @@ class Import < ActiveRecord::Base      self.token_download = SecureRandom.urlsafe_base64    end -  def self.symbols_with_indifferent_access(array) -    array.flat_map { |symbol| [symbol, symbol.to_s] } -  end  end diff --git a/app/models/organisation.rb b/app/models/organisation.rb index f6fba2d67..da7d1fcf3 100644 --- a/app/models/organisation.rb +++ b/app/models/organisation.rb @@ -1,3 +1,4 @@ +# coding: utf-8  class Organisation < ActiveRecord::Base    include DataFormatEnumerations @@ -18,36 +19,39 @@ class Organisation < ActiveRecord::Base    validates_presence_of :name    validates_uniqueness_of :code -  def self.portail_api_request -    conf = Rails.application.config.try(:stif_portail_api) -    raise 'Rails.application.config.stif_portail_api configuration is not defined' unless conf +  class << self -    HTTPService.get_json_resource( -      host: conf[:url], -      path: '/api/v1/organizations', -      token: conf[:key]) -  end +    def portail_api_request +      conf = Rails.application.config.try(:stif_portail_api) +      raise 'Rails.application.config.stif_portail_api configuration is not defined' unless conf + +      HTTPService.get_json_resource( +        host: conf[:url], +        path: '/api/v1/organizations', +        token: conf[:key]) +    end -  def self.sync_update code, name, scope -    org = Organisation.find_or_initialize_by(code: code) -    if scope -      org.sso_attributes ||= {} -      if org.sso_attributes['functional_scope'] != scope -        org.sso_attributes['functional_scope'] = scope -        # FIXME see #1941 -        org.sso_attributes_will_change! +    def sync_update code, name, scope +      org = Organisation.find_or_initialize_by(code: code) +      if scope +        org.sso_attributes ||= {} +        if org.sso_attributes['functional_scope'] != scope +          org.sso_attributes['functional_scope'] = scope +          # FIXME see #1941 +          org.sso_attributes_will_change! +        end        end +      org.name      = name +      org.synced_at = Time.now +      org.save +      org      end -    org.name      = name -    org.synced_at = Time.now -    org.save -    org -  end -  def self.portail_sync -    self.portail_api_request.each do |el| -      org = self.sync_update el['code'], el['name'], el['functional_scope'] -      puts "✓ Organisation #{org.name} has been updated" unless Rails.env.test? +    def portail_sync +      portail_api_request.each do |el| +        org = self.sync_update el['code'], el['name'], el['functional_scope'] +        puts "✓ Organisation #{org.name} has been updated" unless Rails.env.test? +      end      end    end @@ -64,4 +68,16 @@ class Organisation < ActiveRecord::Base      raise ActiveRecord::RecordNotFound    end +  def functional_scope +    JSON.parse( (sso_attributes || {}).fetch('functional_scope', '[]') ) +  end + +  def lines_set +    STIF::CodifligneLineId.lines_set_from_functional_scope( functional_scope ) +  end + +  def has_feature?(feature) +    features && features.include?(feature.to_s) +  end +  end diff --git a/app/models/referential.rb b/app/models/referential.rb index 3a9ef2027..1cdda9e6a 100644 --- a/app/models/referential.rb +++ b/app/models/referential.rb @@ -1,3 +1,4 @@ +# coding: utf-8  class Referential < ActiveRecord::Base    include DataFormatEnumerations    include ObjectidFormatterSupport @@ -34,7 +35,7 @@ class Referential < ActiveRecord::Base                 I18n.t('referentials.errors.inconsistent_organisation',                        indirect_name: workbench.organisation.name,                        direct_name: organisation.name)) -  end +  end, if: :organisation    belongs_to :line_referential    validates_presence_of :line_referential @@ -139,6 +140,10 @@ class Referential < ActiveRecord::Base      Chouette::RoutingConstraintZone.all    end +  def purchase_windows +    Chouette::PurchaseWindow.all +  end +    before_validation :define_default_attributes    def define_default_attributes @@ -295,7 +300,8 @@ class Referential < ActiveRecord::Base    def detect_overlapped_referentials      self.class.where(id: overlapped_referential_ids).each do |referential| -      errors.add :metadatas, I18n.t("referentials.errors.overlapped_referential", referential: referential.name) +      Rails.logger.info "Referential #{referential.id} #{referential.metadatas.inspect} overlaps #{metadatas.inspect}" +      errors.add :metadatas, I18n.t("referentials.errors.overlapped_referential", :referential => referential.name)      end    end @@ -315,11 +321,11 @@ class Referential < ActiveRecord::Base    end    def assign_slug -    self.slug ||= "#{self.name.parameterize.gsub('-', '_')}_#{Time.now.to_i}" +    self.slug ||= "#{name.parameterize.gsub('-', '_')}_#{Time.now.to_i}" if name    end    def assign_prefix -    self.prefix = organisation.name.parameterize.gsub('-', '_') +    self.prefix = organisation.name.parameterize.gsub('-', '_') if organisation    end    def assign_line_and_stop_area_referential diff --git a/app/models/referential_cloning.rb b/app/models/referential_cloning.rb index 5bf283814..24117e6c8 100644 --- a/app/models/referential_cloning.rb +++ b/app/models/referential_cloning.rb @@ -2,14 +2,27 @@ class ReferentialCloning < ActiveRecord::Base    include AASM    belongs_to :source_referential, class_name: 'Referential'    belongs_to :target_referential, class_name: 'Referential' -  after_commit :perform_clone, :on => :create +  after_commit :clone, on: :create -  private -  def perform_clone +  def clone      ReferentialCloningWorker.perform_async(id) -    # ReferentialCloningWorker.new.perform(id)    end +  def clone! +    run! + +    AF83::SchemaCloner +      .new(source_referential.slug, target_referential.slug) +      .clone_schema + +    successful! +  rescue Exception => e +    Rails.logger.error "Clone failed : #{e}" +    failed! +  end + +  private +    aasm column: :status do      state :new, :initial => true      state :pending diff --git a/app/models/user.rb b/app/models/user.rb index 37d35209a..1342f60ed 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -36,7 +36,7 @@ class User < ActiveRecord::Base      self.name         = extra[:full_name]      self.email        = extra[:email]      self.organisation = Organisation.sync_update extra[:organisation_code], extra[:organisation_name], extra[:functional_scope] -    self.permissions  = Stif::PermissionTranslator.translate(extra[:permissions]) +    self.permissions  = Stif::PermissionTranslator.translate(extra[:permissions], self.organisation)    end    def self.portail_api_request diff --git a/app/models/vehicle_journey_control/delta.rb b/app/models/vehicle_journey_control/delta.rb index 1f3a4d492..077dd6c4a 100644 --- a/app/models/vehicle_journey_control/delta.rb +++ b/app/models/vehicle_journey_control/delta.rb @@ -1,7 +1,7 @@  module VehicleJourneyControl    class Delta < ComplianceControl -    hstore_accessor :control_attributes, maximum: :integer +    store_accessor :control_attributes, :maximum      validates :maximum, numericality: true, allow_nil: true diff --git a/app/models/vehicle_journey_control/speed.rb b/app/models/vehicle_journey_control/speed.rb index be9f838e4..14fad9139 100644 --- a/app/models/vehicle_journey_control/speed.rb +++ b/app/models/vehicle_journey_control/speed.rb @@ -1,6 +1,6 @@  module VehicleJourneyControl    class Speed < ComplianceControl -    hstore_accessor :control_attributes, minimum: :integer, maximum: :integer +    store_accessor :control_attributes, :minimum, :maximum      validates :minimum, numericality: true, allow_nil: true      validates :maximum, numericality: true, allow_nil: true diff --git a/app/models/vehicle_journey_control/waiting_time.rb b/app/models/vehicle_journey_control/waiting_time.rb index 68fccb5c1..f2666cb72 100644 --- a/app/models/vehicle_journey_control/waiting_time.rb +++ b/app/models/vehicle_journey_control/waiting_time.rb @@ -1,8 +1,8 @@  module VehicleJourneyControl    class WaitingTime < ComplianceControl -    hstore_accessor :control_attributes, maximum: :integer +    store_accessor :control_attributes, :maximum -    validates :maximum, numericality: true, allow_nil: true +    validates_numericality_of :maximum, allow_nil: true, greater_than_or_equal_to: 0      def self.default_code; "3-VehicleJourney-1" end    end diff --git a/app/policies/calendar_policy.rb b/app/policies/calendar_policy.rb index 074c41d8d..c2da8c924 100644 --- a/app/policies/calendar_policy.rb +++ b/app/policies/calendar_policy.rb @@ -5,18 +5,15 @@ class CalendarPolicy < ApplicationPolicy      end    end -  def create?  -    !archived? && user.has_permission?('calendars.create') -  end -  def destroy? -    !archived? & organisation_match? && user.has_permission?('calendars.destroy') -  end -  def update? -    !archived? && organisation_match? && user.has_permission?('calendars.update') +  def create? +    user.has_permission?('calendars.create')    end +  def destroy?; instance_permission("destroy") end +  def update?; instance_permission("update") end +  def share?; instance_permission("share") end -  def share? -    user.organisation.name == 'STIF' # FIXME +  private +  def instance_permission permission +    organisation_match? && user.has_permission?("calendars.#{permission}")    end -  end diff --git a/app/policies/line_policy.rb b/app/policies/line_policy.rb index 67ea0b611..e7263cc3b 100644 --- a/app/policies/line_policy.rb +++ b/app/policies/line_policy.rb @@ -6,7 +6,6 @@ class LinePolicy < ApplicationPolicy    end    def create? -    Rails.logger.debug "LinePolicy.create?"      user.has_permission?('lines.create')    end @@ -14,6 +13,14 @@ class LinePolicy < ApplicationPolicy      user.has_permission?('lines.destroy')    end +  def deactivate? +    !record.deactivated? && user.has_permission?('lines.change_status') +  end + +  def activate? +    record.deactivated? && user.has_permission?('lines.change_status') +  end +    def update?      user.has_permission?('lines.update')    end diff --git a/app/policies/line_referential_policy.rb b/app/policies/line_referential_policy.rb new file mode 100644 index 000000000..ee742a083 --- /dev/null +++ b/app/policies/line_referential_policy.rb @@ -0,0 +1,14 @@ +class LineReferentialPolicy < ApplicationPolicy +  class Scope < Scope +    def resolve +      scope +    end +  end + +  def synchronize?; instance_permission("synchronize") end + +  private +  def instance_permission permission +    user.has_permission?("line_referentials.#{permission}") +  end +end diff --git a/app/policies/purchase_window_policy.rb b/app/policies/purchase_window_policy.rb new file mode 100644 index 000000000..75143a8bd --- /dev/null +++ b/app/policies/purchase_window_policy.rb @@ -0,0 +1,20 @@ +class PurchaseWindowPolicy < ApplicationPolicy +  class Scope < Scope +    def resolve +      scope +    end +  end + +  def create? +     !archived? && organisation_match? && user.has_permission?('purchase_windows.create') +  end + +  def update? +    !archived? && organisation_match? && user.has_permission?('purchase_windows.update') +  end + +  def destroy? +    !archived? && organisation_match? && user.has_permission?('purchase_windows.destroy') +  end + +end
\ No newline at end of file diff --git a/app/policies/referential_policy.rb b/app/policies/referential_policy.rb index bc8a3e24b..253917509 100644 --- a/app/policies/referential_policy.rb +++ b/app/policies/referential_policy.rb @@ -22,7 +22,7 @@ class ReferentialPolicy < ApplicationPolicy    end    def validate? -    !archived? && create? +    !archived? && create? && organisation_match?    end    def archive? diff --git a/app/policies/stop_area_policy.rb b/app/policies/stop_area_policy.rb index faeebbc2a..6db48b702 100644 --- a/app/policies/stop_area_policy.rb +++ b/app/policies/stop_area_policy.rb @@ -1,5 +1,13 @@  class StopAreaPolicy < ApplicationPolicy    class Scope < Scope +    def search_scope scope_name +      scope = resolve +      if scope_name&.to_s == "route_editor" +        scope = scope.where(area_type: 'zdep') unless user.organisation.has_feature?("route_stop_areas_all_types") +      end +      scope +    end +      def resolve        scope      end @@ -16,4 +24,12 @@ class StopAreaPolicy < ApplicationPolicy    def update?      user.has_permission?('stop_areas.update')    end + +  def deactivate? +    !record.deactivated? && user.has_permission?('stop_areas.change_status') +  end + +  def activate? +    record.deactivated? && user.has_permission?('stop_areas.change_status') +  end  end diff --git a/app/policies/stop_area_referential_policy.rb b/app/policies/stop_area_referential_policy.rb new file mode 100644 index 000000000..e370babf8 --- /dev/null +++ b/app/policies/stop_area_referential_policy.rb @@ -0,0 +1,14 @@ +class StopAreaReferentialPolicy < ApplicationPolicy +  class Scope < Scope +    def resolve +      scope +    end +  end + +  def synchronize?; instance_permission("synchronize") end + +  private +  def instance_permission permission +    user.has_permission?("stop_area_referentials.#{permission}") +  end +end diff --git a/app/services/zip_service.rb b/app/services/zip_service.rb index 7a4bdad1b..7166e6448 100644 --- a/app/services/zip_service.rb +++ b/app/services/zip_service.rb @@ -1,12 +1,16 @@  class ZipService -  class Subdir < Struct.new(:name, :stream, :spurious) +  class Subdir < Struct.new(:name, :stream, :spurious, :foreign_lines) +    def ok? +      foreign_lines.empty? && spurious.empty? +    end    end -  attr_reader :current_key, :current_output, :current_spurious, :yielder +  attr_reader :allowed_lines, :current_key, :foreign_lines, :current_output, :current_spurious, :yielder -  def initialize data +  def initialize data, allowed_lines      @zip_data       = StringIO.new(data) +    @allowed_lines  = allowed_lines      @current_key    = nil      @current_output = nil    end @@ -35,7 +39,8 @@ class ZipService    end    def add_to_current_output entry -    return if is_spurious! entry.name +    return if is_spurious!(entry.name) || is_foreign_line!(entry.name) +      current_output.put_next_entry entry.name      write_to_current_output entry.get_input_stream    end @@ -52,20 +57,22 @@ class ZipService          current_key,          # Second part of the solution, yield the closed stream          current_output.close_buffer, -        current_spurious) +        current_spurious.to_a, +        foreign_lines)      end    end    def open_new_output entry_key      @current_key    = entry_key      # First piece of the solution, use internal way to create a Zip::OutputStream -    @current_output = Zip::OutputStream.new(StringIO.new(''), true, nil) -    @current_spurious = [] +    @current_output   = Zip::OutputStream.new(StringIO.new(''), true, nil) +    @current_spurious = Set.new +    @foreign_lines    = []    end    def entry_key entry      # last dir name File.dirname.split("/").last -    entry.name.split('/', -1)[-2] +    entry.name.split('/').first    end    def is_spurious! entry_name @@ -75,4 +82,12 @@ class ZipService      current_spurious << segments.second      return true    end + +  def is_foreign_line! entry_name +    STIF::NetexFile::Frame.get_short_id(entry_name).tap do | line_object_id | +      return nil unless line_object_id +      return nil if line_object_id.in? allowed_lines +      foreign_lines << line_object_id +    end +  end  end diff --git a/app/uploaders/import_uploader.rb b/app/uploaders/import_uploader.rb index 2740393ca..60e17ca0f 100644 --- a/app/uploaders/import_uploader.rb +++ b/app/uploaders/import_uploader.rb @@ -36,9 +36,9 @@ class ImportUploader < CarrierWave::Uploader::Base    # Add a white list of extensions which are allowed to be uploaded.    # For images you might use something like this: -  # def extension_whitelist -  #   %w(jpg jpeg gif png) -  # end +  def extension_whitelist +    %w(zip) +  end    # Override the filename of the uploaded files:    # Avoid using model.id or version_name here, see uploader/store.rb for details. diff --git a/app/views/autocomplete_purchase_windows/index.rabl b/app/views/autocomplete_purchase_windows/index.rabl new file mode 100644 index 000000000..1d0287602 --- /dev/null +++ b/app/views/autocomplete_purchase_windows/index.rabl @@ -0,0 +1,12 @@ +collection @purchase_windows, :object_root => false + +node do |window| +  { +    :id => window.id, +    :name => window.name, +    :objectid => window.objectid, +    :color => window.color, +    :short_id => window.get_objectid.short_id, +    :text => "<strong><span class='fa fa-circle' style='color:" + (window.color ? window.color : '#4b4b4b') + "'></span> " + window.name + " - " + window.get_objectid.short_id + "</strong>" +  } +end diff --git a/app/views/autocomplete_stop_areas/index.rabl b/app/views/autocomplete_stop_areas/index.rabl index 5a9f76a47..d20051ad5 100644 --- a/app/views/autocomplete_stop_areas/index.rabl +++ b/app/views/autocomplete_stop_areas/index.rabl @@ -14,7 +14,8 @@ node do |stop_area|    :longitude => stop_area.longitude,    :latitude => stop_area.latitude,    :area_type => stop_area.area_type, -  :comment => stop_area.comment +  :comment => stop_area.comment, +  :text => stop_area.full_name    }  end diff --git a/app/views/companies/index.html.slim b/app/views/companies/index.html.slim index 5d746642f..e031f3776 100644 --- a/app/views/companies/index.html.slim +++ b/app/views/companies/index.html.slim @@ -34,7 +34,7 @@                  end \                ) \              ], -            links: [:show, :edit], +            links: [:show],              cls: 'table has-search'            = new_pagination @companies, 'pull-right' diff --git a/app/views/compliance_check_sets/index.html.slim b/app/views/compliance_check_sets/index.html.slim index f5d1bd777..f109845b4 100644 --- a/app/views/compliance_check_sets/index.html.slim +++ b/app/views/compliance_check_sets/index.html.slim @@ -12,7 +12,7 @@                [ \                  TableBuilderHelper::Column.new( \                    key: :ref, \ -                  attribute: 'compliance_check_set_id' \ +                  attribute: 'id' \                  ), \                  TableBuilderHelper::Column.new( \                    key: :creation_date, \ @@ -41,5 +41,3 @@        .row.mt-xs          .col-lg-12            = replacement_msg t('compliance_check_sets.search_no_results') - - diff --git a/app/views/compliance_check_sets/show.html.slim b/app/views/compliance_check_sets/show.html.slim index c9d0583a5..5d8e3fa15 100644 --- a/app/views/compliance_check_sets/show.html.slim +++ b/app/views/compliance_check_sets/show.html.slim @@ -1,11 +1,6 @@  - breadcrumb :compliance_check_sets, @workbench, @compliance_check_set  / PageHeader -= pageheader 'jeux-de-donnees', -             @compliance_check_set.name, -             '', -             t('last_update', time: l(@compliance_check_set.updated_at, format: :short)) do - -  / Below is secundary actions & optional contents (filters, ...) +- content_for :page_header_content do    .row      .col-lg-12.text-right.mb-sm        - @compliance_check_set.action_links.each do |link| @@ -13,7 +8,23 @@              method: link.method,              data: link.data,              class: 'btn btn-primary' do -          = link.content +              = link.content + +- page_header_content_for @compliance_check_set +/ = pageheader 'jeux-de-donnees', +/              @compliance_check_set.name, +/              '', +/              t('last_update', time: l(@compliance_check_set.updated_at, format: :short)) do + +  / Below is secundary actions & optional contents (filters, ...) +  / .row +  /   .col-lg-12.text-right.mb-sm +  /     - @compliance_check_set.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.import_messages diff --git a/app/views/compliance_control_sets/index.html.slim b/app/views/compliance_control_sets/index.html.slim index 2a5651280..28d2254bf 100644 --- a/app/views/compliance_control_sets/index.html.slim +++ b/app/views/compliance_control_sets/index.html.slim @@ -30,7 +30,7 @@                  ), \                  TableBuilderHelper::Column.new( \                    key: :control_numbers, \ -                  attribute: 'control_numbers' \ +                  attribute: Proc.new {|n| n.compliance_controls.count }\                  ), \                  TableBuilderHelper::Column.new( \                    key: :updated_at, \ diff --git a/app/views/compliance_control_sets/show.html.slim b/app/views/compliance_control_sets/show.html.slim index e6416fda4..d915bbdaf 100644 --- a/app/views/compliance_control_sets/show.html.slim +++ b/app/views/compliance_control_sets/show.html.slim @@ -26,82 +26,23 @@        .col-lg-12          = render '/compliance_controls/filters' +  / compliance controls without block +  = render_compliance_control_block +  = render_compliance_controls(@direct_compliance_controls) -    - if @direct_compliance_controls.try(:any?) -      .row -        .col-lg-12 -          h2 -            = transport_mode_text() -      .row -        .col-lg-12 -          .select_table -            = table_builder_2 @direct_compliance_controls, -                    [ \ -                TableBuilderHelper::Column.new( \ -                  key: :code, \ -                  attribute: 'code' \ -                ), \ -                TableBuilderHelper::Column.new( \ -                  key: :name, \ -                  attribute: 'name', \ -                  link_to: lambda do |compliance_control| \ -                      compliance_control_set_compliance_control_path(@compliance_control_set, compliance_control) \ -                    end \ -                ), \ -                TableBuilderHelper::Column.new( \ -                  key: :criticity, \ -                  attribute: 'criticity' \ -                ), \ -                TableBuilderHelper::Column.new( \ -                  key: :comment, \ -                  attribute: 'comment' \ -                ), \ -              ], -              sortable: true, -              cls: 'table has-filter has-search', -              model: ComplianceControl - +  / compliance controls with block +  - if params[:q] && params[:q][:compliance_control_block_id_eq_any].try(:present?)      - @blocks_to_compliance_controls_map.each do |block, compliance_controls| +      = render_compliance_control_block(block) +      = render_compliance_controls(compliance_controls) +  - else +    - @compliance_control_set.compliance_control_blocks.each do |block| +      = render_compliance_control_block(block) +      = render_compliance_controls(@blocks_to_compliance_controls_map[block]) -      - if compliance_controls.try(:any?) -        .row -          .col-lg-12 -            h2 -              = transport_mode_text(block) -              .btn-group -                .btn.dropdown-toggle{ data-toggle="dropdown" } -                  .span.fa.fa-cog -                ul.dropdown-menu -                  li -                    = link_to t('compliance_control_sets.actions.edit'), edit_compliance_control_set_compliance_control_block_path(@compliance_control_set.id, block.id) -                    = link_to t('compliance_control_sets.actions.destroy'), compliance_control_set_compliance_control_block_path(@compliance_control_set.id, block.id), :method => :delete, :data => {:confirm =>  t('compliance_control_sets.actions.destroy_confirm')} -        .row -          .col-lg-12 -            .select_table -              = table_builder_2 compliance_controls, -                      [ \ -                  TableBuilderHelper::Column.new( \ -                    key: :code, \ -                    attribute: 'code' \ -                  ), \ -                  TableBuilderHelper::Column.new( \ -                    key: :name, \ -                    attribute: 'name', \ -                    link_to: lambda do |compliance_control| \ -                        compliance_control_set_compliance_control_path(@compliance_control_set, compliance_control) \ -                      end \ -                  ), \ -                  TableBuilderHelper::Column.new( \ -                    key: :criticity, \ -                    attribute: 'criticity' \ -                  ), \ -                  TableBuilderHelper::Column.new( \ -                    key: :comment, \ -                    attribute: 'comment' \ -                  ), \ -                ], -                sortable: true, -                cls: 'table has-filter has-search', -                model: ComplianceControl +  - if params[:q].present? && !@blocks_to_compliance_controls_map.try(:any?) && @direct_compliance_controls.nil? +    .row.mt-xs +      .col-lg-12 +        = replacement_msg t('compliance_controls.search_no_results') -  = flotted_links @compliance_control_set.id +  = floating_links @compliance_control_set.id diff --git a/app/views/dashboards/_dashboard.html.slim b/app/views/dashboards/_dashboard.html.slim index f03301e23..075b94ddc 100644 --- a/app/views/dashboards/_dashboard.html.slim +++ b/app/views/dashboards/_dashboard.html.slim @@ -5,7 +5,7 @@          .panel-heading            h3.panel-title.with_actions              div -              = workbench.name +              = link_to workbench.name, workbench_path(workbench)                span.badge.ml-xs = workbench.referentials.count if workbench.referentials.present?              div @@ -22,7 +22,7 @@      .panel.panel-default        .panel-heading          h3.panel-title.with_actions -          = "Modèles de calendrier" +          = link_to I18n.t("activerecord.models.calendar", count: @dashboard.current_organisation.calendars.size), calendars_path            div              = link_to '', calendars_path, class: ' fa fa-chevron-right pull-right'        - if @dashboard.current_organisation.calendars.present? @@ -31,21 +31,24 @@              = link_to calendar.name, calendar_path(calendar), class: 'list-group-item'        - else          .panel-body -          em.small.text-muted Aucun modèle de calendrier défini +          em.small.text-muted +            = t('dasboard.calendars.none')    .col-lg-6.col-md-6.col-sm-6.col-xs-12      .panel.panel-default -      .panel-heading -        h3.panel-title -          = "Référentiels d'arrêts" -      .list-group -        - @dashboard.current_organisation.stop_area_referentials.each do |referential| -          = link_to referential.name, stop_area_referential_stop_areas_path(referential), class: 'list-group-item' +      - @dashboard.current_organisation.stop_area_referentials.each do |referential| +        .panel-heading +          h3.panel-title +            = referential.name +        .list-group +          = link_to Chouette::StopArea.model_name.human.pluralize.capitalize, stop_area_referential_stop_areas_path(referential), class: 'list-group-item'      .panel.panel-default -      .panel-heading -        h3.panel-title -          = "Référentiels de lignes" -      .list-group -        - @dashboard.current_organisation.line_referentials.all.each do |referential| -          = link_to referential.name, line_referential_lines_path(referential), class: 'list-group-item' +      - @dashboard.current_organisation.line_referentials.all.each do |referential| +        .panel-heading +          h3.panel-title +            = referential.name +        .list-group +            = link_to Chouette::Line.model_name.human.pluralize.capitalize, line_referential_lines_path(referential), class: 'list-group-item' +            = link_to Chouette::Company.model_name.human.pluralize.capitalize, line_referential_companies_path(referential), class: 'list-group-item' +            = link_to "Réseaux", line_referential_networks_path(referential), class: 'list-group-item' diff --git a/app/views/devise/invitations/edit.html.slim b/app/views/devise/invitations/edit.html.slim index 6c9a6f436..7a22146c0 100644 --- a/app/views/devise/invitations/edit.html.slim +++ b/app/views/devise/invitations/edit.html.slim @@ -1,14 +1,19 @@ -.col-md-offset-2.col-md-8 -  .panel.panel-default -    .panel-heading = t('devise.invitations.edit.header') -     -    .panel-body -      = simple_form_for resource, as: resource_name, :url => invitation_path(resource_name), :html => { :method => :put, class: "form-horizontal" } do |form| -        = form.hidden_field :invitation_token - -        = form.input :name -        = form.input :password, as: :password -        = form.input :password_confirmation, as: :password - -        .submit -          = form.button :submit, value: t('devise.invitations.edit.submit_button'), class: 'btn-info'
\ No newline at end of file +/ PageHeader + +- content_for :page_header_title, t('.title') + +/ PageContent +.page_content +  .container-fluid +    .row +      .col-lg-8.col-lg-offset-2.col-md-8.col-md-offset-2.col-sm-10.col-sm-offset-1 +        = simple_form_for resource, as: resource_name, :url => invitation_path(resource_name), :html => { :method => :put, class: "form-horizontal", id: 'invitation_form' } do |form| +            .row +              .col-lg-12 +                = form.hidden_field :invitation_token + +                = form.input :name +                = form.input :password, as: :password +                = form.input :password_confirmation, as: :password + +            = form.button :submit, value: t('devise.invitations.edit.submit_button'), class: 'btn-info btn-default formSubmitr', form: 'invitation_form' diff --git a/app/views/devise/mailer/invitation_instructions.fr.html.slim b/app/views/devise/mailer/invitation_instructions.fr.html.slim index 484e0d68d..e263e188a 100644 --- a/app/views/devise/mailer/invitation_instructions.fr.html.slim +++ b/app/views/devise/mailer/invitation_instructions.fr.html.slim @@ -1,7 +1,7 @@ -p = t("devise.mailer.invitation_instructions.hello", email: @resource.email) +p = t("devise.mailer.invitation_instructions.hello", email: @resource.name)  p = t("devise.mailer.invitation_instructions.someone_invited_you", url: unauthenticated_root_url)  p = link_to t("devise.mailer.invitation_instructions.accept"), accept_invitation_url(@resource, :invitation_token => @token) -p = t("devise.mailer.invitation_instructions.ignore").html_safe
\ No newline at end of file +p = t("devise.mailer.invitation_instructions.ignore").html_safe diff --git a/app/views/devise/passwords/edit.html.slim b/app/views/devise/passwords/edit.html.slim index 864a44499..0d18f657c 100644 --- a/app/views/devise/passwords/edit.html.slim +++ b/app/views/devise/passwords/edit.html.slim @@ -1,13 +1,17 @@ -.col-md-offset-2.col-md-8 -  .panel.panel-default -    .panel-heading = t('.title') -    .panel-body -      = simple_form_for(resource, :as => resource_name, :url => password_path(resource_name), :html => { :method => :put, class: "form-horizontal" }) do |f| +/ PageHeader -        = f.input :reset_password_token, as: :hidden -        = f.input :password, as: :password -        = f.input :password_confirmation, as: :password -         -        .form-actions -          = link_to t("cancel"), unauthenticated_root_path, class: 'btn btn-default' -          = f.button :submit, :value => t("devise.passwords.edit.commit"), class: 'btn-info'
\ No newline at end of file +- content_for :page_header_title, t('.title') + +/ PageContent +.page_content +  .container-fluid +    .row +      .col-lg-8.col-lg-offset-2.col-md-8.col-md-offset-2.col-sm-10.col-sm-offset-1 +        = simple_form_for(resource, :as => resource_name, :url => password_path(resource_name), :html => { :method => :put, class: "form-horizontal", id: 'password_form' }) do |f| +          .row +            .col-lg-12 +              = f.input :reset_password_token, as: :hidden +              = f.input :password, as: :password +              = f.input :password_confirmation, as: :password + +          = f.button :submit, :value => t("devise.passwords.edit.commit"), class: 'btn btn-default formSubmitr', form: 'password_form' diff --git a/app/views/devise/passwords/new.html.slim b/app/views/devise/passwords/new.html.slim index 8a739ecc1..303f78f0e 100644 --- a/app/views/devise/passwords/new.html.slim +++ b/app/views/devise/passwords/new.html.slim @@ -1,11 +1,15 @@ -.col-md-offset-2.col-md-8 -  .panel.panel-default -    .panel-heading = t('.title') -     -    .panel-body -      = simple_form_for(resource, :as => resource_name, :url => password_path(resource_name), html: {class: 'form-horizontal' } ) do |form| -        = form.input :email, :as => :email, placeholder: 'user@domain.com' +/ PageHeader -        .form-actions -          = link_to t("cancel"), unauthenticated_root_path, class: 'btn btn-default' -          = form.button :submit, :value => t("devise.passwords.new.commit"), class: 'btn-info'
\ No newline at end of file +- content_for :page_header_title, t('.title') + +/ PageContent +.page_content +  .container-fluid +    .row +      .col-lg-8.col-lg-offset-2.col-md-8.col-md-offset-2.col-sm-10.col-sm-offset-1 +        = simple_form_for(resource, :as => resource_name, :url => password_path(resource_name), html: {class: 'form-horizontal', id: 'password_form' } ) do |form| +          .row +            .col-lg-12 +              = form.input :email, :as => :email, placeholder: 'user@domain.com' + +          = form.button :submit, :value => t("devise.passwords.new.commit"), class: 'btn btn-default formSubmitr', form: 'password_form' diff --git a/app/views/devise/sessions/new.html.slim b/app/views/devise/sessions/new.html.slim index 0ed17e24a..a812726e5 100644 --- a/app/views/devise/sessions/new.html.slim +++ b/app/views/devise/sessions/new.html.slim @@ -1,3 +1,5 @@ +- content_for :page_header_title, t('.title') +  .page_content#devise    .container-fluid      #sessions_new.row diff --git a/app/views/import_resources/index.html.slim b/app/views/import_resources/index.html.slim index 1c4a5a765..728d9f4a8 100644 --- a/app/views/import_resources/index.html.slim +++ b/app/views/import_resources/index.html.slim @@ -1,4 +1,4 @@ - +- breadcrumb :import_resources, @import, @import_resources  .page_content.import_messages    .container-fluid      .row diff --git a/app/views/imports/_import_messages.html.slim b/app/views/imports/_import_messages.html.slim new file mode 100644 index 000000000..a10dce065 --- /dev/null +++ b/app/views/imports/_import_messages.html.slim @@ -0,0 +1,11 @@ +- if import_messages.any? +  .import_message-list +    .import_messages-head Messages +    dl +      - import_messages.each do | import_message | +        dt.criticity +          = import_message.criticity +        dd +          = t( ['import_messages', +            'compliance_check_messages', +            import_message.message_key].join('.'), import_message.message_attributes.symbolize_keys) diff --git a/app/views/imports/show.html.slim b/app/views/imports/show.html.slim index 5e22e03e0..cf137867b 100644 --- a/app/views/imports/show.html.slim +++ b/app/views/imports/show.html.slim @@ -19,46 +19,52 @@      .row        .col-lg-12 -        = table_builder_2 @import.children, -          [ \ -            TableBuilderHelper::Column.new( \ -              name: 'Nom du jeu de données', \ -              attribute: 'name', \ -              sortable: false, \ -              link_to: lambda do |import| \ -                referential_path(import.referential) if import.referential.present? \ -              end \ -            ), \ -            TableBuilderHelper::Column.new( \ -              key: :status, \ -              attribute: Proc.new { |n| import_status(n.status) }, \ -              sortable: false, \ -              link_to: lambda do |import| \ -                workbench_import_import_resources_path(import.workbench_id, import) \ -              end \ -            ), \ -            TableBuilderHelper::Column.new( \ -              name: 'Contrôle STIF', \ -              attribute: '', \ -              sortable: false, \ -            ), \ -            TableBuilderHelper::Column.new( \ -              name: 'Contrôle organisation', \ -              attribute: '', \ -              sortable: false, \ -            ) \ -          ], -          links: [], -          cls: 'table', -          overhead: [ \ -            {}, \ -            { \ -              title: "#{@import.children_succeedeed} jeu de données validé sur #{@import.children.count} présent(s) dans l'archive", \ -              width: 1, \ -              cls: "#{@import.import_status_css_class} full-border" \ -            }, { \ -              title: 'Bilan des jeux de contrôles d\'import <span title="Lorem ipsum..." class="fa fa-lg fa-info-circle text-info"></span>', \ -              width: 2, \ -              cls: 'overheaded-default colspan="2"' \ -            } \ -           ] +        .error_messages +          = render 'import_messages', import_messages: @import.messages + +    - if @import.children.any? +      .row +        .col-lg-12 +          = table_builder_2 @import.children, +            [ \ +              TableBuilderHelper::Column.new( \ +                name: 'Nom du jeu de données', \ +                attribute: 'name', \ +                sortable: false, \ +                link_to: lambda do |import| \ +                  referential_path(import.referential) if import.referential.present? \ +                end \ +              ), \ +              TableBuilderHelper::Column.new( \ +                key: :status, \ +                attribute: Proc.new { |n| import_status(n.status) }, \ +                sortable: false, \ +                link_to: lambda do |import| \ +                  workbench_import_import_resources_path(import.workbench_id, import) \ +                end \ +              ), \ +              TableBuilderHelper::Column.new( \ +                name: 'Contrôle STIF', \ +                attribute: '', \ +                sortable: false, \ +              ), \ +              TableBuilderHelper::Column.new( \ +                name: 'Contrôle organisation', \ +                attribute: '', \ +                sortable: false, \ +              ) \ +            ], +            links: [], +            cls: 'table', +            overhead: [ \ +              {}, \ +              { \ +                title: "#{@import.children_succeedeed} jeu de données validé sur #{@import.children.count} présent(s) dans l'archive", \ +                width: 1, \ +                cls: "#{@import.import_status_css_class} full-border" \ +              }, { \ +                title: 'Bilan des jeux de contrôles d\'import <span title="Lorem ipsum..." class="fa fa-lg fa-info-circle text-info"></span>', \ +                width: 2, \ +                cls: 'overheaded-default colspan="2"' \ +              } \ +            ] diff --git a/app/views/journey_patterns_collections/show.html.slim b/app/views/journey_patterns_collections/show.html.slim index 834501da3..8a7b0c47c 100644 --- a/app/views/journey_patterns_collections/show.html.slim +++ b/app/views/journey_patterns_collections/show.html.slim @@ -17,6 +17,7 @@    | window.stopPoints = #{(@stop_points_list.to_json).html_safe};    | window.journeyPatternLength = #{@journey_patterns.total_entries()};    | window.journeyPatternsPerPage = #{@ppage}; -  | window.perms = #{raw @perms} +  | window.perms = #{raw @perms}; +  | window.features = #{raw @features};  = javascript_pack_tag 'journey_patterns/index.js' diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb new file mode 100644 index 000000000..977cbfe5b --- /dev/null +++ b/app/views/layouts/mailer.html.erb @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html> +  <head> +    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> +    <style> +      /* Email styles need to be inline */ +      body { +        font-family: "Open Sans", sans-serif; +        font-size: 14px; +        line-height: 1.4; +        color: #4b4b4b; +				width: 800px; +      } +    </style> +  </head> + +  <body> +		<%= image_tag 'mail-header.png' %> + +    <%= yield %> +  </body> +</html> diff --git a/app/views/layouts/mailer.html.slim b/app/views/layouts/mailer.html.slim deleted file mode 100644 index 5db6917b3..000000000 --- a/app/views/layouts/mailer.html.slim +++ /dev/null @@ -1,7 +0,0 @@ -doctype html -html -  head -    meta charset="utf-8" -    /!* Email styles need to be inline */ -  body -    = yield diff --git a/app/views/layouts/navigation/_main_nav_top.html.slim b/app/views/layouts/navigation/_main_nav_top.html.slim index d6c849d3f..278249e09 100644 --- a/app/views/layouts/navigation/_main_nav_top.html.slim +++ b/app/views/layouts/navigation/_main_nav_top.html.slim @@ -1,5 +1,5 @@  .nav-menu#menu_top -  .brandname = t('brandname') +  .brandname = link_to t('brandname'), root_path    .menu-content      .menu-item @@ -11,7 +11,7 @@            span.fa.fa-lg.fa-tasks          = link_to '#', class: 'menu-item', data: { panel: 'toggle', target: '#profile_panel' }, title: 'Profil' do -          span = current_user.username +          span = current_user.name            span.fa.fa-lg.fa-user diff --git a/app/views/line_referentials/show.html.slim b/app/views/line_referentials/show.html.slim index b4b32bc52..763eb076e 100644 --- a/app/views/line_referentials/show.html.slim +++ b/app/views/line_referentials/show.html.slim @@ -1,7 +1,8 @@  - breadcrumb :line_referential, @line_referential  - page_header_content_for @line_referential -- content_for :page_header_actions do -  = link_to(t('actions.sync'), sync_line_referential_path(@line_referential), method: :post, class: 'btn btn-default') +- if policy(@line_referential).synchronize? +  - content_for :page_header_actions do +    = link_to(t('actions.sync'), sync_line_referential_path(@line_referential), method: :post, class: 'btn btn-default')  - content_for :page_header_content do    .row.mb-md diff --git a/app/views/lines/_form.html.slim b/app/views/lines/_form.html.slim index 4952b72ff..909d6512e 100644 --- a/app/views/lines/_form.html.slim +++ b/app/views/lines/_form.html.slim @@ -3,7 +3,8 @@      .col-lg-12        = f.input :name        = f.input :network_id, as: :select, :collection => @line_referential.networks, include_blank: false -      = f.input :company_id, as: :select, :collection => @line_referential.companies, include_blank: false +      = f.input :company_id, as: :select, :collection => @line_referential.companies, include_blank: true +      = f.input :secondary_company_ids, :collection => @line_referential.companies, include_blank: false, input_html: { multiple: true, 'data-select2ed': true }, label: t('activerecord.attributes.line.secondary_company')        = f.input :published_name        = f.input :registration_number        = f.input :number @@ -19,4 +20,3 @@    .separator    = f.button :submit, t('actions.submit'), class: 'btn btn-default formSubmitr', form: 'lines_form' - diff --git a/app/views/lines/index.html.slim b/app/views/lines/index.html.slim index 8b035b477..b62263263 100644 --- a/app/views/lines/index.html.slim +++ b/app/views/lines/index.html.slim @@ -1,6 +1,6 @@  - breadcrumb :lines, @line_referential  - content_for :page_header_actions do -  - if (policy(Chouette::Line).create? && @line_referential.organisations.include?(current_organisation)) +  - if policy(Chouette::Line).create?      = link_to(t('lines.actions.new'), new_line_referential_line_path(@line_referential), class: 'btn btn-primary')  .page_content @@ -41,7 +41,7 @@                ), \                TableBuilderHelper::Column.new( \                  key: 'companies.name', \ -                attribute: Proc.new { |n| n.try(:company).try(:name) } \ +                attribute: Proc.new { |n| n&.company&.name || "-" } \                ), \                TableBuilderHelper::Column.new( \                  key: :transport_mode, \ diff --git a/app/views/lines/show.html.slim b/app/views/lines/show.html.slim index 4969ca3cd..83244f739 100644 --- a/app/views/lines/show.html.slim +++ b/app/views/lines/show.html.slim @@ -6,7 +6,7 @@          = link_to link.href,              method: link.method,              data: link.data, -            class: 'btn btn-primary' do +            class: "btn btn-primary #{link.disabled ? "disabled" : ""}" do                = link.content  - page_header_content_for @line @@ -20,7 +20,7 @@               'Activé' => (@line.deactivated? ? t('false') : t('true')),               @line.human_attribute_name(:network_id) => (@line.network.nil? ? t('lines.index.unset') : @line.network.name),               @line.human_attribute_name(:company_id) => (@line.company.nil? ? t('lines.index.unset') : @line.company.name), -             'Transporteur(s) secondaire(s)' => (@line.secondary_companies.nil? ? t('lines.index.unset') : @line.secondary_companies.collect(&:name).join(', ')), +             'Transporteur(s) secondaire(s)' => (@line.secondary_companies.nil? ? t('lines.index.unset') : array_to_html_list(@line.secondary_companies.collect(&:name))),               'Nom court' => @line.number,               'Code public' => (@line.registration_number ? @line.registration_number : '-'),               @line.human_attribute_name(:transport_mode) => (@line.transport_mode.present? ? t("enumerize.transport_mode.#{@line.transport_mode}") : '-'), diff --git a/app/views/purchase_windows/_date_value_fields.html.slim b/app/views/purchase_windows/_date_value_fields.html.slim new file mode 100644 index 000000000..7bde06a94 --- /dev/null +++ b/app/views/purchase_windows/_date_value_fields.html.slim @@ -0,0 +1,13 @@ +.nested-fields +  - if f.object.errors.has_key? :base +    .row +      .col-lg-12 +        .alert.alert-danger +          - f.object.errors[:base].each do |message| +            p.small = message + +  .wrapper +    div +      = f.input :value, as: :date, label: false, wrapper_html: { class: 'date smart_date' } +    div +      = link_to_remove_association '', f, class: 'fa fa-trash', data: { confirm: 'Etes-vous sûr(e) ?' }, title: t('actions.delete') diff --git a/app/views/purchase_windows/_filters.html.slim b/app/views/purchase_windows/_filters.html.slim new file mode 100644 index 000000000..4d7c8ce26 --- /dev/null +++ b/app/views/purchase_windows/_filters.html.slim @@ -0,0 +1,15 @@ += search_form_for @q, url: referential_purchase_windows_path, builder: SimpleForm::FormBuilder, html: { method: :get, class: 'form form-filter' } do |f| +  .ffg-row +    .input-group.search_bar +      = f.search_field :name_cont, class: 'form-control', placeholder: t('purchase_windows.index.filter_placeholder') +      span.input-group-btn +        button.btn.btn-default#search_btn type='submit' +          span.fa.fa-search + +    .form-group +      = f.label Chouette::PurchaseWindow.human_attribute_name(:date), class: 'control-label' +      = f.input :contains_date, as: :date, label: false, wrapper_html: { class: 'date smart_date' }, class: 'form-control', default: @date, include_blank: @date ? false : true + +  .actions +    = link_to t('actions.erase'), referential_purchase_windows_path, class: 'btn btn-link' +    = f.submit t('actions.filter'), id: 'purchase_window_filter_btn', class: 'btn btn-default' diff --git a/app/views/purchase_windows/_form.html.slim b/app/views/purchase_windows/_form.html.slim new file mode 100644 index 000000000..2101ae6db --- /dev/null +++ b/app/views/purchase_windows/_form.html.slim @@ -0,0 +1,29 @@ += simple_form_for [@referential, @purchase_window], html: { class: 'form-horizontal', id: 'purchase_window_form' }, wrapper: :horizontal_form do |f| +  .row +    .col-lg-12 +      = f.input :name +      = f.input :color, as: :color_select, collection: Chouette::PurchaseWindow.colors_i18n + +  .separator + +  .row +    .col-lg-12 +      .subform +        .nested-head +          .wrapper +            div +              .form-group +                label.control-label +                  = t('simple_form.labels.purchase_window.ranges.begin') +            div +              .form-group +                label.control-label +                  = t('simple_form.labels.purchase_window.ranges.end') +            div + +        = f.simple_fields_for :periods do |period| +          = render 'period_fields', f: period +        .links.nested-linker +          = link_to_add_association t('simple_form.labels.purchase_window.add_a_date_range'), f, :periods, class: 'btn btn-outline-primary' + +  = f.button :submit, t('actions.submit'), class: 'btn btn-default formSubmitr', form: 'purchase_window_form' diff --git a/app/views/purchase_windows/_period_fields.html.slim b/app/views/purchase_windows/_period_fields.html.slim new file mode 100644 index 000000000..95e204554 --- /dev/null +++ b/app/views/purchase_windows/_period_fields.html.slim @@ -0,0 +1,15 @@ +.nested-fields +  - if f.object.errors.has_key? :base +    .row +      .col-lg-12 +        .alert.alert-danger +          - f.object.errors[:base].each do |message| +            p.small = message + +  .wrapper +    div +      = f.input :begin, as: :date, label: false, wrapper_html: { class: 'date smart_date' } +    div +      = f.input :end, as: :date, label: false, wrapper_html: { class: 'date smart_date' } +    div +      = link_to_remove_association '', f, class: 'fa fa-trash', data: { confirm: 'Etes-vous sûr(e) ?' }, title: t('actions.delete') diff --git a/app/views/purchase_windows/edit.html.slim b/app/views/purchase_windows/edit.html.slim new file mode 100644 index 000000000..6354db853 --- /dev/null +++ b/app/views/purchase_windows/edit.html.slim @@ -0,0 +1,7 @@ +- breadcrumb :purchase_window, @referential, @purchase_window +- page_header_content_for @purchase_window +.page_content +  .container-fluid +    .row +      .col-lg-8.col-lg-offset-2.col-md-8.col-md-offset-2.col-sm-10.col-sm-offset-1 +        = render 'form' diff --git a/app/views/purchase_windows/index.html.slim b/app/views/purchase_windows/index.html.slim new file mode 100644 index 000000000..04f9fb0a8 --- /dev/null +++ b/app/views/purchase_windows/index.html.slim @@ -0,0 +1,45 @@ +- breadcrumb :purchase_windows, @referential +- content_for :page_header_actions do +  - if policy(Chouette::PurchaseWindow).create? +    = link_to(t('actions.add'), new_referential_purchase_window_path(@referential), class: 'btn btn-default') + +.page_content +  .container-fluid +    - if params[:q].present? or @purchase_windows.any? +      .row +        .col-lg-12 +          = render 'filters' + +    - if @purchase_windows.any? +      .row +        .col-lg-12 +          = table_builder_2 @purchase_windows, +            [ \ +              TableBuilderHelper::Column.new( \ +                key: :name, \ +                attribute: 'name', \ +                link_to: lambda do |purchase_window| \ +                  referential_purchase_window_path(purchase_window.referential, purchase_window) \ +                end \ +              ), \ +              TableBuilderHelper::Column.new( \ +                key: :color, \ +                attribute: Proc.new { |tt| tt.color ? content_tag(:span, '', class: 'fa fa-circle', style: "color:#{tt.color}") : '-' }\ +              ), \ +              TableBuilderHelper::Column.new( \ +                key: :bounding_dates, \ +                attribute: Proc.new {|w| w.bounding_dates.nil? ? '-' : t('validity_range', debut: l(w.bounding_dates.begin, format: :short), end: l(w.bounding_dates.end, format: :short))},  \ +                sortable: false \ +              ) \ +            ], +            links: [:show], +            cls: 'table has-filter' + +          = new_pagination @purchase_windows, 'pull-right' + +    - unless @purchase_windows.any? +      .row.mt-xs +        .col-lg-12 +          = replacement_msg t('purchase_windows.search_no_results') + += javascript_pack_tag 'date_filters' diff --git a/app/views/purchase_windows/new.html.slim b/app/views/purchase_windows/new.html.slim new file mode 100644 index 000000000..402084167 --- /dev/null +++ b/app/views/purchase_windows/new.html.slim @@ -0,0 +1,6 @@ +- breadcrumb :purchase_windows, @referential +.page_content +  .container-fluid +    .row +      .col-lg-8.col-lg-offset-2.col-md-8.col-md-offset-2.col-sm-10.col-sm-offset-1 +        = render 'form' diff --git a/app/views/purchase_windows/show.html.slim b/app/views/purchase_windows/show.html.slim new file mode 100644 index 000000000..9f3abb267 --- /dev/null +++ b/app/views/purchase_windows/show.html.slim @@ -0,0 +1,20 @@ +- breadcrumb :purchase_window, @referential, @purchase_window +- page_header_content_for @purchase_window +- content_for :page_header_content do +  .row.mb-sm +    .col-lg-12.text-right +      - @purchase_window.action_links.each do |link| +        = link_to link.href, +            method: link.method, +            data: link.data, +            class: 'btn btn-primary' do +              = link.content + +.page_content +  .container-fluid +    .row +      .col-lg-6.col-md-6.col-sm-12.col-xs-12 +        = definition_list t('metadatas'), +          { Chouette::PurchaseWindow.human_attribute_name(:name) => @purchase_window.try(:name), +            'Organisation' => @purchase_window.referential.organisation.name, +            Chouette::PurchaseWindow.human_attribute_name(:date_ranges) => @purchase_window.periods.map{|d| t('validity_range', debut: l(d.begin, format: :short), end: l(d.end, format: :short))}.join('<br>').html_safe } diff --git a/app/views/referential_companies/index.html.slim b/app/views/referential_companies/index.html.slim index de0f7de69..07de2bc9d 100644 --- a/app/views/referential_companies/index.html.slim +++ b/app/views/referential_companies/index.html.slim @@ -46,7 +46,7 @@                  attribute: 'url' \                ) \              ], -            links: [:show, :edit], +            links: [:show],              cls: 'table has-search'            = new_pagination @companies, 'pull-right' diff --git a/app/views/referential_vehicle_journeys/_filters.html.slim b/app/views/referential_vehicle_journeys/_filters.html.slim new file mode 100644 index 000000000..963da8cea --- /dev/null +++ b/app/views/referential_vehicle_journeys/_filters.html.slim @@ -0,0 +1,11 @@ += search_form_for @q, url: referential_vehicle_journeys_path(@referential), html: {method: :get}, class: 'form form-filter' do |f| +  .ffg-row +    .input-group.search_bar +      = f.search_field :published_journey_name_or_objectid_cont, placeholder: t('.published_journey_name_or_objectid'), class: 'form-control' +      span.input-group-btn +        button.btn.btn-default#search-btn type='submit' +          span.fa.fa-search + +  .actions +    = link_to 'Effacer', referential_vehicle_journeys_path(@referential), class: 'btn btn-link' +    = f.submit 'Filtrer', class: 'btn btn-default' diff --git a/app/views/referential_vehicle_journeys/index.html.slim b/app/views/referential_vehicle_journeys/index.html.slim new file mode 100644 index 000000000..394f4a3f7 --- /dev/null +++ b/app/views/referential_vehicle_journeys/index.html.slim @@ -0,0 +1,58 @@ +- breadcrumb :referential_vehicle_journeys, @referential +- content_for :page_header_title, t('.title') + +.page_content +  .container-fluid +    - if params[:q].present? or @vehicle_journeys.present? +      .row +        .col-lg-12 +          = render 'filters' + +    - if @vehicle_journeys.present? +      .row +        .col-lg-12 +          .select_table +            = table_builder_2 @vehicle_journeys, +              [ \ +                TableBuilderHelper::Column.new( \ +                  name: t('objectid'), \ +                  attribute: Proc.new { |n| n.get_objectid.short_id }, \ +                  sortable: false \ +                ), \ +                TableBuilderHelper::Column.new( \ +                  key: :published_journey_name, \ +                  attribute: 'published_journey_name', \ +                  link_to: lambda do |vehicle_journey| \ +                    referential_line_route_vehicle_journeys_path(@referential, vehicle_journey.route.line, vehicle_journey.route) \ +                  end, \ +                  sortable: false \ +                ), \ +                TableBuilderHelper::Column.new( \ +                  key: :line, \ +                  attribute: Proc.new {|v| v.route.line.name}, \ +                  sortable: false \ +                ), \ +                TableBuilderHelper::Column.new( \ +                  key: :route, \ +                  attribute: Proc.new {|v| v.route.name}, \ +                  sortable: false \ +                ), \ +                TableBuilderHelper::Column.new( \ +                  key: :departure_time, \ +                  attribute: Proc.new {|v| v.vehicle_journey_at_stops.first&.departure }, \ +                  sortable: false \ +                ), \ +                TableBuilderHelper::Column.new( \ +                  key: :arrival_time, \ +                  attribute: Proc.new {|v| v.vehicle_journey_at_stops.last&.arrival }, \ +                  sortable: false \ +                ), \ +              ], +              cls: 'table has-filter has-search' + +          = new_pagination @vehicle_journeys, 'pull-right' + +    - unless @vehicle_journeys.any? +      .row.mt-xs +        .col-lg-12 +          = replacement_msg t('.search_no_results') diff --git a/app/views/referentials/_filters.html.slim b/app/views/referentials/_filters.html.slim index c5b6042f0..93fa679df 100644 --- a/app/views/referentials/_filters.html.slim +++ b/app/views/referentials/_filters.html.slim @@ -12,11 +12,11 @@        = f.input :transport_mode_eq_any, collection: @referential.lines.pluck(:transport_mode).uniq.compact, as: :check_boxes, label: false, label_method: lambda{|l| ("<span>" + t("enumerize.transport_mode.#{l}") + "</span>").html_safe}, required: false, wrapper_html: { class: 'checkbox_list' }      .form-group.togglable -      = f.label Chouette::Line.human_attribute_name(:network), required: false, class: 'control-label' +      = f.label  t('activerecord.attributes.referential.networks'), required: false, class: 'control-label'        = f.input :network_id_eq_any, collection: LineReferential.first.networks.order('name').pluck(:id), as: :check_boxes, label: false, label_method: lambda{|l| ("<span>#{LineReferential.first.networks.find(l).name}</span>").html_safe}, required: false, wrapper_html: { class: 'checkbox_list' }      .form-group.togglable -      = f.label Chouette::Line.human_attribute_name(:company), required: false, class: 'control-label' +      = f.label t('activerecord.attributes.referential.companies'), required: false, class: 'control-label'        = f.input :company_id_eq_any, collection: LineReferential.first.companies.order('name').pluck(:id), as: :check_boxes, label: false, label_method: lambda{|l| ("<span>#{LineReferential.first.companies.find(l).name}</span>").html_safe}, required: false, wrapper_html: { class: 'checkbox_list' }    .actions diff --git a/app/views/referentials/_form.html.slim b/app/views/referentials/_form.html.slim index 6f7da84c7..1611ee6dd 100644 --- a/app/views/referentials/_form.html.slim +++ b/app/views/referentials/_form.html.slim @@ -17,7 +17,7 @@      .row        .col-lg-12          - if @referential.errors.has_key? :metadatas -          .row +          .row.metadatas-errors              .col-lg-12                .alert.alert-danger                  - @referential.errors[:metadatas].each do |msg| diff --git a/app/views/referentials/new.html.slim b/app/views/referentials/new.html.slim index 2bed9f912..13d58ee71 100644 --- a/app/views/referentials/new.html.slim +++ b/app/views/referentials/new.html.slim @@ -1,3 +1,5 @@ +- breadcrumb :referentials +  .page_content    .container-fluid      .row diff --git a/app/views/referentials/show.html.slim b/app/views/referentials/show.html.slim index 9852fb0a3..96755359c 100644 --- a/app/views/referentials/show.html.slim +++ b/app/views/referentials/show.html.slim @@ -68,7 +68,7 @@                ), \                TableBuilderHelper::Column.new( \                  key: 'companies.name', \ -                attribute: Proc.new { |n| n.try(:company).try(:name) } \ +                attribute: Proc.new { |n| n&.company&.name || "-" } \                ) \              ],              links: [:show], diff --git a/app/views/routes/show.html.slim b/app/views/routes/show.html.slim index 3adf3e2f6..2b4ebf159 100644 --- a/app/views/routes/show.html.slim +++ b/app/views/routes/show.html.slim @@ -40,7 +40,7 @@                ), \                TableBuilderHelper::Column.new( \                  key: :name, \ -                attribute: Proc.new {|s| s.try(:stop_area).try(:name)}, \ +                attribute: Proc.new { |s| content_tag :span, s.stop_area&.name, class: s.stop_area&.area_type }, \                  link_to: lambda do |stop_point| \                    referential_stop_area_path(@referential, stop_point.stop_area) \                  end \ diff --git a/app/views/stop_area_referentials/show.html.slim b/app/views/stop_area_referentials/show.html.slim index d43333fd9..911006c39 100644 --- a/app/views/stop_area_referentials/show.html.slim +++ b/app/views/stop_area_referentials/show.html.slim @@ -1,13 +1,14 @@  - breadcrumb :stop_area_referential, @stop_area_referential -- content_for :page_header_actions do -  = link_to(t('actions.sync'), sync_stop_area_referential_path(@stop_area_referential), method: :post, class: 'btn btn-default') +- if policy(@stop_area_referential).synchronize? +  - content_for :page_header_actions do +    = link_to(t('actions.sync'), sync_stop_area_referential_path(@stop_area_referential), method: :post, class: 'btn btn-default')  - content_for :page_header_content do    .row.mb-md      .col-lg-12.text-right        = link_to stop_area_referential_stop_areas_path(@stop_area_referential), class: 'btn btn-primary' do          = Referential.human_attribute_name(:stop_areas) -        em.small = " (#{@stop_area_referential.stop_areas.size})" +        em.small = " (#{@stop_area_referential.stop_areas.count})"  - page_header_content_for @stop_area_referential  .page_content diff --git a/app/views/stop_areas/_filters.html.slim b/app/views/stop_areas/_filters.html.slim index 3b99f377c..90368dff4 100644 --- a/app/views/stop_areas/_filters.html.slim +++ b/app/views/stop_areas/_filters.html.slim @@ -12,7 +12,7 @@      .form-group.togglable        = f.label Chouette::StopArea.human_attribute_name(:area_type), required: false, class: 'control-label' -      = f.input :area_type_eq_any, collection: Chouette::StopArea.area_type.options.sort, as: :check_boxes, label: false, label_method: lambda{|w| ("<span>" + t("enumerize.stop_area.area_type.#{w[1]}") + "</span>").html_safe}, required: false, wrapper_html: { class: 'checkbox_list' } +      = f.input :area_type_eq_any, collection: Chouette::AreaType.options, as: :check_boxes, label: false, label_method: lambda{|w| ("<span>" + w[0] + "</span>").html_safe}, required: false, wrapper_html: { class: 'checkbox_list' }    .actions      = link_to 'Effacer', @workbench, class: 'btn btn-link' diff --git a/app/views/stop_areas/_form.html.slim b/app/views/stop_areas/_form.html.slim index 20c7c0468..af8b9d889 100644 --- a/app/views/stop_areas/_form.html.slim +++ b/app/views/stop_areas/_form.html.slim @@ -6,7 +6,10 @@          /= @map.to_html          = f.input :id, as: :hidden          = f.input :name, :input_html => {:title => t("formtastic.titles#{format_restriction_for_locales(@referential)}.stop_area.name")} -        = f.input :area_type, as: :select, :input_html => {:disabled => !@stop_area.new_record?}, :collection => Chouette::StopArea.area_type.options, :include_blank => false + +        = f.input :parent_id, as: :select, :collection => [f.object.parent_id], input_html: { data: { select2_ajax: 'true', url: autocomplete_stop_area_referential_stop_areas_path(@stop_area_referential), initvalue: {id: f.object.parent_id, text: f.object.parent.try(:full_name)}}} + +        = f.input :area_type, as: :select, :input_html => {:disabled => !@stop_area.new_record?}, :collection => Chouette::AreaType.options, :include_blank => false          .location_info            h3 = t("stop_areas.stop_area.localisation") @@ -19,13 +22,16 @@            = f.input :coordinates, :input_html => {:title => t("formtastic.titles#{format_restriction_for_locales(@referential)}.stop_area.coordinates")}, required: true            = f.input :street_name -          /= f.input :country_code, required: format_restriction_for_locales(@referential) == '.hub'            = f.input :zip_code, :input_html => {:title => t("formtastic.titles#{format_restriction_for_locales(@referential)}.stop_area.zip_code")}            = f.input :city_name, required: format_restriction_for_locales(@referential) == '.hub', :input_html => {:title => t("formtastic.titles#{format_restriction_for_locales(@referential)}.stop_area.city_name")} +          = f.input :country_code, as: :country, priority: ['FR', 'GB', 'DE', 'ES'], :include_blank => true          .stop_areas.stop_area.general_info            h3 = t("stop_areas.stop_area.general") +          - if has_feature?(:stop_area_waiting_time) +            = f.input :waiting_time +            = f.input :registration_number, required: format_restriction_for_locales(@referential) == '.hub', :input_html => {:title => t("formtastic.titles#{format_restriction_for_locales(@referential)}.stop_area.registration_number")}            = f.input :fare_code            = f.input :nearest_topic_name, :input_html => {:title => t("formtastic.titles#{format_restriction_for_locales(@referential)}.stop_area.nearest_topic_name")} diff --git a/app/views/stop_areas/autocomplete.rabl b/app/views/stop_areas/autocomplete.rabl new file mode 100644 index 000000000..3208289b5 --- /dev/null +++ b/app/views/stop_areas/autocomplete.rabl @@ -0,0 +1,24 @@ +collection @stop_areas + +node do |stop_area| +  { +    :id                        => stop_area.id, +    :registration_number       => stop_area.registration_number || "", +    :short_registration_number => truncate(stop_area.registration_number, :length => 10) || "", +    :name                      => stop_area.name || "", +    :short_name                => truncate(stop_area.name, :length => 30) || "", +    :zip_code                  => stop_area.zip_code || "", +    :city_name                 => stop_area.city_name || "", +    :short_city_name           => truncate(stop_area.city_name, :length => 15) || "", +    :user_objectid             => stop_area.user_objectid, +    :longitude                 => stop_area.longitude, +    :latitude                  => stop_area.latitude, +    :area_type                 => stop_area.area_type, +    :comment                   => stop_area.comment, +    :text                      => stop_area.full_name +  } +end + +node(:stop_area_path) { |stop_area| +  stop_area_picture_url(stop_area) || "" +} diff --git a/app/views/stop_areas/index.html.slim b/app/views/stop_areas/index.html.slim index c4d880081..63e99fd75 100644 --- a/app/views/stop_areas/index.html.slim +++ b/app/views/stop_areas/index.html.slim @@ -24,7 +24,7 @@                  key: :name, \                  attribute: 'name', \                  link_to: lambda do |stop_area| \ -                  referential_stop_area_path( \ +                  stop_area_referential_stop_area_path( \                      @stop_area_referential, \                      stop_area \                    ) \ @@ -48,10 +48,10 @@                ), \                TableBuilderHelper::Column.new( \                  key: :area_type, \ -                attribute: Proc.new { |s| (s.area_type.nil? ? '-' : t("enumerize.stop_area.area_type.#{s.try(:area_type)}")) } \ +                attribute: Proc.new { |s| Chouette::AreaType.find(s.area_type).try :label } \                ), \              ], -            links: [:show, :edit, :delete], +            links: [:show],              cls: 'table has-filter has-search'            = new_pagination @stop_areas, 'pull-right' diff --git a/app/views/stop_areas/show.html.slim b/app/views/stop_areas/show.html.slim index af673bb25..f9de34a98 100644 --- a/app/views/stop_areas/show.html.slim +++ b/app/views/stop_areas/show.html.slim @@ -15,12 +15,17 @@    .container-fluid      .row        .col-lg-6.col-md-6.col-sm-12.col-xs-12 -        = definition_list t('metadatas'), -          { t('id_reflex') => @stop_area.get_objectid.short_id, -            @stop_area.human_attribute_name(:stop_area_type) => t("area_types.label.#{@stop_area.stop_area_type}"), +        - attributes = { t('id_reflex') => @stop_area.get_objectid.short_id, +            @stop_area.human_attribute_name(:parent) => @stop_area.parent ? link_to(@stop_area.parent.name, stop_area_referential_stop_area_path(@stop_area_referential, @stop_area.parent)) : "-", +            @stop_area.human_attribute_name(:stop_area_type) => Chouette::AreaType.find(@stop_area.area_type).try(:label),              @stop_area.human_attribute_name(:registration_number) => @stop_area.registration_number, -            'Coordonnées' => geo_data(@stop_area, @stop_area_referential), +            } +        - attributes.merge!(@stop_area.human_attribute_name(:waiting_time) => @stop_area.waiting_time_text) if has_feature?(:stop_area_waiting_time) +        - attributes.merge!({ "Coordonnées" => geo_data(@stop_area, @stop_area_referential),              @stop_area.human_attribute_name(:zip_code) => @stop_area.zip_code,              @stop_area.human_attribute_name(:city_name) => @stop_area.city_name, +            @stop_area.human_attribute_name(:country_code) => @stop_area.country_code.presence || '-',              'Etat' => (@stop_area.deleted_at ? 'Supprimé' : 'Actif'), -            @stop_area.human_attribute_name(:comment) => @stop_area.try(:comment) } +            @stop_area.human_attribute_name(:comment) => @stop_area.try(:comment), +            }) +        = definition_list t('metadatas'), attributes diff --git a/app/views/stop_points/_stop_point.html.slim b/app/views/stop_points/_stop_point.html.slim index ca86e339a..e54158cef 100644 --- a/app/views/stop_points/_stop_point.html.slim +++ b/app/views/stop_points/_stop_point.html.slim @@ -5,7 +5,7 @@          = link_to [@referential, stop_point.stop_area], class: "preview", title: "#{Chouette::StopArea.model_name.human.capitalize} #{stop_point.stop_area.name}" do            span.name              span.label.label-primary = stop_point.position + 1 -            = image_tag "map/" + stop_point.stop_area.stop_area_type + ".png" +            = image_tag "map/" + stop_point.stop_area.area_type + ".png"              = truncate(stop_point.stop_area.name, length: 20)    .panel-body @@ -27,4 +27,4 @@          = t(".no_object")        - else          - stop_point.stop_area.lines.each do |line| -          span.label.label-default.line = line.number || truncate( line.name, length: 4 )
\ No newline at end of file +          span.label.label-default.line = line.number || truncate( line.name, length: 4 ) diff --git a/app/views/vehicle_journeys/index.html.slim b/app/views/vehicle_journeys/index.html.slim index 52c1a9728..ebcac8197 100644 --- a/app/views/vehicle_journeys/index.html.slim +++ b/app/views/vehicle_journeys/index.html.slim @@ -1,5 +1,11 @@  - breadcrumb :vehicle_journeys, @referential, @route  - content_for :page_header_title, t('vehicle_journeys.index.title', route: @route.name) +- if @route.opposite_route.present? +  - content_for :page_header_content do +    .row.mb-sm +     .col-lg-12.text-right +       = link_to(t('routes.actions.opposite_route_timetable'), [@referential, @route.line, @route.opposite_route, :vehicle_journeys], class: 'btn btn-primary sticky-action') +  .page_content    .container-fluid @@ -19,6 +25,7 @@    | window.vehicleJourneysPerPage = #{@ppage};    | window.line_footnotes = #{raw @footnotes};    | window.perms = #{raw @perms}; +  | window.features = #{raw @features};    | window.I18n = #{(I18n.backend.send(:translations).to_json).html_safe};  = javascript_pack_tag 'vehicle_journeys/index.js' diff --git a/app/views/vehicle_journeys/show.rabl b/app/views/vehicle_journeys/show.rabl index 830dee8bd..01175a85d 100644 --- a/app/views/vehicle_journeys/show.rabl +++ b/app/views/vehicle_journeys/show.rabl @@ -1,6 +1,6 @@  object @vehicle_journey -[:objectid, :published_journey_name, :published_journey_identifier, :company_id].each do |attr| +[:objectid, :published_journey_name, :published_journey_identifier, :company_id, :comment].each do |attr|    attributes attr, :unless => lambda { |m| m.send( attr).nil?}  end @@ -28,12 +28,22 @@ child(:time_tables, :object_root => false) do |time_tables|    end  end +if has_feature? :purchase_windows +  child(:purchase_windows, :object_root => false) do |purchase_windows| +    attributes :id, :objectid, :name, :color +  end +end +  child :footnotes, :object_root => false do |footnotes|    attributes :id, :code, :label  end  child(:vehicle_journey_at_stops_matrix, :object_root => false) do |vehicle_stops|    node do |vehicle_stop| +    [:id, :connecting_service_id, :boarding_alighting_possibility].map do |att| +      node(att) { vehicle_stop.send(att) ? vehicle_stop.send(att) : nil  } +    end +      node(:dummy) { vehicle_stop.dummy }      node(:stop_area_object_id) do @@ -49,11 +59,7 @@ child(:vehicle_journey_at_stops_matrix, :object_root => false) do |vehicle_stops        vehicle_stop.stop_point.stop_area.city_name      end -    [:id, :connecting_service_id, :boarding_alighting_possibility].map do |att| -      node(att) { vehicle_stop.send(att) ? vehicle_stop.send(att) : nil  } -    end - -    [:arrival_time, :departure_time].map do |att| +    [:arrival_time, :departure_time].each do |att|        node(att) do |vs|          {            hour: vs.send(att).try(:strftime, '%H'), diff --git a/app/views/workbenches/show.html.slim b/app/views/workbenches/show.html.slim index 22869b2d7..1c82c34b7 100644 --- a/app/views/workbenches/show.html.slim +++ b/app/views/workbenches/show.html.slim @@ -57,11 +57,11 @@                    attribute: '' \                  ) \                ], -              selectable: true, +              selectable: ->(ref){ @workbench.referentials.include?(ref) },                links: [:show, :edit],                cls: 'table has-filter has-search' -            = multiple_selection_toolbox([:delete]) +            = multiple_selection_toolbox([:delete], collection_name: 'referentials')            = new_pagination @wbench_refs, 'pull-right' diff --git a/app/workers/referential_cloning_worker.rb b/app/workers/referential_cloning_worker.rb index 6592160ec..e20148055 100644 --- a/app/workers/referential_cloning_worker.rb +++ b/app/workers/referential_cloning_worker.rb @@ -1,32 +1,7 @@  class ReferentialCloningWorker    include Sidekiq::Worker -  # Replace default apartment created schema with clone schema from source referential    def perform(id) -    ref_cloning = ReferentialCloning.find id - -    source_schema = ref_cloning.source_referential.slug -    target_schema = ref_cloning.target_referential.slug - -    clone_schema ref_cloning, source_schema, target_schema -  end - -  private - -  def clone_schema ref_cloning, source_schema, target_schema -    ref_cloning.run! - -    AF83::SchemaCloner -      .new(source_schema, target_schema)  -      .clone_schema - -    ref_cloning.successful! -  rescue Exception => e -    Rails.logger.error "ReferentialCloningWorker : #{e}" -    ref_cloning.failed! -  end - -  def execute_sql sql -    ActiveRecord::Base.connection.execute sql +    ReferentialCloning.find(id).clone!    end  end diff --git a/app/workers/workbench_import_worker.rb b/app/workers/workbench_import_worker.rb index de51efded..6420be835 100644 --- a/app/workers/workbench_import_worker.rb +++ b/app/workers/workbench_import_worker.rb @@ -3,29 +3,25 @@ class WorkbenchImportWorker    include Rails.application.routes.url_helpers    include Configurable +  include ObjectStateUpdater + +  attr_reader :entries, :workbench_import +    # Workers    # =======    def perform(import_id) -    @workbench_import = WorkbenchImport.find(import_id) -    @response         = nil -    @workbench_import.update(status: 'running', started_at: Time.now) -    downloaded  = download -    zip_service = ZipService.new(downloaded) +    @entries = 0 +    @workbench_import ||= WorkbenchImport.find(import_id) + +    workbench_import.update(status: 'running', started_at: Time.now) +    zip_service = ZipService.new(downloaded, allowed_lines)      upload zip_service -    @workbench_import.update(ended_at: Time.now) +    workbench_import.update(ended_at: Time.now)    rescue Zip::Error      handle_corrupt_zip_file    end -  def download -    logger.info  "HTTP GET #{import_url}" -    HTTPService.get_resource( -      host: import_host, -      path: import_path, -      params: {token: @workbench_import.token_download}).body -  end -    def execute_post eg_name, eg_file      logger.info  "HTTP POST #{export_url} (for #{complete_entry_group_name(eg_name)})"      HTTPService.post_resource( @@ -35,48 +31,43 @@ class WorkbenchImportWorker    end    def handle_corrupt_zip_file -    @workbench_import.messages.create(criticity: :error, message_key: 'corrupt_zip_file', message_attributes: {source_filename: @workbench_import.file.file.file}) +    workbench_import.messages.create(criticity: :error, message_key: 'corrupt_zip_file', message_attributes: {source_filename: workbench_import.file.file.file})    end    def upload zip_service      entry_group_streams = zip_service.subdirs -    @workbench_import.update total_steps: entry_group_streams.size      entry_group_streams.each_with_index(&method(:upload_entry_group)) +    workbench_import.update total_steps: @entries    rescue Exception => e      logger.error e.message -    @workbench_import.update( current_step: entry_group_streams.size, status: 'failed' ) +    workbench_import.update( current_step: @entries, status: 'failed' )      raise    end -  def update_object_state entry, count -    @workbench_import.update( current_step: count ) -    unless entry.spurious.empty? -      @workbench_import.messages.create( -        criticity: :warning, -        message_key: 'inconsistent_zip_file', -        message_attributes: { -          'source_filename' => @workbench_import.file.file.file, -          'spurious_dirs'   => entry.spurious.join(', ') -        })  -    end -  end    def upload_entry_group entry, element_count      update_object_state entry, element_count.succ +    return unless entry.ok?      # status = retry_service.execute(&upload_entry_group_proc(entry)) -    eg_name = entry.name -    eg_stream = entry.stream +    upload_entry_group_stream entry.name, entry.stream +  end +  def upload_entry_group_stream eg_name, eg_stream      FileUtils.mkdir_p(Rails.root.join('tmp', 'imports')) -    eg_file = File.new(Rails.root.join('tmp', 'imports', "WorkbenchImport_#{eg_name}_#{$$}.zip"), 'wb').tap do |file| +    File.open(Rails.root.join('tmp', 'imports', "WorkbenchImport_#{eg_name}_#{$$}.zip"), 'wb') do |file|        eg_stream.rewind        file.write eg_stream.read      end -    eg_file.close -    eg_file = File.new(Rails.root.join('tmp', 'imports', "WorkbenchImport_#{eg_name}_#{$$}.zip")) + +    upload_entry_group_tmpfile eg_name, File.new(Rails.root.join('tmp', 'imports', "WorkbenchImport_#{eg_name}_#{$$}.zip")) +  end +     +  def upload_entry_group_tmpfile eg_name, eg_file      result = execute_post eg_name, eg_file      if result && result.status < 400 +      @entries += 1 +      workbench_import.update( current_step: @entries )        result      else        raise StopIteration, result.body @@ -91,7 +82,7 @@ class WorkbenchImportWorker    # =======    def complete_entry_group_name entry_group_name -    [@workbench_import.name, entry_group_name].join("--") +    [workbench_import.name, entry_group_name].join("--")    end    # Constants @@ -111,7 +102,7 @@ class WorkbenchImportWorker      Rails.application.config.rails_host    end    def import_path -    @__import_path__ ||= download_workbench_import_path(@workbench_import.workbench, @workbench_import) +    @__import_path__ ||= download_workbench_import_path(workbench_import.workbench, workbench_import)    end    def import_url      @__import_url__ ||= File.join(import_host, import_path) @@ -119,10 +110,29 @@ class WorkbenchImportWorker    def params file, name      { netex_import: -        { parent_id: @workbench_import.id, -          parent_type: @workbench_import.class.name, -          workbench_id: @workbench_import.workbench_id, -          name: name, -          file: HTTPService.upload(file, 'application/zip', "#{name}.zip") } } +      { parent_id: workbench_import.id, +        parent_type: workbench_import.class.name, +        workbench_id: workbench_import.workbench_id, +        name: name, +        file: HTTPService.upload(file, 'application/zip', "#{name}.zip") } } +  end + +  # Lazy Values +  # =========== + +  def allowed_lines +    @__allowed_lines__ ||= workbench_import.workbench.organisation.lines_set    end +  def downloaded +    @__downloaded__ ||= download_response.body +  end +  def download_response +    @__download_response__ ||= HTTPService.get_resource( +      host: import_host, +      path: import_path, +      params: {token: workbench_import.token_download}).tap do +        logger.info  "HTTP GET #{import_url}" +      end +  end +  end diff --git a/app/workers/workbench_import_worker/object_state_updater.rb b/app/workers/workbench_import_worker/object_state_updater.rb new file mode 100644 index 000000000..67bdc0654 --- /dev/null +++ b/app/workers/workbench_import_worker/object_state_updater.rb @@ -0,0 +1,36 @@ + +class WorkbenchImportWorker +  module ObjectStateUpdater + +    def update_object_state entry, count +      workbench_import.update( total_steps: count ) +      update_spurious entry +      update_foreign_lines entry +    end + + +    private + +    def update_foreign_lines entry +      return if entry.foreign_lines.empty? +      workbench_import.messages.create( +        criticity: :error, +        message_key: 'foreign_lines_in_referential', +        message_attributes: { +          'source_filename' => workbench_import.file.file.file, +          'foreign_lines'   => entry.foreign_lines.join(', ') +        })  +    end + +    def update_spurious entry +      return if entry.spurious.empty? +      workbench_import.messages.create( +        criticity: :error, +        message_key: 'inconsistent_zip_file', +        message_attributes: { +          'source_filename' => workbench_import.file.file.file, +          'spurious_dirs'   => entry.spurious.join(', ') +        })  +    end +  end +end | 
