diff options
| -rw-r--r-- | Gemfile | 2 | ||||
| -rw-r--r-- | Gemfile.lock | 9 | ||||
| -rw-r--r-- | app/models/chouette/company.rb | 2 | ||||
| -rw-r--r-- | app/models/chouette/line.rb | 2 | ||||
| -rw-r--r-- | app/models/chouette/stop_area.rb | 2 | ||||
| -rw-r--r-- | app/models/concerns/application_days_support.rb | 8 | ||||
| -rw-r--r-- | app/models/import/gtfs.rb | 302 | ||||
| -rw-r--r-- | app/models/import/workbench.rb | 21 | ||||
| -rw-r--r-- | app/models/referential.rb | 8 | ||||
| -rw-r--r-- | app/workers/gtfs_import_worker.rb | 7 | ||||
| -rw-r--r-- | db/migrate/20180403100007_add_registration_number_indexes.rb | 7 | ||||
| -rw-r--r-- | lib/gtfs/time.rb | 28 | ||||
| -rw-r--r-- | spec/fixtures/google-sample-feed.zip | bin | 0 -> 3217 bytes | |||
| -rw-r--r-- | spec/lib/gtfs/time_spec.rb | 27 | ||||
| -rw-r--r-- | spec/models/import/gtfs_spec.rb | 276 | 
15 files changed, 673 insertions, 28 deletions
| @@ -143,6 +143,8 @@ gem 'puma', '~> 3.10.0'  gem 'newrelic_rpm'  gem 'letter_opener' +gem 'gtfs' +  group :development do    gem 'capistrano', '2.13.5'    gem 'capistrano-ext' diff --git a/Gemfile.lock b/Gemfile.lock index 0c1243593..4fb77eeb9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -258,6 +258,10 @@ GEM      google-analytics-rails (1.1.0)      gretel (3.0.9)        rails (>= 3.1.0) +    gtfs (0.2.5) +      multi_json +      rake +      rubyzip (~> 1.1)      has_scope (0.7.0)        actionpack (>= 4.1, < 5.1)        activesupport (>= 4.1, < 5.1) @@ -321,7 +325,7 @@ GEM      minitest (5.11.3)      money (6.10.1)        i18n (>= 0.6.4, < 1.0) -    multi_json (1.12.1) +    multi_json (1.13.1)      multi_test (0.1.2)      multi_xml (0.6.0)      multipart-post (2.0.0) @@ -420,7 +424,7 @@ GEM        thor (>= 0.18.1, < 2.0)      rainbow (2.2.2)        rake -    rake (12.3.0) +    rake (12.3.1)      ransack (1.8.3)        actionpack (>= 3.0)        activerecord (>= 3.0) @@ -633,6 +637,7 @@ DEPENDENCIES    georuby-ext (= 0.0.5)    google-analytics-rails    gretel +  gtfs    has_array_of!    htmlbeautifier    i18n-js diff --git a/app/models/chouette/company.rb b/app/models/chouette/company.rb index cb2266a3d..9d5737a6c 100644 --- a/app/models/chouette/company.rb +++ b/app/models/chouette/company.rb @@ -9,7 +9,7 @@ module Chouette      has_many :lines -    validates_format_of :registration_number, :with => %r{\A[0-9A-Za-z_-]+\Z}, :allow_nil => true, :allow_blank => true +    # validates_format_of :registration_number, :with => %r{\A[0-9A-Za-z_-]+\Z}, :allow_nil => true, :allow_blank => true      validates_presence_of :name      validates_format_of :url, :with => %r{\Ahttps?:\/\/([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?\Z}, :allow_nil => true, :allow_blank => true diff --git a/app/models/chouette/line.rb b/app/models/chouette/line.rb index 51851fc2e..4b5d1a68d 100644 --- a/app/models/chouette/line.rb +++ b/app/models/chouette/line.rb @@ -29,7 +29,7 @@ module Chouette      # validates_presence_of :network      # validates_presence_of :company -    validates_format_of :registration_number, :with => %r{\A[\d\w_\-]+\Z}, :allow_nil => true, :allow_blank => true +    # validates_format_of :registration_number, :with => %r{\A[\d\w_\-]+\Z}, :allow_nil => true, :allow_blank => true      validates_format_of :stable_id, :with => %r{\A[\d\w_\-]+\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_format_of :color, :with => %r{\A[0-9a-fA-F]{6}\Z}, :allow_nil => true, :allow_blank => true diff --git a/app/models/chouette/stop_area.rb b/app/models/chouette/stop_area.rb index 9f28b7ee6..4ddc7403b 100644 --- a/app/models/chouette/stop_area.rb +++ b/app/models/chouette/stop_area.rb @@ -33,7 +33,7 @@ module Chouette      after_update :journey_patterns_control_route_sections,                  if: Proc.new { |stop_area| ['boarding_position', 'quay'].include? stop_area.stop_area_type } -    validates_format_of :registration_number, :with => %r{\A[\d\w_\-]+\Z}, :allow_blank => true +    # validates_format_of :registration_number, :with => %r{\A[\d\w_:\-]+\Z}, :allow_blank => true      validates_presence_of :name      validates_presence_of :kind      validates_presence_of :latitude, :if => :longitude diff --git a/app/models/concerns/application_days_support.rb b/app/models/concerns/application_days_support.rb index 2d00b5847..6086d9580 100644 --- a/app/models/concerns/application_days_support.rb +++ b/app/models/concerns/application_days_support.rb @@ -10,8 +10,10 @@ module ApplicationDaysSupport    SUNDAY    = 256    EVERYDAY  = MONDAY | TUESDAY | WEDNESDAY | THURSDAY | FRIDAY | SATURDAY | SUNDAY +  ALL_DAYS = %w(monday tuesday wednesday thursday friday saturday sunday).freeze +    def display_day_types -    %w(monday tuesday wednesday thursday friday saturday sunday).select{ |d| self.send(d) }.map{ |d| self.human_attribute_name(d).first(2)}.join(', ') +    ALL_DAYS.select{ |d| self.send(d) }.map{ |d| self.human_attribute_name(d).first(2)}.join(', ')    end    def day_by_mask(flag) @@ -39,6 +41,10 @@ module ApplicationDaysSupport      def self.day_by_mask(int_day_types,flag)        int_day_types & flag == flag      end + +    def self.all_days +      ALL_DAYS +    end    end    def valid_days diff --git a/app/models/import/gtfs.rb b/app/models/import/gtfs.rb index 03cf49e60..70f448132 100644 --- a/app/models/import/gtfs.rb +++ b/app/models/import/gtfs.rb @@ -1,36 +1,296 @@ -require 'net/http'  class Import::Gtfs < Import::Base -  before_destroy :destroy_non_ready_referential +  after_commit :launch_worker, :on => :create -  after_commit :launch_java_import, on: :create -  before_save def abort_unless_referential -    self.status = 'aborted' unless referential +  def launch_worker +    GtfsImportWorker.perform_async id    end -  def launch_java_import -    return if self.class.finished_statuses.include?(status) -    threaded_call_boiv_iev +  def import +    update status: 'running', started_at: Time.now + +    import_without_status +    update status: 'successful', ended_at: Time.now +  rescue Exception => e +    update status: 'failed', ended_at: Time.now +    Rails.logger.error "Error in GTFS import: #{e} #{e.backtrace.join('\n')}" +  ensure +    notify_parent +    referential&.update ready: true +  end + +  def self.accept_file?(file) +    Zip::File.open(file) do |zip_file| +      zip_file.glob('agency.txt').size == 1 +    end +  rescue Exception => e +    Rails.logger.debug "Error in testing GTFS file: #{e}" +    return false +  end + +  def create_referential +    self.referential ||= Referential.create!( +      name: "GTFS Import", +      organisation_id: workbench.organisation_id, +      workbench_id: workbench.id, +      metadatas: [referential_metadata] +    ) +  end + +  def referential_metadata +    registration_numbers = source.routes.map(&:id) +    line_ids = line_referential.lines.where(registration_number: registration_numbers).pluck(:id) + +    start_dates, end_dates = source.calendars.map { |c| [c.start_date, c.end_date ] }.transpose +    excluded_dates = source.calendar_dates.select { |d| d.exception_type == "2" }.map(&:date) + +    min_date = Date.parse (start_dates + [excluded_dates.min]).compact.min +    max_date = Date.parse (end_dates + [excluded_dates.max]).compact.max + +    ReferentialMetadata.new line_ids: line_ids, periodes: [min_date..max_date] +  end + +  attr_accessor :local_file +  def local_file +    @local_file ||= download_local_file    end -  private +  attr_accessor :download_host +  def download_host +    @download_host ||= Rails.application.config.rails_host.gsub("http://","") +  end -  def destroy_non_ready_referential -    if referential && !referential.ready -      referential.destroy +  def local_temp_directory +    Rails.application.config.try(:import_temporary_directory) || +      Rails.root.join('tmp', 'imports') +  end + +  def local_temp_file(&block) +    Tempfile.open("chouette-import", local_temp_directory) do |file| +      file.binmode +      yield file      end    end -  def threaded_call_boiv_iev -    Thread.new(&method(:call_boiv_iev)) +  def download_path +    Rails.application.routes.url_helpers.download_workbench_import_path(workbench, id, token: token_download)    end -  def call_boiv_iev -    Rails.logger.error("Begin IEV call for import") -    Net::HTTP.get(URI("#{Rails.configuration.iev_url}/boiv_iev/referentials/importer/new?id=#{id}")) -    Rails.logger.error("End IEV call for import") -  rescue Exception => e -    logger.error "IEV server error : #{e.message}" -    logger.error e.backtrace.inspect +  def download_local_file +    local_temp_file do |file| +      begin +        Net::HTTP.start(download_host) do |http| +          http.request_get(download_path) do |response| +            response.read_body do |segment| +              file.write segment +            end +          end +        end +      ensure +        file.close +      end + +      file.path +    end +  end + +  def source +    @source ||= ::GTFS::Source.build local_file +  end + +  delegate :line_referential, :stop_area_referential, to: :workbench + +  def prepare_referential +    import_agencies +    import_stops +    import_routes + +    create_referential +    referential.switch +  end + +  def import_without_status +    prepare_referential + +    import_calendars +    import_trips +    import_stop_times +  end + +  def import_agencies +    Chouette::Company.transaction do +      source.agencies.each do |agency| +        company = line_referential.companies.find_or_initialize_by(registration_number: agency.id) +        company.attributes = { name: agency.name } + +        save_model company +      end +    end +  end + +  def import_stops +    Chouette::StopArea.transaction do +      source.stops.each do |stop| +        stop_area = stop_area_referential.stop_areas.find_or_initialize_by(registration_number: stop.id) + +        stop_area.name = stop.name +        stop_area.area_type = stop.location_type == "1" ? "zdlp" : "zdep" +        stop_area.parent = stop_area_referential.stop_areas.find_by!(registration_number: stop.parent_station) if stop.parent_station.present? +        stop_area.latitude, stop_area.longitude = stop.lat, stop.lon +        stop_area.kind = "commercial" + +        # TODO correct default timezone + +        save_model stop_area +      end +    end +  end + +  def import_routes +    Chouette::Line.transaction do +      source.routes.each do |route| +        line = line_referential.lines.find_or_initialize_by(registration_number: route.id) +        line.name = route.long_name.presence || route.short_name +        line.number = route.short_name +        line.published_name = route.long_name + +        line.company = line_referential.companies.find_by(registration_number: route.agency_id) if route.agency_id.present? + +        # TODO transport mode + +        line.comment = route.desc + +        # TODO colors + +        line.url = route.url + +        save_model line +      end +    end +  end + +  def vehicle_journey_by_trip_id +    @vehicle_journey_by_trip_id ||= {} +  end + +  def import_trips +    source.trips.each_slice(100) do |slice| +      slice.each do |trip| +        Chouette::Route.transaction do +          line = line_referential.lines.find_by registration_number: trip.route_id + +          route = referential.routes.build line: line +          route.wayback = (trip.direction_id == "0" ? :outbound : :inbound) +          # TODO better name ? +          name = route.published_name = trip.short_name.presence || trip.headsign.presence || route.wayback.to_s.capitalize +          route.name = name +          save_model route + +          journey_pattern = route.journey_patterns.build name: name +          save_model journey_pattern + +          vehicle_journey = journey_pattern.vehicle_journeys.build route: route +          vehicle_journey.published_journey_name = trip.headsign.presence || trip.id +          save_model vehicle_journey + +          time_table = referential.time_tables.find_by(id: time_tables_by_service_id[trip.service_id]) if time_tables_by_service_id[trip.service_id] +          if time_table +            vehicle_journey.time_tables << time_table +          else +            messages.create! criticity: "warning", message_key: "gtfs.trips.unkown_service_id", message_attributes: {service_id: trip.service_id} +          end + +          vehicle_journey_by_trip_id[trip.id] = vehicle_journey.id +        end +      end +    end +  end + +  def import_stop_times +    source.stop_times.group_by(&:trip_id).each_slice(50) do |slice| +      slice.each do |trip_id, stop_times| +        Chouette::VehicleJourneyAtStop.transaction do +          vehicle_journey = referential.vehicle_journeys.find vehicle_journey_by_trip_id[trip_id] +          journey_pattern = vehicle_journey.journey_pattern +          route = journey_pattern.route + +          stop_times.sort_by! { |s| s.stop_sequence.to_i } + +          stop_times.each do |stop_time| +            stop_area = stop_area_referential.stop_areas.find_by(registration_number: stop_time.stop_id) + +            stop_point = route.stop_points.build stop_area: stop_area +            save_model stop_point + +            journey_pattern.stop_points << stop_point + +            # JourneyPattern#vjas_add creates automaticaly VehicleJourneyAtStop +            vehicle_journey_at_stop = journey_pattern.vehicle_journey_at_stops.find_by(stop_point_id: stop_point.id) + +            departure_time = GTFS::Time.parse(stop_time.departure_time) +            arrival_time = GTFS::Time.parse(stop_time.arrival_time) + +            vehicle_journey_at_stop.departure_time = departure_time.time +            vehicle_journey_at_stop.arrival_time = arrival_time.time +            vehicle_journey_at_stop.departure_day_offset = departure_time.day_offset +            vehicle_journey_at_stop.arrival_day_offset = arrival_time.day_offset + +            # TODO offset + +            save_model vehicle_journey_at_stop +          end +        end +      end +    end +  end + +  def time_tables_by_service_id +    @time_tables_by_service_id ||= {} +  end + +  def import_calendars +    source.calendars.each_slice(500) do |slice| +      Chouette::TimeTable.transaction do +        slice.each do |calendar| +          time_table = referential.time_tables.build comment: "Calendar #{calendar.service_id}" +          Chouette::TimeTable.all_days.each do |day| +            time_table.send("#{day}=", calendar.send(day)) +          end +          time_table.periods.build period_start: calendar.start_date, period_end: calendar.end_date + +          save_model time_table + +          time_tables_by_service_id[calendar.service_id] = time_table.id +        end +      end +    end +  end + +  def import_calendar_dates +    source.calendar_dates.each_slice(500) do |slice| +      Chouette::TimeTable.transaction do +        slice.each do |calendar_date| +          time_table = referential.time_tables.find time_tables_by_service_id[calendar_date.service_id] +          date = time_table.dates.build date: Date.parse(calendar_date.date), in_out: calendar_date.exception_type == "1" + +          save_model date +        end +      end +    end +  end + +  def save_model(model) +    unless model.save +      Rails.logger.info "Can't save #{model.class.name} : #{model.errors.inspect}" +      raise ActiveRecord::RecordNotSaved.new("Invalid #{model.class.name} : #{model.errors.inspect}") +    end +    Rails.logger.debug "Created #{model.inspect}" +  end + +  def notify_parent +    return unless parent.present? +    return if notified_parent_at +    parent.child_change +    update_column :notified_parent_at, Time.now    end  end diff --git a/app/models/import/workbench.rb b/app/models/import/workbench.rb index f6e15cb89..124b9b0d8 100644 --- a/app/models/import/workbench.rb +++ b/app/models/import/workbench.rb @@ -2,6 +2,25 @@ class Import::Workbench < Import::Base    after_commit :launch_worker, :on => :create    def launch_worker -    WorkbenchImportWorker.perform_async(id) +    unless Import::Gtfs.accept_file?(file.path) +      WorkbenchImportWorker.perform_async(id) +    else +      import_gtfs +    end +  end + +  def import_gtfs +    update_column :status, 'running' +    update_column :started_at, Time.now + +    Import::Gtfs.create! parent_id: self.id, workbench: workbench, file: File.new(file.path), name: "Import GTFS", creator: "Web service" + +    update_column :status, 'successful' +    update_column :ended_at, Time.now +  rescue Exception => e +    Rails.logger.error "Error while processing GTFS file: #{e}" + +    update_column :status, 'failed' +    update_column :ended_at, Time.now    end  end diff --git a/app/models/referential.rb b/app/models/referential.rb index 3304108d0..1794126a2 100644 --- a/app/models/referential.rb +++ b/app/models/referential.rb @@ -168,6 +168,10 @@ class Referential < ApplicationModel      Chouette::TimeTable.all    end +  def time_table_dates +    Chouette::TimeTableDate.all +  end +    def timebands      Chouette::Timeband.all    end @@ -184,6 +188,10 @@ class Referential < ApplicationModel      Chouette::VehicleJourneyFrequency.all    end +  def vehicle_journey_at_stops +    Chouette::VehicleJourneyAtStop.all +  end +    def routing_constraint_zones      Chouette::RoutingConstraintZone.all    end diff --git a/app/workers/gtfs_import_worker.rb b/app/workers/gtfs_import_worker.rb new file mode 100644 index 000000000..02f5053b0 --- /dev/null +++ b/app/workers/gtfs_import_worker.rb @@ -0,0 +1,7 @@ +class GtfsImportWorker +  include Sidekiq::Worker + +  def perform(import_id) +    Import::Gtfs.find(import_id).import +  end +end diff --git a/db/migrate/20180403100007_add_registration_number_indexes.rb b/db/migrate/20180403100007_add_registration_number_indexes.rb new file mode 100644 index 000000000..bc2d48329 --- /dev/null +++ b/db/migrate/20180403100007_add_registration_number_indexes.rb @@ -0,0 +1,7 @@ +class AddRegistrationNumberIndexes < ActiveRecord::Migration +  def change +    add_index :stop_areas, [:stop_area_referential_id, :registration_number], name: 'index_stop_areas_on_referential_id_and_registration_number' +    add_index :lines, [:line_referential_id, :registration_number], name: 'index_lines_on_referential_id_and_registration_number' +    add_index :companies, [:line_referential_id, :registration_number], name: 'index_companies_on_referential_id_and_registration_number' +  end +end diff --git a/lib/gtfs/time.rb b/lib/gtfs/time.rb new file mode 100644 index 000000000..49546532a --- /dev/null +++ b/lib/gtfs/time.rb @@ -0,0 +1,28 @@ +module GTFS +  class Time +    attr_reader :hours, :minutes, :seconds +    def initialize(hours, minutes, seconds) +      @hours, @minutes, @seconds = hours, minutes, seconds +    end + +    def real_hours +      hours.modulo(24) +    end + +    def time +      @time ||= ::Time.new(2000, 1, 1, real_hours, minutes, seconds, "+00:00") +    end + +    def day_offset +      hours / 24 +    end + +    FORMAT = /(\d{1,2}):(\d{2}):(\d{2})/ + +    def self.parse(definition) +      if definition.to_s =~ FORMAT +        new *[$1, $2, $3].map(&:to_i) +      end +    end +  end +end diff --git a/spec/fixtures/google-sample-feed.zip b/spec/fixtures/google-sample-feed.zipBinary files differ new file mode 100644 index 000000000..79819e21a --- /dev/null +++ b/spec/fixtures/google-sample-feed.zip diff --git a/spec/lib/gtfs/time_spec.rb b/spec/lib/gtfs/time_spec.rb new file mode 100644 index 000000000..540d7cc79 --- /dev/null +++ b/spec/lib/gtfs/time_spec.rb @@ -0,0 +1,27 @@ +require "rails_helper" + +RSpec.describe GTFS::Time do + +  it "returns an UTC Time with given H:M:S" do +    expect(GTFS::Time.parse("14:29:00").time).to eq(Time.parse("2000-01-01 14:29:00 +00")) +  end + +  it "support hours with a single number" do +    expect(GTFS::Time.parse("4:29:00").time).to eq(Time.parse("2000-01-01 04:29:00 +00")) +  end + +  it "return nil for invalid format" do +    expect(GTFS::Time.parse("abc")).to be_nil +  end + +  it "removes 24 hours after 23:59:59" do +    expect(GTFS::Time.parse("25:29:00").time).to eq(Time.parse("2000-01-01 01:29:00 +00")) +  end + +  it "returns a day_offset for each 24 hours turn" do +    expect(GTFS::Time.parse("10:00:00").day_offset).to eq(0) +    expect(GTFS::Time.parse("30:00:00").day_offset).to eq(1) +    expect(GTFS::Time.parse("50:00:00").day_offset).to eq(2) +  end + +end diff --git a/spec/models/import/gtfs_spec.rb b/spec/models/import/gtfs_spec.rb new file mode 100644 index 000000000..66607c27f --- /dev/null +++ b/spec/models/import/gtfs_spec.rb @@ -0,0 +1,276 @@ +require "rails_helper" + +RSpec.describe Import::Gtfs do + +  let(:referential) do +    create :referential do |referential| +      referential.line_referential.objectid_format = "netex" +      referential.stop_area_referential.objectid_format = "netex" +    end +  end + +  let(:workbench) do +    create :workbench do |workbench| +      workbench.line_referential.objectid_format = "netex" +      workbench.stop_area_referential.objectid_format = "netex" +    end +  end + +  def create_import(file) +    Import::Gtfs.new workbench: workbench, local_file: fixtures_path(file) +  end + +  describe "#import_agencies" do +    let(:import) { create_import "google-sample-feed.zip" } +    it "should create a company for each agency" do +      import.import_agencies + +      expect(workbench.line_referential.companies.pluck(:registration_number, :name)).to eq([["DTA","Demo Transit Authority"]]) +    end +  end + +  describe "#import_stops" do +    let(:import) { create_import "google-sample-feed.zip" } +    it "should create a company for each agency" do +      import.import_stops + +      defined_attributes = [ +        :registration_number, :name, :parent_id, :latitude, :longitude +      ] +      expected_attributes = [ +        ["AMV", "Amargosa Valley (Demo)", nil, 36.641496, -116.40094], +        ["EMSI", "E Main St / S Irving St (Demo)", nil, 36.905697, -116.76218], +        ["DADAN", "Doing Ave / D Ave N (Demo)", nil, 36.909489, -116.768242], +        ["NANAA", "North Ave / N A Ave (Demo)", nil, 36.914944, -116.761472], +        ["NADAV", "North Ave / D Ave N (Demo)", nil, 36.914893, -116.76821], +        ["STAGECOACH", "Stagecoach Hotel & Casino (Demo)", nil, 36.915682, -116.751677], +        ["BULLFROG", "Bullfrog (Demo)", nil, 36.88108, -116.81797], +        ["BEATTY_AIRPORT", "Nye County Airport (Demo)", nil, 36.868446, -116.784582], +        ["FUR_CREEK_RES", "Furnace Creek Resort (Demo)", nil, 36.425288, -117.133162] +      ] + +      expect(workbench.stop_area_referential.stop_areas.pluck(*defined_attributes)).to match_array(expected_attributes) +    end +  end + +  describe "#import_routes" do +    let(:import) { create_import "google-sample-feed.zip" } +    it "should create a line for each route" do +      import.import_routes + +      defined_attributes = [ +        :registration_number, :name, :number, :published_name, +        "companies.registration_number", +        :comment, :url +      ] +      expected_attributes = [ +        ["AAMV", "Airport - Amargosa Valley", "50", "Airport - Amargosa Valley", nil, nil, nil], +        ["CITY", "City", "40", "City", nil, nil, nil], +        ["STBA", "Stagecoach - Airport Shuttle", "30", "Stagecoach - Airport Shuttle", nil, nil, nil], +        ["BFC", "Bullfrog - Furnace Creek Resort", "20", "Bullfrog - Furnace Creek Resort", nil, nil, nil], +        ["AB", "Airport - Bullfrog", "10", "Airport - Bullfrog", nil, nil, nil] +      ] + +      expect(workbench.line_referential.lines.includes(:company).pluck(*defined_attributes)).to match_array(expected_attributes) +    end +  end + +  describe "#import_trips" do +    let(:import) { create_import "google-sample-feed.zip" } +    before do +      import.prepare_referential +      import.import_calendars +    end + +    it "should create a Route for each trip" do +      import.import_trips + +      defined_attributes = [ +        "lines.registration_number", :wayback, :name, :published_name +      ] +      expected_attributes = [ +        ["AB", "outbound", "to Bullfrog", "to Bullfrog"], +        ["AB", "inbound", "to Airport", "to Airport"], +        ["STBA", "inbound", "Shuttle", "Shuttle"], +        ["CITY", "outbound", "Outbound", "Outbound"], +        ["CITY", "inbound", "Inbound", "Inbound"], +        ["BFC", "outbound", "to Furnace Creek Resort", "to Furnace Creek Resort"], +        ["BFC", "inbound", "to Bullfrog", "to Bullfrog"], +        ["AAMV", "outbound", "to Amargosa Valley", "to Amargosa Valley"], +        ["AAMV", "inbound", "to Airport", "to Airport"], +        ["AAMV", "outbound", "to Amargosa Valley", "to Amargosa Valley"], +        ["AAMV", "inbound", "to Airport", "to Airport"] +      ] + +      expect(import.referential.routes.includes(:line).pluck(*defined_attributes)).to match_array(expected_attributes) +    end + +    it "should create a JourneyPattern for each trip" do +      import.import_trips + +      defined_attributes = [ +        :name +      ] +      expected_attributes = [ +        "to Bullfrog", "to Airport", "Shuttle", "Outbound", "Inbound", "to Furnace Creek Resort", "to Bullfrog", "to Amargosa Valley", "to Airport", "to Amargosa Valley", "to Airport" +      ] + +      expect(import.referential.journey_patterns.pluck(*defined_attributes)).to match_array(expected_attributes) +    end + +    it "should create a VehicleJourney for each trip" do +      import.import_trips + +      defined_attributes = ->(v) { +        [v.published_journey_name, v.time_tables.first&.comment] +      } +      expected_attributes = [ +        ["to Bullfrog", "Calendar FULLW"], +        ["to Airport", "Calendar FULLW"], +        ["Shuttle", "Calendar FULLW"], +        ["CITY1", "Calendar FULLW"], +        ["CITY2", "Calendar FULLW"], +        ["to Furnace Creek Resort", "Calendar FULLW"], +        ["to Bullfrog", "Calendar FULLW"], +        ["to Amargosa Valley", "Calendar WE"], +        ["to Airport", "Calendar WE"], +        ["to Amargosa Valley", "Calendar WE"], +        ["to Airport", "Calendar WE"] +      ] + +      expect(import.referential.vehicle_journeys.map(&defined_attributes)).to match_array(expected_attributes) +    end +  end + +  describe "#import_stop_times" do +    let(:import) { create_import "google-sample-feed.zip" } + +    before do +      import.prepare_referential +      import.import_calendars +      import.import_trips +    end + +    it "should create a VehicleJourneyAtStop for each stop_time" do +      import.import_stop_times + +      def t(value) +        Time.parse(value) +      end + +      defined_attributes = [ +        "stop_areas.registration_number", :position, :departure_time, :arrival_time, +      ] +      expected_attributes = [ +        ["STAGECOACH", 0, t("2000-01-01 06:00:00 UTC"), t("2000-01-01 06:00:00 UTC")], +        ["BEATTY_AIRPORT", 1, t("2000-01-01 06:20:00 UTC"), t("2000-01-01 06:20:00 UTC")], +        ["STAGECOACH", 0, t("2000-01-01 06:00:00 UTC"), t("2000-01-01 06:00:00 UTC")], +        ["NANAA", 1, t("2000-01-01 06:07:00 UTC"), t("2000-01-01 06:05:00 UTC")], +        ["NADAV", 2, t("2000-01-01 06:14:00 UTC"), t("2000-01-01 06:12:00 UTC")], +        ["DADAN", 3, t("2000-01-01 06:21:00 UTC"), t("2000-01-01 06:19:00 UTC")], +        ["EMSI", 4, t("2000-01-01 06:28:00 UTC"), t("2000-01-01 06:26:00 UTC")], +        ["EMSI", 0, t("2000-01-01 06:30:00 UTC"), t("2000-01-01 06:28:00 UTC")], +        ["DADAN", 1, t("2000-01-01 06:37:00 UTC"), t("2000-01-01 06:35:00 UTC")], +        ["NADAV", 2, t("2000-01-01 06:44:00 UTC"), t("2000-01-01 06:42:00 UTC")], +        ["NANAA", 3, t("2000-01-01 06:51:00 UTC"), t("2000-01-01 06:49:00 UTC")], +        ["STAGECOACH", 4, t("2000-01-01 06:58:00 UTC"), t("2000-01-01 06:56:00 UTC")], +        ["BEATTY_AIRPORT", 0, t("2000-01-01 08:00:00 UTC"), t("2000-01-01 08:00:00 UTC")], +        ["BULLFROG", 1, t("2000-01-01 08:15:00 UTC"), t("2000-01-01 08:10:00 UTC")], +        ["BULLFROG", 0, t("2000-01-01 12:05:00 UTC"), t("2000-01-01 12:05:00 UTC")], +        ["BEATTY_AIRPORT", 1, t("2000-01-01 12:15:00 UTC"), t("2000-01-01 12:15:00 UTC")], +        ["BULLFROG", 0, t("2000-01-01 08:20:00 UTC"), t("2000-01-01 08:20:00 UTC")], +        ["FUR_CREEK_RES", 1, t("2000-01-01 09:20:00 UTC"), t("2000-01-01 09:20:00 UTC")], +        ["FUR_CREEK_RES", 0, t("2000-01-01 11:00:00 UTC"), t("2000-01-01 11:00:00 UTC")], +        ["BULLFROG", 1, t("2000-01-01 12:00:00 UTC"), t("2000-01-01 12:00:00 UTC")], +        ["BEATTY_AIRPORT", 0, t("2000-01-01 08:00:00 UTC"), t("2000-01-01 08:00:00 UTC")], +        ["AMV", 1, t("2000-01-01 09:00:00 UTC"), t("2000-01-01 09:00:00 UTC")], +        ["AMV", 0, t("2000-01-01 10:00:00 UTC"), t("2000-01-01 10:00:00 UTC")], +        ["BEATTY_AIRPORT", 1, t("2000-01-01 11:00:00 UTC"), t("2000-01-01 11:00:00 UTC")], +        ["BEATTY_AIRPORT", 0, t("2000-01-01 13:00:00 UTC"), t("2000-01-01 13:00:00 UTC")], +        ["AMV", 1, t("2000-01-01 14:00:00 UTC"), t("2000-01-01 14:00:00 UTC")], +        ["AMV", 0, t("2000-01-01 15:00:00 UTC"), t("2000-01-01 15:00:00 UTC")], +        ["BEATTY_AIRPORT", 1, t("2000-01-01 16:00:00 UTC"), t("2000-01-01 16:00:00 UTC")] +      ] +      expect(referential.vehicle_journey_at_stops.includes(stop_point: :stop_area).pluck(*defined_attributes)).to match_array(expected_attributes) +    end +  end + +  describe "#import_calendars" do +    let(:import) { create_import "google-sample-feed.zip" } + +    before do +      import.prepare_referential +    end + +    it "should create a Timetable for each calendar" do +      import.import_calendars + +      def d(value) +        Date.parse(value) +      end + +      defined_attributes = ->(t) { +        [t.comment, t.valid_days, t.periods.first.period_start, t.periods.first.period_end] +      } +      expected_attributes = [ +        ["Calendar FULLW", [1, 2, 3, 4, 5, 6, 7], d("Mon, 01 Jan 2007"), d("Fri, 31 Dec 2010")], +        ["Calendar WE", [6, 7], d("Mon, 01 Jan 2007"), d("Fri, 31 Dec 2010")] +      ] +      expect(referential.time_tables.map(&defined_attributes)).to match_array(expected_attributes) +    end +  end + +  describe "#import_calendar_dates" do +    let(:import) { create_import "google-sample-feed.zip" } + +    before do +      import.prepare_referential +      import.import_calendars +    end + +    it "should create a Timetable::Date for each calendar date" do +      import.import_calendar_dates + +      def d(value) +        Date.parse(value) +      end + +      defined_attributes = ->(d) { +        [d.time_table.comment, d.date, d.in_out] +      } +      expected_attributes = [ +        ["Calendar FULLW", d("Mon, 04 Jun 2007"), false] +      ] +      expect(referential.time_table_dates.map(&defined_attributes)).to match_array(expected_attributes) +    end +  end + +  describe "#download_local_file" do + +    let(:file) { "google-sample-feed.zip" } +    let(:import) do +      Import::Gtfs.create! name: "GTFS test", creator: "Test", workbench: workbench, file: open_fixture(file), download_host: "rails_host" +    end + +    let(:download_url) { "#{import.download_host}/workbenches/#{import.workbench_id}/imports/#{import.id}/download?token=#{import.token_download}" } + +    before do +      stub_request(:get, download_url).to_return(status: 200, body: read_fixture(file)) +    end + +    it "should download local_file" do +      expect(File.read(import.download_local_file)).to eq(read_fixture(file)) +    end + +  end + +  describe "#download_host" do +    it "should return host defined by Rails.application.config.rails_host (without http:// schema)" do +      allow(Rails.application.config).to receive(:rails_host).and_return("http://download_host") + +      expect(Import::Gtfs.new.download_host).to eq("download_host") +    end + +  end + +end | 
