diff options
49 files changed, 1069 insertions, 56 deletions
| diff --git a/INSTALL.md b/INSTALL.md index 28ffdeb4d..5ed8ca9f1 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -137,7 +137,7 @@ RAILS_ENV=test bundle exec rake db:create db:migrate  #### Load seed datas  ```sh -bundle exec rake db:seed +bundle exec rake db:seed:stif  ```  #### Synchronise datas with lines and stop areas referentials diff --git a/app/controllers/merges_controller.rb b/app/controllers/merges_controller.rb new file mode 100644 index 000000000..26e2c2e3c --- /dev/null +++ b/app/controllers/merges_controller.rb @@ -0,0 +1,34 @@ +class MergesController < ChouetteController +  # include PolicyChecker + +  defaults resource_class: Merge +  belongs_to :workbench + +  respond_to :html + +  before_action :set_mergeable_controllers, only: [:new] + +  private + +  def set_mergeable_controllers +    @mergeable_referentials ||= parent.referentials.ready.not_in_referential_suite +    Rails.logger.debug "Mergeables: #{@mergeable_referentials.inspect}" +  end + +  # def build_resource +  #   @import ||= WorkbenchImport.new(*resource_params) do |import| +  #     import.workbench = parent +  #     import.creator   = current_user.name +  #   end +  # end + +  def merge_params +    params.require(:merge).permit( +      referentials: [] +      # :name, +      # :file, +      # :type, +      # :referential_id +    ) +  end +end diff --git a/app/controllers/referentials_controller.rb b/app/controllers/referentials_controller.rb index 83e3bc56a..436d5ccb5 100644 --- a/app/controllers/referentials_controller.rb +++ b/app/controllers/referentials_controller.rb @@ -66,7 +66,7 @@ class ReferentialsController < ChouetteController    def validate      ComplianceControlSetCopyWorker.perform_async(params[:compliance_control_set], params[:id])      flash[:notice] = t('notice.referentials.validate') -    redirect_to(referential_path) +    redirect_to workbench_compliance_check_sets_path(referential.workbench_id)    end    def destroy diff --git a/app/controllers/workbench_outputs_controller.rb b/app/controllers/workbench_outputs_controller.rb new file mode 100644 index 000000000..67ed7569e --- /dev/null +++ b/app/controllers/workbench_outputs_controller.rb @@ -0,0 +1,9 @@ +class WorkbenchOutputsController < ChouetteController +  respond_to :html, only: [:show] +  defaults resource_class: Workbench + +  def show +    @workbench = current_organisation.workbenches.find params[:workbench_id] +    @workbench_merges = @workbench.merges.order("created_at desc").paginate(page: params[:page], per_page: 10) +  end +end diff --git a/app/javascript/vehicle_journeys/components/VehicleJourney.js b/app/javascript/vehicle_journeys/components/VehicleJourney.js index 5f6281487..7ac2a7ce7 100644 --- a/app/javascript/vehicle_journeys/components/VehicleJourney.js +++ b/app/javascript/vehicle_journeys/components/VehicleJourney.js @@ -69,6 +69,7 @@ export default class VehicleJourney extends Component {            >            <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.company ? this.props.value.company.name : '-'}</div>            <div>{this.props.value.journey_pattern.short_id || '-'}</div>            <div>              {time_tables.slice(0,3).map((tt, i)=> diff --git a/app/javascript/vehicle_journeys/components/VehicleJourneys.js b/app/javascript/vehicle_journeys/components/VehicleJourneys.js index dc480d6b4..0cac0344c 100644 --- a/app/javascript/vehicle_journeys/components/VehicleJourneys.js +++ b/app/javascript/vehicle_journeys/components/VehicleJourneys.js @@ -117,6 +117,7 @@ export default class VehicleJourneys extends Component {                    <div className='strong mb-xs'>ID course</div>                    <div>Nom course</div>                    <div>ID mission</div> +                  <div>Transporteur</div>                    <div>Calendriers</div>                    { this.hasFeature('purchase_windows') && <div>Calendriers Commerciaux</div> }                  </div> diff --git a/app/models/chouette/journey_pattern.rb b/app/models/chouette/journey_pattern.rb index 367b00449..366fde188 100644 --- a/app/models/chouette/journey_pattern.rb +++ b/app/models/chouette/journey_pattern.rb @@ -27,7 +27,7 @@ module Chouette      def checksum_attributes        values = self.slice(*['name', 'published_name', 'registration_number']).values -      values << self.stop_points.map(&:stop_area).map(&:user_objectid) +      values << self.stop_points.sort_by(&:position).map(&:stop_area).map(&:user_objectid)        values.flatten      end diff --git a/app/models/chouette/line.rb b/app/models/chouette/line.rb index 2d776e94b..389240ec7 100644 --- a/app/models/chouette/line.rb +++ b/app/models/chouette/line.rb @@ -21,6 +21,7 @@ module Chouette      has_many :journey_patterns, :through => :routes      has_many :vehicle_journeys, :through => :journey_patterns      has_many :routing_constraint_zones, through: :routes +    has_many :time_tables, -> { distinct }, :through => :vehicle_journeys      has_and_belongs_to_many :group_of_lines, :class_name => 'Chouette::GroupOfLine', :order => 'group_of_lines.name' diff --git a/app/models/chouette/time_table.rb b/app/models/chouette/time_table.rb index 74c20f061..db97dd2fa 100644 --- a/app/models/chouette/time_table.rb +++ b/app/models/chouette/time_table.rb @@ -569,5 +569,56 @@ module Chouette        tt.comment      = I18n.t("activerecord.copy", :name => self.comment)        tt      end + +    def intersect_periods!(mask_periods) +      dates.each do |date| +        unless mask_periods.any? { |p| p.include? date.date } +          dates.delete date +        end +      end + +      periods.each do |period| +        mask_periods_with_common_part = mask_periods.select { |p| p.intersect? period.range } + +        if mask_periods_with_common_part.empty? +          self.periods.delete period +        else +          mask_periods_with_common_part.each do |mask_period| +            intersection = (mask_period & period.range) +            period.period_start, period.period_end = intersection.begin, intersection.end +          end +        end +      end +    end + +    def remove_periods!(removed_periods) +      dates.each do |date| +        if removed_periods.any? { |p| p.include? date.date } +          dates.delete date +        end +      end + +      periods.each do |period| +        modified_ranges = removed_periods.inject([period.range]) do |period_ranges, removed_period| +          period_ranges.map { |p| p.remove removed_period }.flatten +        end + +        unless modified_ranges.empty? +          modified_ranges.each_with_index do |modified_range, index| +            new_period = index == 0 ? period : periods.build + +            new_period.period_start, new_period.period_end = +                                     modified_range.min, modified_range.max +          end +        else +          periods.delete period +        end +      end +    end + +    def empty? +      dates.empty? && periods.empty? +    end +    end  end diff --git a/app/models/chouette/time_table_period.rb b/app/models/chouette/time_table_period.rb index ab3e79d7e..d9b707675 100644 --- a/app/models/chouette/time_table_period.rb +++ b/app/models/chouette/time_table_period.rb @@ -42,5 +42,10 @@ module Chouette      def contains?(p)        (p.period_start >= self.period_start && p.period_end <= self.period_end)      end + +    def range +      period_start..period_end +    end +    end -end
\ No newline at end of file +end diff --git a/app/models/chouette/vehicle_journey.rb b/app/models/chouette/vehicle_journey.rb index d84bebf18..d4dc82a56 100644 --- a/app/models/chouette/vehicle_journey.rb +++ b/app/models/chouette/vehicle_journey.rb @@ -70,7 +70,7 @@ module Chouette          attrs << self.published_journey_identifier          attrs << self.try(:company).try(:get_objectid).try(:local_id)          attrs << self.footnotes.map(&:checksum).sort -        attrs << self.vehicle_journey_at_stops.map(&:checksum).sort +        attrs << self.vehicle_journey_at_stops.sort_by { |s| s.stop_point&.position }.map(&:checksum).sort        end      end diff --git a/app/models/concerns/checksum_support.rb b/app/models/concerns/checksum_support.rb index c95e23bcf..b700ef286 100644 --- a/app/models/concerns/checksum_support.rb +++ b/app/models/concerns/checksum_support.rb @@ -26,4 +26,11 @@ module ChecksumSupport        self.checksum = Digest::SHA256.new.hexdigest(self.checksum_source)      end    end + +  def update_checksum! +    set_current_checksum_source +    if checksum_source_changed? +      update checksum: Digest::SHA256.new.hexdigest(checksum_source) +    end +  end  end diff --git a/app/models/generic_attribute_control/min_max.rb b/app/models/generic_attribute_control/min_max.rb index 1c429b9a4..18873b683 100644 --- a/app/models/generic_attribute_control/min_max.rb +++ b/app/models/generic_attribute_control/min_max.rb @@ -2,8 +2,8 @@ module GenericAttributeControl    class MinMax < ComplianceControl      store_accessor :control_attributes, :minimum, :maximum, :target -    validates :minimum, numericality: true, allow_nil: true -    validates :maximum, numericality: true, allow_nil: true +    validates_numericality_of :minimum, allow_nil: true, greater_than_or_equal_to: 0 +    validates_numericality_of :maximum, allow_nil: true, greater_than_or_equal_to: 0      validates :target, presence: true      include MinMaxValuesValidation diff --git a/app/models/merge.rb b/app/models/merge.rb new file mode 100644 index 000000000..4cbbd18ab --- /dev/null +++ b/app/models/merge.rb @@ -0,0 +1,426 @@ +class Merge < ActiveRecord::Base +  extend Enumerize + +  belongs_to :workbench +  validates :workbench, presence: true + +  enumerize :status, in: %w[new pending successful failed running], default: :new + +  has_array_of :referentials, class_name: 'Referential' + +  delegate :output, to: :workbench + +  after_commit :merge, :on => :create + +  def merge +    MergeWorker.perform_async(id) +  end + +  def name +    "Dummy" # FIXME +  end + +  attr_reader :new + +  def merge! +    update started_at: Time.now, status: :running + +    prepare_new + +    referentials.each do |referential| +      merge_referential referential +    end + +    save_current +  rescue => e +    Rails.logger.error "Merge failed: #{e} #{e.backtrace.join("\n")}" +    update status: :failed +    raise e if Rails.env.test? +  ensure +    attributes = { ended_at: Time.now } +    attributes[:status] = :successful if status == :running +    update attributes +  end + +  def prepare_new +    new = +      if workbench.output.current +        Rails.logger.debug "Clone current output" +        Referential.new_from(workbench.output.current, fixme_functional_scope).tap do |clone| +          clone.inline_clone = true +        end +      else +        Rails.logger.debug "Create a new output" +        # 'empty' one +        attributes = { +          workbench: workbench, +          organisation: workbench.organisation, # TODO could be workbench.organisation by default +          name: I18n.t("merges.referential_name"), +        } +        workbench.output.referentials.new attributes +      end + +    new.referential_suite = output +    new.organisation = workbench.organisation +    new.slug = "output_#{workbench.id}_#{Time.now.to_i}" + +    unless new.valid? +      Rails.logger.error "New referential isn't valid : #{new.errors.inspect}" +    end + +    new.save! + +    output.update new: new +    @new = new +  end + +  def merge_referential(referential) +    Rails.logger.debug "Merge #{referential.slug}" + +    metadata_merger = MetadatasMerger.new new, referential +    metadata_merger.merge + +    new.metadatas.delete metadata_merger.empty_metadatas + +    new.save! + +    line_periods = LinePeriods.from_metadatas(referential.metadatas) + +    new.switch do +      line_periods.each do |line_id, periods| +        Rails.logger.debug "Clean data for #{line_id} #{periods.inspect}" + +        new.lines.find(line_id).time_tables.find_each do |time_table| +          time_table.remove_periods! periods +          unless time_table.empty? +            puts "Remove period on #{time_table.inspect}" +            time_table.save! +          else +            puts "Remove TimeTable #{time_table.inspect}" +            time_table.destroy +          end +        end +      end +    end + +    # let's merge data :) + +    # Routes + +    # Always the same pattern : +    # - load models from original Referential +    # - load associated datas (children, checksum for associated models) +    # - switch to new Referential +    # - enumerate loaded models +    # - skip model if its checksum exists "in the same line" +    # - prepare attributes for a fresh model +    # - remove all primary keys +    # - compute an ObjectId (TODO) +    # - process children models as nested attributes +    # - associated other models (by line/checksum) +    # - save! and next one + +    referential_routes = referential.switch do +      referential.routes.all.to_a +    end + +    referential_routes_checksums = Hash[referential_routes.map { |r| [ r.id, r.checksum ] }] + +    referential_stop_points = referential.switch do +      referential.stop_points.all.to_a +    end + +    referential_stop_points_by_route = referential_stop_points.group_by(&:route_id) + +    new.switch do +      referential_routes.each do |route| +        existing_route = new.routes.find_by line_id: route.line_id, checksum: route.checksum +        unless existing_route +          attributes = route.attributes.merge( +            id: nil, +            objectid: "merge:route:#{route.checksum}", #FIXME +            # line_id is the same +            # all other primary must be changed +            opposite_route_id: nil #FIXME +          ) +          new_route = new.routes.build attributes + +          route_stop_points = referential_stop_points_by_route[route.id] + +          # Stop Points +          route_stop_points.each do |stop_point| +            attributes = stop_point.attributes.merge( +              id: nil, +              route_id: nil, +              objectid: "merge:stop_point:#{route.checksum}-#{stop_point.position}", #FIXME +            ) + +            new_route.stop_points.build attributes +          end + +          new_route.save! + +          if new_route.checksum != route.checksum +            raise "Checksum has changed: #{route.inspect} #{new_route.inspect}" +          end +        end +      end +    end + +    # JourneyPatterns + +    referential_journey_patterns, referential_journey_patterns_stop_areas_objectids = referential.switch do +      journey_patterns = referential.journey_patterns.includes(:stop_points) + +      journey_patterns_stop_areas_objectids = Hash[ +        journey_patterns.map do |journey_pattern| +          [ journey_pattern.id, journey_pattern.stop_points.map(&:stop_area).map(&:objectid)] +        end +      ] + +      [journey_patterns, journey_patterns_stop_areas_objectids] +    end + +    referential_journey_patterns_checksums = Hash[referential_journey_patterns.map { |j| [ j.id, j.checksum ] }] + +    new.switch do +      referential_journey_patterns.each do |journey_pattern| +        # find parent route by checksum +        # TODO add line_id for security +        associated_route_checksum = referential_routes_checksums[journey_pattern.route_id] +        existing_associated_route = new.routes.find_by checksum: associated_route_checksum + +        existing_journey_pattern = new.journey_patterns.find_by route_id: existing_associated_route.id, checksum: journey_pattern.checksum + +        unless existing_journey_pattern +          attributes = journey_pattern.attributes.merge( +            id: nil, + +            objectid: "merge:journey_pattern:#{existing_associated_route.checksum}-#{journey_pattern.checksum}", #FIXME + +            # all other primary must be changed +            route_id: existing_associated_route.id, + +            departure_stop_point_id: nil, # FIXME +            arrival_stop_point_id: nil +          ) + +          stop_areas_objectids = referential_journey_patterns_stop_areas_objectids[journey_pattern.id] + +          stop_points = existing_associated_route.stop_points.joins(:stop_area).where("stop_areas.objectid": stop_areas_objectids).order(:position) +          attributes.merge!(stop_points: stop_points) + +          new_journey_pattern = new.journey_patterns.create! attributes +          if new_journey_pattern.checksum != journey_pattern.checksum +            raise "Checksum has changed: #{journey_pattern.checksum_source} #{new_journey_pattern.checksum_source}" +          end +        end +      end +    end + +    # Vehicle Journeys + +    referential_vehicle_journeys = referential.switch do +      referential.vehicle_journeys.includes(:vehicle_journey_at_stops).all.to_a +    end + +    new.switch do +      referential_vehicle_journeys.each do |vehicle_journey| +        # find parent journey pattern by checksum +        # TODO add line_id for security +        associated_journey_pattern_checksum = referential_journey_patterns_checksums[vehicle_journey.journey_pattern_id] +        existing_associated_journey_pattern = new.journey_patterns.find_by checksum: associated_journey_pattern_checksum + +        existing_vehicle_journey = new.vehicle_journeys.find_by journey_pattern_id: existing_associated_journey_pattern.id, checksum: vehicle_journey.checksum + +        unless existing_vehicle_journey +          attributes = vehicle_journey.attributes.merge( +            id: nil, + +            objectid: "merge:vehicle_journey:#{existing_associated_journey_pattern.checksum}-#{vehicle_journey.checksum}", #FIXME + +            # all other primary must be changed +            route_id: existing_associated_journey_pattern.route_id, +            journey_pattern_id: existing_associated_journey_pattern.id, +          ) +          new_vehicle_journey = new.vehicle_journeys.build attributes + +          # Create VehicleJourneyAtStops + +          vehicle_journey.vehicle_journey_at_stops.each_with_index do |vehicle_journey_at_stop, index| +            at_stop_attributes = vehicle_journey_at_stop.attributes.merge( +              id: nil, +              stop_point_id: existing_associated_journey_pattern.stop_points[index].id +            ) +            new_vehicle_journey.vehicle_journey_at_stops.build at_stop_attributes +          end + +          new_vehicle_journey.save! + +          if new_vehicle_journey.checksum != vehicle_journey.checksum +            raise "Checksum has changed: #{vehicle_journey.checksum_source} #{new_vehicle_journey.checksum_source}" +          end +        end + +      end +    end + +    # Time Tables + +    referential_time_tables_by_id, referential_time_tables_with_lines = referential.switch do +      time_tables_by_id = Hash[referential.time_tables.includes(:dates, :periods).all.to_a.map { |t| [t.id, t] }] + +      time_tables_with_associated_lines = +        referential.time_tables.joins(vehicle_journeys: {route: :line}).pluck("lines.id", :id, "vehicle_journeys.checksum") + +      # Because TimeTables will be modified according metadata periods +      # we're loading timetables per line (line is associated to a period list) +      # +      # line_id: [ { time_table.id, vehicle_journey.checksum } ] +      time_tables_by_lines = time_tables_with_associated_lines.inject(Hash.new { |h,k| h[k] = [] }) do |hash, row| +        hash[row.shift] << {id: row.first, vehicle_journey_checksum: row.second} +        hash +      end + +      [ time_tables_by_id, time_tables_by_lines ] +    end + +    new.switch do +      referential_time_tables_with_lines.each do |line_id, time_tables_properties| +        # Because TimeTables will be modified according metadata periods +        # we're loading timetables per line (line is associated to a period list) +        line = workbench.line_referential.lines.find(line_id) + +        time_tables_properties.each do |properties| +          time_table = referential_time_tables_by_id[properties[:id]] + +          # we can't test if TimeTable already exist by checksum +          # because checksum is modified by intersect_periods! + +          attributes = time_table.attributes.merge( +            id: nil, +            comment: "Ligne #{line.name} - #{time_table.comment}", +            calendar_id: nil +          ) +          candidate_time_table = new.time_tables.build attributes + +          time_table.dates.each do |date| +            date_attributes = date.attributes.merge( +              id: nil, +              time_table_id: nil +            ) +            candidate_time_table.dates.build date_attributes +          end +          time_table.periods.each do |period| +            period_attributes = period.attributes.merge( +              id: nil, +              time_table_id: nil +            ) +            candidate_time_table.periods.build period_attributes +          end + +          candidate_time_table.intersect_periods! line_periods.periods(line_id) + +          # FIXME +          candidate_time_table.set_current_checksum_source +          candidate_time_table.update_checksum + +          # after intersect_periods!, the checksum is the expected one +          # we can search an existing TimeTable + +          existing_time_table = line.time_tables.find_by checksum: candidate_time_table.checksum + +          unless existing_time_table +            # FIXME use real ObjectId +            # Referential id is (temporary) used because the "same" TimeTable can be defined in several merged Referentials +            # and checksum are modified by clean/remove_periods! but this temporary object id is constant +            candidate_time_table.objectid = "merge:time_table:#{line.id}-#{candidate_time_table.checksum}-#{referential.id}:LOC" + +            candidate_time_table.save! + +            # Checksum is changed by #intersect_periods +            # if new_time_table.checksum != time_table.checksum +            #   raise "Checksum has changed: #{time_table.checksum_source} #{new_time_table.checksum_source}" +            # end + +            existing_time_table = candidate_time_table +          end + +          # associate VehicleJourney + +          associated_vehicle_journey = line.vehicle_journeys.find_by!(checksum: properties[:vehicle_journey_checksum]) +          associated_vehicle_journey.time_tables << existing_time_table +        end +      end +    end +  end + +  def save_current +    output.update current: new, new: nil +    output.current.update referential_suite: output +  end + +  def fixme_functional_scope +    if attribute = workbench.organisation.sso_attributes.try(:[], "functional_scope") +      JSON.parse(attribute) +    end +  end + +  def child_change + +  end + +  class MetadatasMerger + +    attr_reader :merge_metadatas, :referential +    def initialize(merge_referential, referential) +      @merge_metadatas = merge_referential.metadatas +      @referential = referential +    end + +    delegate :metadatas, to: :referential, prefix: :referential + +    def merge +      referential_metadatas.each do |metadata| +        merge_one metadata +      end +    end + +    def merged_line_metadatas(line_id) +      merge_metadatas.select do |m| +        m.line_ids.include? line_id +      end +    end + +    def merge_one(metadata) +      metadata.line_ids.each do |line_id| +        line_metadatas = merged_line_metadatas(line_id) + +        metadata.periodes.each do |period| +          line_metadatas.each do |m| +            m.periodes = m.periodes.map do |existing_period| +              existing_period.remove period +            end.flatten +          end + +          attributes = { +            line_ids: [line_id], +            periodes: [period], +            referential_source_id: referential.id, +            created_at: metadata.created_at # TODO check required dates +          } + +          # line_metadatas should not contain conflicted metadatas +          merge_metadatas << ReferentialMetadata.new(attributes) +        end +      end +    end + +    def empty_metadatas +      merge_metadatas.select { |m| m.periodes.empty? } +    end + + +  end + +end diff --git a/app/models/referential.rb b/app/models/referential.rb index 1cdda9e6a..a5d5acbf9 100644 --- a/app/models/referential.rb +++ b/app/models/referential.rb @@ -61,6 +61,7 @@ class Referential < ActiveRecord::Base    scope :include_metadatas_lines, ->(line_ids) { where('referential_metadata.line_ids && ARRAY[?]::bigint[]', line_ids) }    scope :order_by_validity_period, ->(dir) { joins(:metadatas).order("unnest(periodes) #{dir}") }    scope :order_by_lines, ->(dir) { joins(:metadatas).group("referentials.id").order("sum(array_length(referential_metadata.line_ids,1)) #{dir}") } +  scope :not_in_referential_suite, -> { where referential_suite_id: nil }    def save_with_table_lock_timeout(options = {})      save_without_table_lock_timeout(options) @@ -144,6 +145,22 @@ class Referential < ActiveRecord::Base      Chouette::PurchaseWindow.all    end +  def routes +    Chouette::Route.all +  end + +  def journey_patterns +    Chouette::JourneyPattern.all +  end + +  def stop_points +    Chouette::StopPoint.all +  end +   +  def compliance_check_sets +    ComplianceCheckSet.all +  end +    before_validation :define_default_attributes    def define_default_attributes @@ -151,10 +168,22 @@ class Referential < ActiveRecord::Base      self.objectid_format ||= workbench.objectid_format if workbench    end -  def switch +  def switch(&block)      raise "Referential not created" if new_record? -    Apartment::Tenant.switch!(slug) -    self + +    unless block_given? +      Rails.logger.debug "Referential switch to #{slug}" +      Apartment::Tenant.switch! slug +      self +    else +      result = nil +      Apartment::Tenant.switch slug do +        Rails.logger.debug "Referential switch to #{slug}" +        result = yield +      end +      Rails.logger.debug "Referential back" +      result +    end    end    def self.new_from(from, functional_scope) @@ -296,7 +325,7 @@ class Referential < ActiveRecord::Base      overlapped_referential_ids.present?    end -  validate :detect_overlapped_referentials +  validate :detect_overlapped_referentials, unless: :in_referential_suite?    def detect_overlapped_referentials      self.class.where(id: overlapped_referential_ids).each do |referential| @@ -305,8 +334,19 @@ class Referential < ActiveRecord::Base      end    end +  def in_referential_suite? +    referential_suite_id.present? +  end + +  attr_accessor :inline_clone    def clone_schema -    ReferentialCloning.create(source_referential: created_from, target_referential: self) +    cloning = ReferentialCloning.new source_referential: created_from, target_referential: self + +    if inline_clone +      cloning.clone! +    else +      cloning.save! +    end    end    def create_schema diff --git a/app/models/referential_cloning.rb b/app/models/referential_cloning.rb index 24117e6c8..a2b23e819 100644 --- a/app/models/referential_cloning.rb +++ b/app/models/referential_cloning.rb @@ -8,19 +8,22 @@ class ReferentialCloning < ActiveRecord::Base      ReferentialCloningWorker.perform_async(id)    end -  def clone! +  def clone_with_status!      run! - -    AF83::SchemaCloner -      .new(source_referential.slug, target_referential.slug) -      .clone_schema - +    clone!      successful!    rescue Exception => e      Rails.logger.error "Clone failed : #{e}" +    Rails.logger.error e.backtrace.join('\n')      failed!    end +  def clone! +    AF83::SchemaCloner +      .new(source_referential.slug, target_referential.slug) +      .clone_schema +  end +    private    aasm column: :status do diff --git a/app/models/referential_suite.rb b/app/models/referential_suite.rb index 93c2c3f36..4f825628c 100644 --- a/app/models/referential_suite.rb +++ b/app/models/referential_suite.rb @@ -1,7 +1,7 @@  class ReferentialSuite < ActiveRecord::Base    belongs_to :new, class_name: 'Referential'    validate def validate_consistent_new -    return true if new_id.nil? +    return true if new_id.nil? || new.nil?      return true if new.referential_suite_id == id      errors.add(:inconsistent_new,                 I18n.t('referential_suites.errors.inconsistent_new', name: new.name)) @@ -9,11 +9,11 @@ class ReferentialSuite < ActiveRecord::Base    belongs_to :current, class_name: 'Referential'    validate def validate_consistent_current -    return true if current_id.nil? +    return true if current_id.nil? || current.nil?      return true if current.referential_suite_id == id      errors.add(:inconsistent_current,                 I18n.t('referential_suites.errors.inconsistent_current', name: current.name))    end -  has_many :referentials +  has_many :referentials, -> { order "created_at desc" }  end diff --git a/app/models/vehicle_journey_control/delta.rb b/app/models/vehicle_journey_control/delta.rb index 077dd6c4a..f061b9fdd 100644 --- a/app/models/vehicle_journey_control/delta.rb +++ b/app/models/vehicle_journey_control/delta.rb @@ -3,7 +3,7 @@ module VehicleJourneyControl      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-3" end    end diff --git a/app/models/vehicle_journey_control/speed.rb b/app/models/vehicle_journey_control/speed.rb index 14fad9139..e5e331b50 100644 --- a/app/models/vehicle_journey_control/speed.rb +++ b/app/models/vehicle_journey_control/speed.rb @@ -2,8 +2,8 @@ module VehicleJourneyControl    class Speed < ComplianceControl      store_accessor :control_attributes, :minimum, :maximum -    validates :minimum, numericality: true, allow_nil: true -    validates :maximum, numericality: true, allow_nil: true +    validates_numericality_of :minimum, allow_nil: true, greater_than_or_equal_to: 0 +    validates_numericality_of :maximum, allow_nil: true, greater_than_or_equal_to: 0      include MinMaxValuesValidation      def self.default_code; "3-VehicleJourney-2" end diff --git a/app/models/workbench.rb b/app/models/workbench.rb index e36589210..3190246ae 100644 --- a/app/models/workbench.rb +++ b/app/models/workbench.rb @@ -14,6 +14,7 @@ class Workbench < ActiveRecord::Base    has_many :workbench_imports    has_many :compliance_check_sets    has_many :compliance_control_sets +  has_many :merges    validates :name, presence: true    validates :organisation, presence: true @@ -29,7 +30,7 @@ class Workbench < ActiveRecord::Base      if line_ids.empty?        Referential.none      else -      Referential.joins(:metadatas).where(['referential_metadata.line_ids && ARRAY[?]::bigint[]', line_ids]).ready +      Referential.joins(:metadatas).where(['referential_metadata.line_ids && ARRAY[?]::bigint[]', line_ids]).ready.not_in_referential_suite      end    end diff --git a/app/policies/merge_policy.rb b/app/policies/merge_policy.rb new file mode 100644 index 000000000..82eb72e08 --- /dev/null +++ b/app/policies/merge_policy.rb @@ -0,0 +1,15 @@ +class MergePolicy < ApplicationPolicy +  class Scope < Scope +    def resolve +      scope +    end +  end + +  def create? +    user.has_permission?('merges.create') +  end + +  def update? +    user.has_permission?('merges.update') +  end +end diff --git a/app/views/merges/_form.html.slim b/app/views/merges/_form.html.slim new file mode 100644 index 000000000..ff85ad76b --- /dev/null +++ b/app/views/merges/_form.html.slim @@ -0,0 +1,7 @@ += simple_form_for merge, as: :merge, url: workbench_merges_path(workbench), html: {class: 'form-horizontal', id: 'wb_merge_form'}, wrapper: :horizontal_form do |form| + +  .row +    .col-lg-12 +      = form.input :referentials, :collection => @mergeable_referentials, include_blank: false, input_html: { multiple: true, 'data-select2ed': true } + +  = form.button :submit, t('actions.submit'), class: 'btn btn-default formSubmitr', form: 'wb_merge_form' diff --git a/app/views/merges/new.html.slim b/app/views/merges/new.html.slim new file mode 100644 index 000000000..dab4bdf4e --- /dev/null +++ b/app/views/merges/new.html.slim @@ -0,0 +1,7 @@ +- breadcrumb :merges, @workbench + +.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', merge: @merge, workbench: @workbench diff --git a/app/views/merges/show.html.slim b/app/views/merges/show.html.slim new file mode 100644 index 000000000..579995ebf --- /dev/null +++ b/app/views/merges/show.html.slim @@ -0,0 +1,13 @@ +- breadcrumb :merge, @merge +- page_header_content_for @merge + +.page_content +  .container-fluid +    .row +      .col-lg-6.col-md-6.col-sm-12.col-xs-12 +        = definition_list t('metadatas'), +          { @merge.class.human_attribute_name(:referentials) => @merge.referentials.map(&:name).join(', '), +            @merge.class.human_attribute_name(:status) => @merge.status, +            @merge.class.human_attribute_name(:created_at) => @merge.created_at, +            @merge.class.human_attribute_name(:started_at) => @merge.started_at, +            @merge.class.human_attribute_name(:ended_at) => @merge.ended_at } diff --git a/app/views/stif/dashboards/_dashboard.html.slim b/app/views/stif/dashboards/_dashboard.html.slim index f3cd01f46..64e7d4f96 100644 --- a/app/views/stif/dashboards/_dashboard.html.slim +++ b/app/views/stif/dashboards/_dashboard.html.slim @@ -56,9 +56,8 @@      .panel.panel-default        .panel-heading          h3.panel-title.with_actions -          div -            = t('.calendars') -            span.badge.ml-xs = @dashboard.calendars.count if @dashboard.calendars.present? +          = I18n.t("calendars.index.title") +          span.badge.ml-xs = @dashboard.calendars.count if @dashboard.calendars.present?            div              = link_to '', calendars_path, class: ' fa fa-chevron-right pull-right', title: t('.see') diff --git a/app/views/workbench_outputs/show.html.slim b/app/views/workbench_outputs/show.html.slim new file mode 100644 index 000000000..67dc6e8d4 --- /dev/null +++ b/app/views/workbench_outputs/show.html.slim @@ -0,0 +1,33 @@ +/ PageHeader + +- breadcrumb :workbench_output, @workbench +- content_for :page_header_title, t('.title') +- content_for :page_header_content do +  .row.mb-sm +    .col-lg-12.text-right +      = link_to t('.see_current_output'), referential_path(@workbench.output.current), class: 'btn btn-primary' if @workbench.output&.current +      = link_to t('merges.actions.create'), new_workbench_merge_path(@workbench), class: 'btn btn-primary' + +.page_content +  .container-fluid +      .row +        .col-lg-12 +          = table_builder_2 @workbench_merges, +            [ \ +              TableBuilderHelper::Column.new( \ +                key: :status, \ +                attribute: Proc.new { |n| import_status(n.status) }, \ +              ), \ +              TableBuilderHelper::Column.new( \ +                key: :started_at, \ +                attribute: Proc.new { |n| l(n.started_at, format: :long) if n.started_at }, \ +              ), \ +              TableBuilderHelper::Column.new( \ +                key: :creator, \ +                attribute: 'creator' \ +              ) \ +            ], +            links: [], +            cls: 'table has-search' + +          = new_pagination @workbench_merges, 'pull-right' diff --git a/app/views/workbenches/show.html.slim b/app/views/workbenches/show.html.slim index 1c82c34b7..fe0b05330 100644 --- a/app/views/workbenches/show.html.slim +++ b/app/views/workbenches/show.html.slim @@ -6,6 +6,7 @@        - if policy(Referential).create?          = link_to t('actions.import'), workbench_imports_path(@workbench), class: 'btn btn-primary'          = link_to t('actions.add'), new_referential_path(workbench_id: @workbench), class: 'btn btn-primary' +      = link_to t('workbenches.actions.show_output'), workbench_output_path(@workbench), class: 'btn btn-primary'  .page_content    .container-fluid diff --git a/app/workers/merge_worker.rb b/app/workers/merge_worker.rb new file mode 100644 index 000000000..8a085a25a --- /dev/null +++ b/app/workers/merge_worker.rb @@ -0,0 +1,7 @@ +class MergeWorker +  include Sidekiq::Worker + +  def perform(id) +    Merge.find(id).merge! +  end +end diff --git a/app/workers/referential_cloning_worker.rb b/app/workers/referential_cloning_worker.rb index e20148055..e24baa90c 100644 --- a/app/workers/referential_cloning_worker.rb +++ b/app/workers/referential_cloning_worker.rb @@ -2,6 +2,6 @@ class ReferentialCloningWorker    include Sidekiq::Worker    def perform(id) -    ReferentialCloning.find(id).clone! +    ReferentialCloning.find(id).clone_with_status!    end  end diff --git a/config/breadcrumbs.rb b/config/breadcrumbs.rb index 3f6503308..ce5cf5b0f 100644 --- a/config/breadcrumbs.rb +++ b/config/breadcrumbs.rb @@ -6,6 +6,21 @@ crumb :workbench do |workbench|    link workbench.name, workbench_path(workbench)  end +crumb :workbench_output do |workbench| +  link I18n.t('workbench_outputs.show.title'), workbench_output_path(workbench) +  parent :workbench, current_offer_workbench +end + +crumb :merges do |workbench| +  link I18n.t('merges.index.title'), workbench_output_path(workbench) +  parent :workbench, workbench +end + +crumb :merge do |merge| +  link breadcrumb_name(merge), workbench_merge_path(merge.workbench, merge) +  parent :merges, merge.workbench +end +  crumb :referential do |referential|    link breadcrumb_name(referential), referential_path(referential)    parent :workbench, current_offer_workbench diff --git a/config/initializers/apartment.rb b/config/initializers/apartment.rb index 69204a5d7..8becd23c2 100644 --- a/config/initializers/apartment.rb +++ b/config/initializers/apartment.rb @@ -77,7 +77,8 @@ Apartment.configure do |config|      'ComplianceCheckSet',      'ComplianceCheckBlock',      'ComplianceCheckResource', -    'ComplianceCheckMessage' +    'ComplianceCheckMessage', +    'Merge'    ]    # use postgres schemas? diff --git a/config/locales/calendars.fr.yml b/config/locales/calendars.fr.yml index 88cb275ff..f9eaf1be5 100644 --- a/config/locales/calendars.fr.yml +++ b/config/locales/calendars.fr.yml @@ -25,28 +25,28 @@ fr:      standard_calendars: Calendriers standards      standard_calendar: Calendrier standard      actions: -      new: Ajouter un calendrier -      edit: Editer cet calendrier -      destroy: Supprimer cet calendrier -      destroy_confirm: Etes vous sûr de supprimer cet calendrier ? +      new: Ajouter un modèle de calendrier +      edit: Editer ce modèle de calendrier +      destroy: Supprimer ce modèle de calendrier +      destroy_confirm: Etes vous sûr de supprimer ce modèle de calendrier ?      errors:        overlapped_periods: Une autre période chevauche cette période        short_period: "Une période doit être d'une durée de deux jours minimum"      index: -      title: Calendriers +      title: Modèles de calendrier        all: Tous        shared: Partagées        not_shared: Non partagées -      search_no_results: Aucun calendrier ne correspond à votre recherche +      search_no_results: Aucun modèle de calendrier ne correspond à votre recherche        date: Date      new: -      title: Ajouter un calendrier +      title: Ajouter un modèle de calendrier      create: -      title: Ajouter un calendrier +      title: Ajouter un modèle de calendrier      edit: -      title: Editer le calendrier %{name} +      title: Editer le modèle de calendrier %{name}      show: -      title: Calendrier %{name} +      title: Modèle de calendrier %{name}    simple_form:      labels:        calendar: @@ -59,8 +59,8 @@ fr:    activerecord:      models:        calendar: -        one: "calendrier" -        other: "calendriers" +        one: "modèle de calendrier" +        other: "modèles de calendrier"      attributes:        calendar:          name: Nom diff --git a/config/locales/merges.yml b/config/locales/merges.yml new file mode 100644 index 000000000..1e2df2459 --- /dev/null +++ b/config/locales/merges.yml @@ -0,0 +1,17 @@ +fr: +  merges: +    index: +      title: "Finalisations de l'offre" +    new: +      title: "Nouvelle finalisation de l'offre" +  activerecord: +    models: +      merge: "Finalisation de l'offre" +    attributes: +      merge: +        created_at: "Créé le" +        started_at: Démarrage +        ended_at: Achevé à +        status: "Etat" +        creator: "Opérateur" +        referentials: "Jeux de données" diff --git a/config/locales/workbenches.fr.yml b/config/locales/workbenches.fr.yml index d76255e86..eff53c2d6 100644 --- a/config/locales/workbenches.fr.yml +++ b/config/locales/workbenches.fr.yml @@ -6,6 +6,11 @@ fr:        zero: "Aucun jeu de données dans cet espace de travail"        one: "1 jeu de données dans cet espace de travail"        other: "#{count} jeux de données dans cet espace de travail" +    actions: +      show_output: "Offre finalisée" +  workbench_outputs: +    show: +      title: "Finalisations de l'offre"    activerecord:      models:        workbench: diff --git a/config/routes.rb b/config/routes.rb index bf796a385..8b5faff03 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -15,6 +15,9 @@ ChouetteIhm::Application.routes.draw do        get :executed, on: :member        resources :compliance_checks, only: [:show]      end + +    resource :output, controller: :workbench_outputs +    resources :merges    end    devise_for :users, :controllers => { diff --git a/db/migrate/20171212152452_create_merges.rb b/db/migrate/20171212152452_create_merges.rb new file mode 100644 index 000000000..7915bd91b --- /dev/null +++ b/db/migrate/20171212152452_create_merges.rb @@ -0,0 +1,16 @@ +class CreateMerges < ActiveRecord::Migration +  def change +    create_table :merges do |t| +      t.bigint :workbench_id, index: true, foreign_key: true +      t.bigint :referential_ids, array: true + +      t.string :creator +      t.string :status + +      t.datetime :started_at +      t.datetime :ended_at + +      t.timestamps null: false +    end +  end +end diff --git a/db/schema.rb b/db/schema.rb index 67c42f568..667b95c84 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -15,8 +15,8 @@ ActiveRecord::Schema.define(version: 20171227113809) do    # These are extensions that must be enabled in order to support this database    enable_extension "plpgsql" -  enable_extension "hstore"    enable_extension "postgis" +  enable_extension "hstore"    enable_extension "unaccent"    create_table "access_links", id: :bigserial, force: :cascade do |t| @@ -522,6 +522,19 @@ ActiveRecord::Schema.define(version: 20171227113809) do    add_index "lines", ["registration_number"], name: "lines_registration_number_key", using: :btree    add_index "lines", ["secondary_company_ids"], name: "index_lines_on_secondary_company_ids", using: :gin +  create_table "merges", id: :bigserial, force: :cascade do |t| +    t.integer  "workbench_id",    limit: 8 +    t.integer  "referential_ids", limit: 8,              array: true +    t.string   "creator" +    t.string   "status" +    t.datetime "started_at" +    t.datetime "ended_at" +    t.datetime "created_at",                null: false +    t.datetime "updated_at",                null: false +  end + +  add_index "merges", ["workbench_id"], name: "index_merges_on_workbench_id", using: :btree +    create_table "networks", id: :bigserial, force: :cascade do |t|      t.string   "objectid",                      null: false      t.integer  "object_version",      limit: 8 diff --git a/lib/line_periods.rb b/lib/line_periods.rb new file mode 100644 index 000000000..c176a7a08 --- /dev/null +++ b/lib/line_periods.rb @@ -0,0 +1,35 @@ +class LinePeriods + +  def initialize +    @periods_by_line = Hash.new { |h,k| h[k] = [] } +  end + +  def add(line_id, period) +    @periods_by_line[line_id] << period +  end + +  def each(&block) +    @periods_by_line.each do |line_id, periods| +      yield line_id, periods +    end +  end + +  def periods(line_id) +    @periods_by_line[line_id] +  end + +  def self.from_metadatas(metadatas) +    line_periods = new + +    metadatas.each do |metadata| +      metadata.line_ids.each do |line_id| +        metadata.periodes.each do |period| +          line_periods.add(line_id, period) +        end +      end +    end + +    line_periods +  end + +end diff --git a/lib/range_ext.rb b/lib/range_ext.rb index f1df5e70d..e7e0e903f 100644 --- a/lib/range_ext.rb +++ b/lib/range_ext.rb @@ -1,8 +1,24 @@  class Range    def intersection(other) -    return nil if (self.max < other.min or other.max < self.min) +    return nil unless intersect?(other)      [self.min, other.min].max..[self.max, other.max].min    end    alias_method :&, :intersection + +  def intersect?(other) +    self.max > other.min and other.max > self.min +  end + +  def remove(other) +    return self if (self.max < other.min or other.max < self.min) + +    [].tap do |remaining| +      remaining << (self.min..other.min-1) if self.min < other.min +      remaining << (other.max+1..self.max) if other.max < self.max +      remaining.compact! +    end +  end +  alias_method :-, :remove +  end diff --git a/lib/stif/permission_translator.rb b/lib/stif/permission_translator.rb index 4acf42884..9e0feb9b8 100644 --- a/lib/stif/permission_translator.rb +++ b/lib/stif/permission_translator.rb @@ -21,6 +21,7 @@ module Stif          calendars          footnotes          imports +        merges          journey_patterns          referentials          routes diff --git a/spec/factories/chouette_time_table.rb b/spec/factories/chouette_time_table.rb index a3ff63b2f..81a08ca2a 100644 --- a/spec/factories/chouette_time_table.rb +++ b/spec/factories/chouette_time_table.rb @@ -25,6 +25,7 @@ FactoryGirl.define do          end_date = start_date + 10        end        time_table.save_shortcuts +      time_table.update_checksum!      end    end diff --git a/spec/factories/chouette_vehicle_journey.rb b/spec/factories/chouette_vehicle_journey.rb index 5f64bd502..7d63a2e58 100644 --- a/spec/factories/chouette_vehicle_journey.rb +++ b/spec/factories/chouette_vehicle_journey.rb @@ -30,6 +30,7 @@ FactoryGirl.define do                     :arrival_time    => "2000-01-01 #{arrival_time} UTC",                     :departure_time  => "2000-01-01 #{departure_time} UTC")            end +          vehicle_journey.update_checksum!          end          factory :vehicle_journey_odd do diff --git a/spec/lib/range_ext_spec.rb b/spec/lib/range_ext_spec.rb index 9c44608b9..eee488c91 100644 --- a/spec/lib/range_ext_spec.rb +++ b/spec/lib/range_ext_spec.rb @@ -1,6 +1,6 @@  require 'range_ext'  RSpec.describe Range do -  context "intersection" do +  describe "#intersection" do      it "is nil (sic) for two distinct ranges" do        expect( (1..2).intersection(3..4) ).to be_nil      end @@ -15,4 +15,53 @@ RSpec.describe Range do        expect( (2..4) & (1..3) ).to eq 2..3      end    end + +  describe "intersect?" do +    it 'is true when the given range includes begin' do +      expect( (2..4).intersect? (1..3) ).to be_truthy +    end + +    it 'is true when the given range includes end' do +      expect( (2..4).intersect? (3..5) ).to be_truthy +    end + +    it 'is true when the given range includes both begin and end' do +      expect( (2..4).intersect? (1..5) ).to be_truthy +    end + +    it 'is true when the given range is the same' do +      expect( (2..4).intersect? (2..4) ).to be_truthy +    end + +    it 'is false when the given range is after' do +      expect( (2..4).intersect? (5..7) ).to be_falsey +    end + +    it 'is false when the given range is before' do +      expect( (2..4).intersect? (0..2) ).to be_falsey +    end +  end + +  context "remove" do +    it "is unchanged when the given range has no intersection" do +      expect( (1..2).remove(3..4) ).to eq 1..2 +      expect( (3..4).remove(1..2) ).to eq 3..4 +    end + +    it "is nil for two equal ranges" do +      expect( (1..2).remove(1..2) ).to be_empty +    end + +    it "is the begin of the range when given range intersect the end" do +      expect( (5..10).remove(8..15) ).to eq [5..7] +    end + +    it "is the end of the range when given range intersect the begin" do +      expect( (5..10).remove(1..6) ).to eq [7..10] +    end + +    it "is the two remaing ranges when given range is the middle" do +      expect( (1..10).remove(4..6) ).to eq [1..3, 7..10] +    end +  end  end diff --git a/spec/models/chouette/time_table_spec.rb b/spec/models/chouette/time_table_spec.rb index 677308fc8..d4a726740 100644 --- a/spec/models/chouette/time_table_spec.rb +++ b/spec/models/chouette/time_table_spec.rb @@ -1187,4 +1187,99 @@ end          expect(subject.tag_list.size).to eq(2)        end    end + +  describe "#intersect_periods!" do +    let(:time_table) { Chouette::TimeTable.new } +    let(:periods) do +      [ +        Date.new(2018, 1, 1)..Date.new(2018, 2, 1), +      ] +    end + +    it "remove a date not included in given periods" do +      time_table.dates.build date: Date.new(2017,12,31) +      time_table.intersect_periods! periods +      expect(time_table.dates).to be_empty +    end + +    it "keep a date included in given periods" do +      time_table.dates.build date: Date.new(2018,1,15) +      expect{time_table.intersect_periods! periods}.to_not change(time_table, :dates) +    end + +    it "remove a period not included in given periods" do +      time_table.periods.build period_start: Date.new(2017,12,1), period_end: Date.new(2017,12,31) +      time_table.intersect_periods! periods +      expect(time_table.periods).to be_empty +    end + +    it "modify a start period if not included in given periods" do +      period = time_table.periods.build period_start: Date.new(2017,12,1), period_end: Date.new(2018,1,15) +      time_table.intersect_periods! periods +      expect(period.period_start).to eq(Date.new(2018, 1, 1)) +    end + +    it "modify a end period if not included in given periods" do +      period = time_table.periods.build period_start: Date.new(2018,1,15), period_end: Date.new(2018,3,1) +      time_table.intersect_periods! periods +      expect(period.period_end).to eq(Date.new(2018, 2, 1)) +    end + +    it "keep a period included in given periods" do +      time_table.periods.build period_start: Date.new(2018,1,10), period_end: Date.new(2018,1,20) +      expect{time_table.intersect_periods! periods}.to_not change(time_table, :periods) +    end + +  end + +  describe "#remove_periods!" do +    let(:time_table) { Chouette::TimeTable.new } +    let(:periods) do +      [ +        Date.new(2018, 1, 1)..Date.new(2018, 2, 1), +      ] +    end + +    it "remove a date included in given periods" do +      time_table.dates.build date: Date.new(2018,1,15) +      time_table.remove_periods! periods +      expect(time_table.dates).to be_empty +    end + +    it "keep a date not included in given periods" do +      time_table.dates.build date: Date.new(2017,12,31) +      expect{time_table.remove_periods! periods}.to_not change(time_table, :dates) +    end + +    it "modify a end period if included in given periods" do +      period = time_table.periods.build period_start: Date.new(2017,12,1), period_end: Date.new(2018,1,15) +      time_table.remove_periods! periods +      expect(period.period_end).to eq(Date.new(2017, 12, 31)) +    end + +    it "modify a start period if included in given periods" do +      period = time_table.periods.build period_start: Date.new(2018,1,15), period_end: Date.new(2018,3,1) +      time_table.remove_periods! periods +      expect(period.period_start).to eq(Date.new(2018, 2, 2)) +    end + +    it "remove a period included in given periods" do +      time_table.periods.build period_start: Date.new(2018,1,10), period_end: Date.new(2018,1,20) +      time_table.remove_periods! periods +      expect(time_table.periods).to be_empty +    end + +    it "split a period including a given period" do +      time_table.periods.build period_start: Date.new(2017,12,1), period_end: Date.new(2018,3,1) +      time_table.remove_periods! periods + +      expected_ranges = [ +        Date.new(2017,12,1)..Date.new(2017,12,31), +        Date.new(2018,2,2)..Date.new(2018,3,1) +      ] +      expect(time_table.periods.map(&:range)).to eq(expected_ranges) +    end + +  end +  end diff --git a/spec/models/merge_spec.rb b/spec/models/merge_spec.rb new file mode 100644 index 000000000..92f8f74b1 --- /dev/null +++ b/spec/models/merge_spec.rb @@ -0,0 +1,58 @@ +require "rails_helper" + +RSpec.describe Merge do + +  it "should work" do +    stop_area_referential = FactoryGirl.create :stop_area_referential +    10.times { FactoryGirl.create :stop_area, stop_area_referential: stop_area_referential } + +    line_referential = FactoryGirl.create :line_referential +    company = FactoryGirl.create :company, line_referential: line_referential +    10.times { FactoryGirl.create :line, line_referential: line_referential, company: company, network: nil } + +    workbench = FactoryGirl.create :workbench, line_referential: line_referential, stop_area_referential: stop_area_referential + +    referential_metadata = FactoryGirl.create(:referential_metadata, lines: line_referential.lines.limit(3)) + +    referential = FactoryGirl.create :referential, +                                      workbench: workbench, +                                      organisation: workbench.organisation, +                                      metadatas: [referential_metadata] + +    factor = 1 + +    referential.switch do +      line_referential.lines.each do |line| +        factor.times do +          stop_areas = stop_area_referential.stop_areas.order("random()").limit(5) +          FactoryGirl.create :route, line: line, stop_areas: stop_areas, stop_points_count: 0 +        end +      end + +      referential.routes.each do |route| +        factor.times do +          FactoryGirl.create :journey_pattern, route: route, stop_points: route.stop_points.sample(3) +        end +      end + +      referential.journey_patterns.each do |journey_pattern| +        factor.times do +          FactoryGirl.create :vehicle_journey, journey_pattern: journey_pattern, company: company +        end +      end + +      shared_time_table = FactoryGirl.create :time_table + +      referential.vehicle_journeys.each do |vehicle_journey| +        vehicle_journey.time_tables << shared_time_table + +        specific_time_table = FactoryGirl.create :time_table +        vehicle_journey.time_tables << specific_time_table +      end +    end + +    merge = Merge.create!(workbench: referential.workbench, referentials: [referential, referential]) +    merge.merge! +  end + +end diff --git a/spec/models/referential_cloning_spec.rb b/spec/models/referential_cloning_spec.rb index 4327c98aa..815e05a67 100644 --- a/spec/models/referential_cloning_spec.rb +++ b/spec/models/referential_cloning_spec.rb @@ -36,40 +36,50 @@ RSpec.describe ReferentialCloning, :type => :model do      let(:cloner) { double } -    before do -      allow(AF83::SchemaCloner).to receive(:new).and_return cloner -      allow(cloner).to receive(:clone_schema) -    end -      it 'creates a schema cloner with source and target schemas and clone schema' do        expect(AF83::SchemaCloner).to receive(:new).with(source_referential.slug, target_referential.slug).and_return(cloner)        expect(cloner).to receive(:clone_schema)        referential_cloning.clone!      end +  end + +  describe '#clone_with_status!' do +    let(:referential_cloning) do +      ReferentialCloning.new(target_referential: Referential.new(slug: "target")) +    end + +    before do +      allow(referential_cloning).to receive(:clone!) +    end + +    it 'invokes clone! method' do +      expect(referential_cloning).to receive(:clone!) +      referential_cloning.clone_with_status! +    end      context 'when clone_schema is performed without error' do        it "should have successful status" do -        referential_cloning.clone! +        referential_cloning.clone_with_status!          expect(referential_cloning.status).to eq("successful")        end      end      context 'when clone_schema raises an error' do        it "should have failed status" do -        expect(cloner).to receive(:clone_schema).and_raise("#fail") -        referential_cloning.clone! +        expect(referential_cloning).to receive(:clone!).and_raise("#fail") +        referential_cloning.clone_with_status!          expect(referential_cloning.status).to eq("failed")        end      end      it "defines started_at" do -      referential_cloning.clone! +      referential_cloning.clone_with_status!        expect(referential_cloning.started_at).not_to be_nil      end      it "defines ended_at" do -      referential_cloning.clone! +      referential_cloning.clone_with_status!        expect(referential_cloning.ended_at).not_to be_nil      end diff --git a/spec/models/referential_spec.rb b/spec/models/referential_spec.rb index 7816e7232..45881333f 100644 --- a/spec/models/referential_spec.rb +++ b/spec/models/referential_spec.rb @@ -125,4 +125,19 @@ describe Referential, :type => :model do      end    end +  context "used in a ReferentialSuite" do +    before do +      ref.referential_suite_id = 42 +    end + +    it "return true to in_referential_suite?" do +      expect(ref.in_referential_suite?).to be(true) +    end + +    it "don't use detect_overlapped_referentials in validation" do +      expect(ref).to_not receive(:detect_overlapped_referentials) +      ref.valid? +    end +  end +  end diff --git a/spec/support/permissions.rb b/spec/support/permissions.rb index dde530871..95afd6c1c 100644 --- a/spec/support/permissions.rb +++ b/spec/support/permissions.rb @@ -18,6 +18,7 @@ module Support          calendars          footnotes          imports +        merges          journey_patterns          referentials          routes diff --git a/spec/workers/referential_cloning_worker_spec.rb b/spec/workers/referential_cloning_worker_spec.rb index 2b9a54805..74e83c3b2 100644 --- a/spec/workers/referential_cloning_worker_spec.rb +++ b/spec/workers/referential_cloning_worker_spec.rb @@ -10,7 +10,7 @@ RSpec.describe ReferentialCloningWorker do      it "invokes the clone! method of the associated ReferentialCloning" do        expect(ReferentialCloning).to receive(:find).with(id).and_return(referential_cloning) -      expect(referential_cloning).to receive(:clone!) +      expect(referential_cloning).to receive(:clone_with_status!)        worker.perform(id)      end | 
