diff options
Diffstat (limited to 'app/models/merge.rb')
| -rw-r--r-- | app/models/merge.rb | 435 | 
1 files changed, 435 insertions, 0 deletions
| diff --git a/app/models/merge.rb b/app/models/merge.rb new file mode 100644 index 000000000..62bf581d6 --- /dev/null +++ b/app/models/merge.rb @@ -0,0 +1,435 @@ +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 +    referentials.first(3).map { |r| r.name.truncate(10) }.join(',') +  end + +  def full_names +    referentials.map(&:name).to_sentence +  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 +        } +        workbench.output.referentials.new attributes +      end + +    new.referential_suite = output +    new.workbench = workbench +    new.organisation = workbench.organisation +    new.slug = "output_#{workbench.id}_#{created_at.to_i}" +    new.name = I18n.t("merges.referential_name", date: I18n.l(created_at)) + +    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? +            time_table.save! +          else +            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 +          objectid = Chouette::Route.where(objectid: route.objectid).exists? ? nil : route.objectid +          attributes = route.attributes.merge( +            id: nil, +            objectid: objectid, +            # 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| +            objectid = Chouette::StopPoint.where(objectid: stop_point.objectid).exists? ? nil : stop_point.objectid +            attributes = stop_point.attributes.merge( +              id: nil, +              route_id: nil, +              objectid: objectid, +            ) + +            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: :stop_area) + +      journey_patterns_stop_areas_objectids = Hash[ +        journey_patterns.map do |journey_pattern| +          [ journey_pattern.id, journey_pattern.stop_points.map(&:stop_area).map(&:raw_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 +          objectid = Chouette::JourneyPattern.where(objectid: journey_pattern.objectid).exists? ? nil : journey_pattern.objectid +          attributes = journey_pattern.attributes.merge( +            id: nil, +            objectid: objectid, + +            # 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) +          if stop_points.count != stop_areas_objectids.count +            raise "Can't find StopPoints for #{stop_areas_objectids} : #{stop_points.inspect} #{existing_associated_route.stop_points.inspect}" +          end + +          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 for #{journey_pattern.inspect}: #{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 +          objectid = Chouette::VehicleJourney.where(objectid: vehicle_journey.objectid).exists? ? nil : vehicle_journey.objectid +          attributes = vehicle_journey.attributes.merge( +            id: nil, +            objectid: objectid, + +            # 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 +            objectid = Chouette::TimeTable.where(objectid: time_table.objectid).exists? ? nil : time_table.objectid +            candidate_time_table.objectid = objectid + +            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 + +    referentials.update_all merged_at: created_at, archived_at: created_at +  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 | 
