af83/chouette-core

View on GitHub
app/services/clean.rb

Summary

Maintainability
A
2 hrs
Test Coverage
# frozen_string_literal: true

#
# Provides all Clean strategies
#
# = Examples
#
# Clean timetables for a date range for a given Line
#
#   scope = Clean::Scope::Line.new(Clean::Scope::Referential.new(referential), line)
#   Clean::Timetable::InPeriod.new(scope, (Date.today-30)..Date.today).clean!
#
module Clean
  class Base
    include Measurable

    attr_reader :scope

    def initialize(scope)
      @scope = scope
    end

    include AroundMethod
    around_method :clean!

    def around_clean!(&block)
      scope.switch(&block)
    end
  end

  # Manages/restricts the scope of cleaned data
  module Scope
    class Referential
      attr_reader :referential

      def initialize(referential)
        @referential = referential
      end

      delegate :routes, :vehicle_journeys, :journey_patterns, :service_counts,
               :time_tables, :time_table_dates, :time_table_periods, :metadatas,
               :companies, to: :referential

      alias timetables time_tables
      alias timetable_periods time_table_periods
      alias timetable_dates time_table_dates

      def restricted_metadata(metadata)
        metadata
      end

      def switch(&block)
        referential.switch(&block)
      end

      def unmodifiable_timetables?
        false
      end
    end

    class Line
      def initialize(scope, line_or_line_id)
        line_id = line_or_line_id.try(:id) || line_or_line_id
        @scope = scope
        @line_id = line_id
      end
      attr_reader :scope, :line_id

      delegate :switch, to: :scope

      def routes
        scope.routes.where(line_id: line_id)
      end

      def journey_patterns
        scope.journey_patterns.where(route: routes)
      end

      # Returns all Vehicle Journeys associated to the selected Line
      def vehicle_journeys
        scope.vehicle_journeys.where(route: routes)
      end

      # Returns all TimeTables associated to the selected Line (via the Vehicle Journeys)
      def timetables
        scope.timetables.joins(:vehicle_journeys).where('vehicle_journeys.id' => vehicle_journeys)
      end

      def timetable_periods
        scope.timetable_periods.where(time_table: timetables)
      end

      def timetable_dates
        scope.timetable_dates.where(time_table: timetables)
      end

      def service_counts
        scope.service_counts.for_lines(line_id)
      end

      def metadatas
        scope.metadatas.include_lines([line_id])
      end

      # Returns a (unsaved) metadata which only covers the current
      # scope and which can be modified/deleted safely, without
      # changing a metadata out of the scope
      def restricted_metadata(metadata)
        # return nil unless metadata.line_ids.include? line_id
        return metadata if metadata.line_ids == [line_id]

        restricted = metadata.dup
        restricted.id = nil
        restricted.line_ids = [line_id]

        metadata.line_ids.delete line_id
        metadata.save!

        restricted
      end

      # Find TimeTables which are used by lines outside of this scope
      def unmodifiable_timetables?
        timetables.shared_by_several_lines?
      end
    end
  end

  class InPeriod < Base
    attr_accessor :period

    def initialize(scope, period)
      super scope
      @period = period
    end

    def clean!
      [
        Timetable::InPeriod.new(scope, period),
        ServiceCount::InPeriod.new(scope, period),
        VehicleJourney::WithoutTimetable.new(scope),
        JourneyPattern::WithoutVehicleJourney.new(scope),
        Route::WithoutJourneyPattern.new(scope)
      ].each(&:clean!)
    end
  end

  module VehicleJourney
    class WithoutTimetable < Base
      def clean!
        scope.vehicle_journeys.without_any_time_table.clean!
      end
    end

    class NullifyCompany < Base
      def clean!
        scope.vehicle_journeys.left_joins(:company)
             .where.not(company_id: nil).where(companies: { id: nil })
             .update_all(company_id: nil) # rubocop:disable Rails/SkipsModelValidations
      end
    end
  end

  module JourneyPattern
    class WithoutVehicleJourney < Base
      def clean!
        scope.journey_patterns.without_any_vehicle_journey.clean!
      end
    end
  end

  module Route
    class WithoutJourneyPattern < Base
      def clean!
        scope.routes.without_any_journey_pattern.clean!
      end
    end
  end

  module ServiceCount
    class InPeriod < Base
      attr_accessor :period

      def initialize(scope, period)
        super scope
        @period = period
      end

      def service_counts
        scope.service_counts.between(period.min, period.max)
      end

      def clean!
        service_counts.delete_all
      end
    end
  end

  module Timetable
    # Delete TimeTables without periods and dates
    #
    # Removes associations with vehicle journeys
    class Empty < Base
      def clean!
        scope.timetables.empty.delete_all
      end
    end

    # Delete or truncate TimeTables with periods or/and dates in the given period
    class InPeriod < Base
      attr_accessor :period

      def initialize(scope, period)
        super scope
        @period = period
      end

      def clean!
        # TODO: Duplicate TimeTables used by another Line in the same Period
        raise "Can't modify shared timetables" if scope.unmodifiable_timetables?

        # Delete dates
        Date::InPeriod.new(scope, period).clean!
        # Delete and truncate periods in the period
        Period::InPeriod.new(scope, period).clean!

        # Remove TimeTables without dates or periods
        Empty.new(scope).clean!

        scope.timetables.update_shortcuts
      end
    end

    module Date
      class InPeriod < Base
        attr_accessor :period

        def initialize(scope, period)
          super scope
          @period = period
        end

        def dates
          scope.timetable_dates.in_date_range(period)
        end

        def clean!
          dates.delete_all
        end
      end

      # Remove excluded TimeTable dates when no period is defined into their TimeTable
      class ExcludedWithoutPeriod < Base
        def dates
          scope.timetable_dates.where time_table: scope.timetables.without_periods, in_out: false
        end

        def clean!
          dates.delete_all
        end
      end
    end

    module Period
      class InPeriod < Base
        attr_accessor :period

        def initialize(scope, period)
          super scope
          @period = period
        end

        delegate :timetable_periods, to: :scope

        # Delete periods which starts and finishs into the period
        def clean_into
          criteria = ['period_start between :min and :max and period_end between :min and :max', {
            min: period.min, max: period.max
          }]
          timetable_periods.where(criteria).delete_all
        end

        # Truncate periods which starts before the period/range and finishs into the period/range
        def truncate_before
          criteria = ['period_start < :min and period_end between :min and :max', { min: period.min, max: period.max }]
          timetable_periods.where(criteria).update_all period_end: period.min - 1 # rubocop:disable Rails/SkipsModelValidations
        end

        # Truncate periods which starts into the period and finishs after the period
        def truncate_after
          criteria = ['period_start between :min and :max and period_end > :max', { min: period.min, max: period.max }]
          timetable_periods.where(criteria).update_all period_start: period.max + 1 # rubocop:disable Rails/SkipsModelValidations
        end

        def split_over
          criteria = ['period_start < :min and period_end > :max', { min: period.min, max: period.max }]
          scope.timetable_periods.where(criteria).select(:id, :time_table_id, :period_start,
                                                         :period_end).find_each do |initial_period|
            # Duplicate the period to create a new TimeTablePeriod after the clean period
            after_period = initial_period.dup
            # Update period_end of the initial period
            initial_period.update period_end: period.min - 1
            after_period.id = nil
            after_period.period_start = period.max + 1
            after_period.save!
          end
        end

        def clean!
          clean_into
          truncate_before
          truncate_after
          split_over

          scope.timetable_periods.transform_in_dates
        end
      end
    end
  end

  module Metadata
    class Cleaner
      def initialize(scope)
        @scope = scope
      end
      attr_reader :scope

      # Modify the given metadata (after scope restriction)
      # Save it or destroy it if empty
      def clean(metadata, &block)
        metadata = scope.restricted_metadata(metadata)

        block.call metadata

        if metadata.periodes.empty?
          metadata.destroy
        else
          metadata.save!
        end
      end
    end

    class Before < Base
      attr_accessor :before

      def initialize(scope, before)
        super scope
        @before = before
      end

      # Truncate or remove the period from the given metadata (after scope restriction)
      def clean_metadata(metadata)
        before_period = Range.new(metadata.bounds.min, before)

        Cleaner.new(scope).clean(metadata) do |m|
          m.periodes = Range.remove(m.periodes, before_period)
        end
      end

      def metadatas
        scope.metadatas.start_before before
      end

      def clean!
        metadatas.find_each do |metadata|
          clean_metadata metadata
        end
      end
    end

    class InPeriod < Base
      attr_accessor :period

      def initialize(scope, period)
        super scope
        @period = period
      end

      # Remove the period from the given metadata (after scope restriction)
      def clean_metadata(metadata)
        Cleaner.new(scope).clean(metadata) do |m|
          m.periodes = Range.remove(m.periodes, period)
        end
      end

      def metadatas
        scope.metadatas.include_daterange period
      end

      def clean!
        metadatas.find_each do |metadata|
          clean_metadata metadata
        end
      end
    end
  end
end