aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAlban Peignier2018-03-05 23:08:45 +0100
committerGitHub2018-03-05 23:08:45 +0100
commit75f50a80a458b1de6d211ee6a98587f1262bc1bf (patch)
tree298111da5f2f3dfccb46e9e142c9ec9d82a8c887
parent4a9b3e5868e379fa41b214b35d3ded60d8464a64 (diff)
parentcfdd12aa6b46331435bc62209c51cc14f470bd38 (diff)
downloadchouette-core-75f50a80a458b1de6d211ee6a98587f1262bc1bf.tar.bz2
Merge pull request #359 from af83/6068-simple-exporter
Simple exporter. Refs #6068
-rw-r--r--.gitignore1
-rw-r--r--app/models/chouette/journey_pattern.rb13
-rw-r--r--app/models/chouette/stop_area.rb5
-rw-r--r--app/models/chouette/vehicle_journey.rb9
-rw-r--r--app/models/concerns/application_days_support.rb47
-rw-r--r--app/models/simple_exporter.rb183
-rw-r--r--app/models/simple_importer.rb298
-rw-r--r--app/models/simple_interface.rb297
-rw-r--r--app/models/simple_json_exporter.rb185
-rw-r--r--config/initializers/apartment.rb2
-rw-r--r--db/migrate/20180301142531_create_simple_exporters.rb7
-rw-r--r--db/schema.rb6
-rw-r--r--lib/tasks/exports.rake97
-rw-r--r--lib/tasks/helpers/simple_interfaces.rb28
-rw-r--r--lib/tasks/imports.rake88
-rw-r--r--spec/models/chouette/journey_pattern_spec.rb17
-rw-r--r--spec/models/simple_exporter_spec.rb118
-rw-r--r--spec/models/simple_importer_spec.rb6
-rw-r--r--spec/models/simple_json_exporter_spec.rb95
19 files changed, 1126 insertions, 376 deletions
diff --git a/.gitignore b/.gitignore
index 28960565b..373908d42 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,6 +13,7 @@
# Ignore all logfiles and tempfiles.
/log/*.log
/log/importers
+/log/exporters
/tmp
*~
public/assets/
diff --git a/app/models/chouette/journey_pattern.rb b/app/models/chouette/journey_pattern.rb
index 2a636a041..ff85f376a 100644
--- a/app/models/chouette/journey_pattern.rb
+++ b/app/models/chouette/journey_pattern.rb
@@ -169,6 +169,19 @@ module Chouette
full
end
+ def distance_to stop
+ val = 0
+ i = 0
+ _end = stop_points.first
+ while _end != stop
+ i += 1
+ _start = _end
+ _end = stop_points[i]
+ val += costs_between(_start, _end)[:distance]
+ end
+ val
+ end
+
def set_distances distances
raise "inconsistent data: #{distances.count} values for #{stop_points.count} stops" unless distances.count == stop_points.count
prev = distances[0].to_i
diff --git a/app/models/chouette/stop_area.rb b/app/models/chouette/stop_area.rb
index 7170dd217..f58f97eee 100644
--- a/app/models/chouette/stop_area.rb
+++ b/app/models/chouette/stop_area.rb
@@ -390,9 +390,12 @@ module Chouette
ActiveSupport::TimeZone[time_zone]&.utc_offset
end
- def country_name
+ def country
return unless country_code
country = ISO3166::Country[country_code]
+ end
+
+ def country_name
return unless country
country.translations[I18n.locale.to_s] || country.name
end
diff --git a/app/models/chouette/vehicle_journey.rb b/app/models/chouette/vehicle_journey.rb
index 78cb6184c..60279422c 100644
--- a/app/models/chouette/vehicle_journey.rb
+++ b/app/models/chouette/vehicle_journey.rb
@@ -88,7 +88,6 @@ module Chouette
end
}
-
scope :in_purchase_window, ->(range){
purchase_windows = Chouette::PurchaseWindow.overlap_dates(range)
sql = purchase_windows.joins(:vehicle_journeys).select('vehicle_journeys.id').uniq.to_sql
@@ -155,6 +154,14 @@ module Chouette
end
end
+ def sales_start
+ purchase_windows.map{|p| p.date_ranges.map &:first}.flatten.min
+ end
+
+ def sales_end
+ purchase_windows.map{|p| p.date_ranges.map &:last}.flatten.max
+ end
+
def calculate_vehicle_journey_at_stop_day_offset
Chouette::VehicleJourneyAtStopsDayOffset.new(
vehicle_journey_at_stops
diff --git a/app/models/concerns/application_days_support.rb b/app/models/concerns/application_days_support.rb
index 348436aa4..2d00b5847 100644
--- a/app/models/concerns/application_days_support.rb
+++ b/app/models/concerns/application_days_support.rb
@@ -15,41 +15,34 @@ module ApplicationDaysSupport
end
def day_by_mask(flag)
- int_day_types & flag == flag
+ self.class.day_by_mask int_day_types, flag
end
- def self.day_by_mask(int_day_types,flag)
- int_day_types & flag == flag
+ def valid_day? wday
+ valid_days.include?(wday)
end
- def valid_days
- # Build an array with day of calendar week (1-7, Monday is 1).
- [].tap do |valid_days|
- valid_days << 1 if monday
- valid_days << 2 if tuesday
- valid_days << 3 if wednesday
- valid_days << 4 if thursday
- valid_days << 5 if friday
- valid_days << 6 if saturday
- valid_days << 7 if sunday
+ included do
+ def self.valid_days(int_day_types)
+ # Build an array with day of calendar week (1-7, Monday is 1).
+ [].tap do |valid_days|
+ valid_days << 1 if day_by_mask(int_day_types,MONDAY)
+ valid_days << 2 if day_by_mask(int_day_types,TUESDAY)
+ valid_days << 3 if day_by_mask(int_day_types,WEDNESDAY)
+ valid_days << 4 if day_by_mask(int_day_types,THURSDAY)
+ valid_days << 5 if day_by_mask(int_day_types,FRIDAY)
+ valid_days << 6 if day_by_mask(int_day_types,SATURDAY)
+ valid_days << 7 if day_by_mask(int_day_types,SUNDAY)
+ end
end
- end
- def valid_day? wday
- valid_days.include?(wday)
+ def self.day_by_mask(int_day_types,flag)
+ int_day_types & flag == flag
+ end
end
- def self.valid_days(int_day_types)
- # Build an array with day of calendar week (1-7, Monday is 1).
- [].tap do |valid_days|
- valid_days << 1 if day_by_mask(int_day_types,MONDAY)
- valid_days << 2 if day_by_mask(int_day_types,TUESDAY)
- valid_days << 3 if day_by_mask(int_day_types,WEDNESDAY)
- valid_days << 4 if day_by_mask(int_day_types,THURSDAY)
- valid_days << 5 if day_by_mask(int_day_types,FRIDAY)
- valid_days << 6 if day_by_mask(int_day_types,SATURDAY)
- valid_days << 7 if day_by_mask(int_day_types,SUNDAY)
- end
+ def valid_days
+ self.class.valid_days int_day_types
end
def monday
diff --git a/app/models/simple_exporter.rb b/app/models/simple_exporter.rb
new file mode 100644
index 000000000..c1ba75d0a
--- /dev/null
+++ b/app/models/simple_exporter.rb
@@ -0,0 +1,183 @@
+# coding: utf-8
+class SimpleExporter < SimpleInterface
+ def export opts={}
+ configuration.validate!
+
+ init_env opts
+
+ @csv = nil
+ fail_with_error "Unable to write in file: #{self.filepath}" do
+ dir = Pathname.new(self.filepath).dirname
+ FileUtils.mkdir_p dir
+ @csv = CSV.open(self.filepath, 'w', self.configuration.csv_options)
+ end
+
+ self.configuration.before_actions(:parsing).each do |action| action.call self end
+
+ @statuses = ""
+
+ if ENV["NO_TRANSACTION"]
+ process_collection
+ else
+ ActiveRecord::Base.transaction do
+ process_collection
+ end
+ end
+ self.status ||= :success
+ rescue SimpleInterface::FailedOperation
+ self.status = :failed
+ ensure
+ @csv&.close
+ self.save!
+ end
+
+ def collection
+ @collection ||= begin
+ coll = configuration.collection
+ coll = coll.call() if coll.is_a?(Proc)
+ coll
+ end
+ end
+
+ def encode_string s
+ s.encode("utf-8").force_encoding("utf-8")
+ end
+
+ protected
+ def init_env opts
+ @number_of_lines = collection.size
+
+ super opts
+ end
+
+ def process_collection
+ self.configuration.before_actions(:all).each do |action| action.call self end
+ log "Starting export ...", color: :green
+ log "Export will be written in #{filepath}", color: :green
+ @csv << self.configuration.columns.map(&:name)
+ if collection.is_a?(ActiveRecord::Relation) && collection.model.column_names.include?("id")
+ ids = collection.pluck :id
+ ids.in_groups_of(configuration.batch_size).each do |batch_ids|
+ collection.where(id: batch_ids).each do |item|
+ handle_item item
+ end
+ end
+ else
+ collection.each{|item| handle_item item }
+ end
+ print_state
+ end
+
+ def map_item_to_rows item
+ return [item] unless configuration.item_to_rows_mapping
+ configuration.item_to_rows_mapping.call(item).map {|row| row.is_a?(ActiveRecord::Base) ? row : CustomRow.new(row) }
+ end
+
+ def resolve_value item, col
+ scoped_item = col.scope.inject(item){|tmp, scope| tmp.send(scope)}
+ val = col[:value]
+ if val.nil? || val.is_a?(Proc)
+ if val.is_a?(Proc)
+ val = instance_exec(scoped_item, &val)
+ else
+ attributes = [col.attribute].flatten
+ val = attributes.inject(scoped_item){|tmp, attr| tmp.send(attr) if tmp }
+ end
+ end
+ if val.nil? && !col.omit_nil?
+ push_in_journal({event: :attribute_not_found, message: "Value missing for: #{[col.scope, col.attribute].flatten.join('.')}", kind: :warning})
+ self.status ||= :success_with_warnings
+ end
+
+ if val.nil? && col.required?
+ @new_status = colorize("x", :red)
+ raise "MISSING VALUE FOR COLUMN #{col.name}"
+ end
+
+ val = encode_string(val) if val.is_a?(String)
+ val
+ end
+
+ def handle_item item
+ number_of_lines = @number_of_lines
+ @current_item = item
+ map_item_to_rows(item).each_with_index do |item, i|
+ @number_of_lines = number_of_lines + i
+ @current_row = item.attributes
+ @current_row = @current_row.slice(*configuration.logged_attributes) if configuration.logged_attributes.present?
+ row = []
+ @new_status = nil
+ self.configuration.columns.each do |col|
+ val = resolve_value item, col
+ @new_status ||= colorize("✓", :green)
+ row << val
+ end
+ push_in_journal({event: :success, kind: :log})
+ @statuses += @new_status
+ print_state if @current_line % 20 == 0 || i > 0
+ @current_line += 1
+ @csv << row
+ end
+ end
+
+ class CustomRow < OpenStruct
+ def initialize data
+ super data
+ @data = data
+ end
+
+ def attributes
+ flatten_hash @data
+ end
+
+ protected
+ def flatten_hash h
+ h.each_with_object({}) do |(k, v), h|
+ if v.is_a? Hash
+ flatten_hash(v).map do |h_k, h_v|
+ h["#{k}.#{h_k}".to_sym] = h_v
+ end
+ else
+ h[k] = v
+ end
+ end
+ end
+ end
+
+ class Configuration < SimpleInterface::Configuration
+ attr_accessor :collection
+ attr_accessor :batch_size
+ attr_accessor :logged_attributes
+ attr_accessor :item_to_rows_mapping
+
+ def initialize import_name, opts={}
+ super import_name, opts
+ @collection = opts[:collection]
+ @batch_size = opts[:batch_size] || 1000
+ @logged_attributes = opts[:logged_attributes]
+ @item_to_rows_mapping = opts[:item_to_rows_mapping]
+ end
+
+ def options
+ super.update({
+ collection: collection,
+ batch_size: batch_size,
+ logged_attributes: logged_attributes,
+ item_to_rows_mapping: item_to_rows_mapping,
+ })
+ end
+
+ def map_item_to_rows &block
+ @item_to_rows_mapping = block
+ end
+
+ def add_column name, opts={}
+ raise "Column already defined: #{name}" if @columns.any?{|c| c.name == name.to_s}
+ super name, opts
+ end
+
+ def validate!
+ raise "Incomplete configuration, missing collection for #{@import_name}" if collection.nil?
+ end
+ end
+end
diff --git a/app/models/simple_importer.rb b/app/models/simple_importer.rb
index d6ba64494..e23b3e524 100644
--- a/app/models/simple_importer.rb
+++ b/app/models/simple_importer.rb
@@ -1,38 +1,4 @@
-class SimpleImporter < ActiveRecord::Base
- attr_accessor :configuration
-
- def self.define name
- @importers ||= {}
- configuration = Configuration.new name
- yield configuration
- configuration.validate!
- @importers[name.to_sym] = configuration
- end
-
- def self.find_configuration name
- @importers ||= {}
- configuration = @importers[name.to_sym]
- raise "Importer not found: #{name}" unless configuration
- configuration
- end
-
- def initialize *args
- super *args
- self.configuration = self.class.find_configuration self.configuration_name
- self.journal ||= []
- end
-
- def configure
- new_config = configuration.duplicate
- yield new_config
- new_config.validate!
- self.configuration = new_config
- end
-
- def context
- self.configuration.context
- end
-
+class SimpleImporter < SimpleInterface
def resolve col_name, value, &block
val = block.call(value)
return val if val.present?
@@ -41,20 +7,15 @@ class SimpleImporter < ActiveRecord::Base
end
def import opts={}
- @verbose = opts.delete :verbose
-
+ configuration.validate!
- @resolution_queue = Hash.new{|h,k| h[k] = []}
- @errors = []
- @messages = []
- @number_of_lines = 0
- @padding = 1
- @current_line = 0
fail_with_error "File not found: #{self.filepath}" do
@number_of_lines = CSV.read(self.filepath, self.configuration.csv_options).length
- @padding = [1, Math.log(@number_of_lines, 10).ceil()].max
end
+ init_env opts
+
+ @resolution_queue = Hash.new{|h,k| h[k] = []}
self.configuration.before_actions(:parsing).each do |action| action.call self end
@@ -68,28 +29,12 @@ class SimpleImporter < ActiveRecord::Base
end
end
self.status ||= :success
- rescue FailedImport
+ rescue SimpleInterface::FailedOperation
self.status = :failed
ensure
self.save!
end
- def fail_with_error msg=nil, opts={}
- begin
- yield
- rescue => e
- msg = msg.call if msg.is_a?(Proc)
- custom_print "\nFAILED: \n errors: #{msg}\n exception: #{e.message}\n#{e.backtrace.join("\n")}", color: :red unless self.configuration.ignore_failures
- push_in_journal({message: msg, error: e.message, event: :error, kind: :error})
- @new_status = colorize("x", :red)
- if self.configuration.ignore_failures
- raise FailedRow if opts[:abort_row]
- else
- raise FailedImport
- end
- end
- end
-
def encode_string s
s.encode("utf-8").force_encoding("utf-8")
end
@@ -109,16 +54,6 @@ class SimpleImporter < ActiveRecord::Base
log "CSV file dumped in #{filepath}"
end
- def log msg, opts={}
- msg = colorize msg, opts[:color] if opts[:color]
- if opts[:append]
- @messages[-1] = (@messages[-1] || "") + msg
- else
- @messages << msg
- end
- print_state
- end
-
protected
def process_csv_file
@@ -154,23 +89,23 @@ class SimpleImporter < ActiveRecord::Base
action.call self, @current_record
end
end
- rescue FailedRow
+ rescue SimpleInterface::FailedRow
@new_status = colorize("x", :red)
end
push_in_journal({event: @event, kind: :log}) if @current_record&.valid?
@statuses += @new_status
self.configuration.columns.each do |col|
- if @current_record && col.name && @resolution_queue.any?
- val = @current_record.send col[:attribute]
- (@resolution_queue.delete([col.name, val]) || []).each do |res|
- record = res[:record]
- attribute = res[:attribute]
- value = res[:block].call(val, record)
- record.send "#{attribute}=", value
- record.save!
+ if @current_record && col.name && @resolution_queue.any?
+ val = @current_record.send col[:attribute]
+ (@resolution_queue.delete([col.name, val]) || []).each do |res|
+ record = res[:record]
+ attribute = res[:attribute]
+ value = res[:block].call(val, record)
+ record.send "#{attribute}=", value
+ record.save!
+ end
end
end
- end
print_state
@current_line += 1
end
@@ -179,7 +114,7 @@ class SimpleImporter < ActiveRecord::Base
self.configuration.after_actions(:all).each do |action|
action.call self
end
- rescue FailedRow
+ rescue SimpleInterface::FailedRow
end
end
@@ -215,211 +150,20 @@ class SimpleImporter < ActiveRecord::Base
end
end
- def push_in_journal data
- line = @current_line + 1
- line += 1 if configuration.headers
- self.journal.push data.update(line: line, row: @current_row)
- if data[:kind] == :error || data[:kind] == :warning
- @errors.push data
- end
- end
-
- def colorize txt, color
- color = {
- red: "31",
- green: "32",
- orange: "33",
- }[color] || "33"
- "\e[#{color}m#{txt}\e[0m"
- end
-
- def print_state
- return unless @verbose
-
- @status_width ||= begin
- term_width = %x(tput cols).to_i
- term_width - @padding - 10
- rescue
- 100
- end
-
- @status_height ||= begin
- term_height = %x(tput lines).to_i
- term_height - 3
- rescue
- 50
- end
-
- full_status = @statuses || ""
- full_status = full_status.last(@status_width*10) || ""
- padding_size = [(@number_of_lines - @current_line - 1), (@status_width - full_status.size/10)].min
- full_status = "#{full_status}#{"."*[padding_size, 0].max}"
-
- msg = "#{"%#{@padding}d" % (@current_line + 1)}/#{@number_of_lines}: #{full_status}"
-
- lines_count = [(@status_height / 2) - 3, 1].max
-
- if @messages.any?
- msg += "\n\n"
- msg += colorize "=== MESSAGES (#{@messages.count}) ===\n", :green
- msg += "[...]\n" if @messages.count > lines_count
- msg += @messages.last(lines_count).map{|m| m.truncate(@status_width)}.join("\n")
- msg += "\n"*[lines_count-@messages.count, 0].max
- end
-
- if @errors.any?
- msg += "\n\n"
- msg += colorize "=== ERRORS (#{@errors.count}) ===\n", :red
- msg += "[...]\n" if @errors.count > lines_count
- msg += @errors.last(lines_count).map do |j|
- kind = j[:kind]
- kind = colorize(kind, kind == :error ? :red : :orange)
- kind = "[#{kind}]"
- kind += " "*(25 - kind.size)
- encode_string("#{kind}L#{j[:line]}\t#{j[:error]}\t\t#{j[:message]}").truncate(@status_width)
- end.join("\n")
- end
- custom_print msg, clear: true
- end
-
- def custom_print msg, opts={}
- return unless @verbose
- out = ""
- msg = colorize(msg, opts[:color]) if opts[:color]
- puts "\e[H\e[2J" if opts[:clear]
- out += msg
- print out
- end
-
- class FailedImport < RuntimeError
- end
-
- class FailedRow < RuntimeError
- end
-
- class Configuration
- attr_accessor :model, :headers, :separator, :key, :context, :encoding, :ignore_failures, :scope
- attr_reader :columns
+ class Configuration < SimpleInterface::Configuration
+ attr_accessor :model
def initialize import_name, opts={}
- @import_name = import_name
- @key = opts[:key] || "id"
- @headers = opts.has_key?(:headers) ? opts[:headers] : true
- @separator = opts[:separator] || ","
- @encoding = opts[:encoding]
- @columns = opts[:columns] || []
+ super import_name, opts
@model = opts[:model]
- @custom_handler = opts[:custom_handler]
- @before = opts[:before]
- @after = opts[:after]
- @ignore_failures = opts[:ignore_failures]
- @context = opts[:context] || {}
- @scope = opts[:scope]
- end
-
- def duplicate
- Configuration.new @import_name, self.options
end
def options
- {
- key: @key,
- headers: @headers,
- separator: @separator,
- encoding: @encoding,
- columns: @columns.map(&:duplicate),
- model: model,
- custom_handler: @custom_handler,
- before: @before,
- after: @after,
- ignore_failures: @ignore_failures,
- context: @context,
- scope: @scope
- }
+ super.update({model: model})
end
def validate!
raise "Incomplete configuration, missing model for #{@import_name}" unless model.present?
end
-
- def attribute_for_col col_name
- column = self.columns.find{|c| c.name == col_name}
- column && column[:attribute] || col_name
- end
-
- def record_scope
- _scope = @scope
- _scope = instance_exec(&_scope) if _scope.is_a?(Proc)
- _scope || model
- end
-
- def find_record attrs
- record_scope.find_or_initialize_by(attribute_for_col(@key) => attrs[@key.to_s])
- end
-
- def csv_options
- {
- headers: self.headers,
- col_sep: self.separator,
- encoding: self.encoding
- }
- end
-
- def add_column name, opts={}
- @columns.push Column.new({name: name.to_s}.update(opts))
- end
-
- def add_value attribute, value
- @columns.push Column.new({attribute: attribute, value: value})
- end
-
- def before group=:all, &block
- @before ||= Hash.new{|h, k| h[k] = []}
- @before[group].push block
- end
-
- def after group=:all, &block
- @after ||= Hash.new{|h, k| h[k] = []}
- @after[group].push block
- end
-
- def before_actions group=:all
- @before ||= Hash.new{|h, k| h[k] = []}
- @before[group]
- end
-
- def after_actions group=:all
- @after ||= Hash.new{|h, k| h[k] = []}
- @after[group]
- end
-
- def custom_handler &block
- @custom_handler = block
- end
-
- def get_custom_handler
- @custom_handler
- end
-
- class Column
- attr_accessor :name
- def initialize opts={}
- @name = opts[:name]
- @options = opts
- @options[:attribute] ||= @name
- end
-
- def duplicate
- Column.new @options.dup
- end
-
- def required?
- !!@options[:required]
- end
-
- def [](key)
- @options[key]
- end
- end
end
end
diff --git a/app/models/simple_interface.rb b/app/models/simple_interface.rb
new file mode 100644
index 000000000..489419482
--- /dev/null
+++ b/app/models/simple_interface.rb
@@ -0,0 +1,297 @@
+class SimpleInterface < ActiveRecord::Base
+ attr_accessor :configuration
+
+ class << self
+ def configuration_class
+ "#{self.name}::Configuration".constantize
+ end
+
+ def define name
+ @importers ||= {}
+ configuration = configuration_class.new name
+ yield configuration if block_given?
+ @importers[name.to_sym] = configuration
+ end
+
+ def find_configuration name
+ @importers ||= {}
+ configuration = @importers[name.to_sym]
+ raise "Importer not found: #{name}" unless configuration
+ configuration
+ end
+ end
+
+ def initialize *args
+ super *args
+ self.configuration = self.class.find_configuration self.configuration_name
+ self.journal ||= []
+ end
+
+ def init_env opts
+ @verbose = opts.delete :verbose
+
+ @errors = []
+ @messages = []
+ @padding = 1
+ @current_line = -1
+ @padding = [1, Math.log([@number_of_lines, 1].max, 10).ceil()].max
+ end
+
+ def configure
+ new_config = configuration.duplicate
+ yield new_config
+ self.configuration = new_config
+ end
+
+ def context
+ self.configuration.context
+ end
+
+ def fail_with_error msg=nil, opts={}
+ begin
+ yield
+ rescue => e
+ msg = msg.call if msg.is_a?(Proc)
+ custom_print "\nFAILED: \n errors: #{msg}\n exception: #{e.message}\n#{e.backtrace.join("\n")}", color: :red unless self.configuration.ignore_failures
+ push_in_journal({message: msg, error: e.message, event: :error, kind: :error})
+ @new_status = colorize("x", :red)
+ if self.configuration.ignore_failures
+ raise SimpleInterface::FailedRow if opts[:abort_row]
+ else
+ raise FailedOperation
+ end
+ end
+ end
+
+ def log msg, opts={}
+ msg = msg.to_s
+ msg = colorize msg, opts[:color] if opts[:color]
+ if opts[:append]
+ @messages[-1] = (@messages[-1] || "") + msg
+ else
+ @messages << msg
+ end
+ print_state
+ end
+
+ protected
+
+ def push_in_journal data
+ line = (@current_line || 0) + 1
+ line += 1 if configuration.headers
+ @errors ||= []
+ self.journal.push data.update(line: line, row: @current_row)
+ if data[:kind] == :error || data[:kind] == :warning
+ @errors.push data
+ end
+ end
+
+ def colorize txt, color
+ color = {
+ red: "31",
+ green: "32",
+ orange: "33",
+ }[color] || "33"
+ "\e[#{color}m#{txt}\e[0m"
+ end
+
+ def print_state
+ return unless @verbose
+
+ @status_width ||= begin
+ term_width = %x(tput cols).to_i
+ term_width - @padding - 10
+ rescue
+ 100
+ end
+
+ @status_height ||= begin
+ term_height = %x(tput lines).to_i
+ term_height - 3
+ rescue
+ 50
+ end
+
+ full_status = @statuses || ""
+ full_status = full_status.last(@status_width*10) || ""
+ padding_size = [(@number_of_lines - @current_line - 1), (@status_width - full_status.size/10)].min
+ full_status = "#{full_status}#{"."*[padding_size, 0].max}"
+
+ msg = "#{"%#{@padding}d" % (@current_line + 1)}/#{@number_of_lines}: #{full_status}"
+
+ lines_count = [(@status_height / 2) - 3, 1].max
+
+ if @messages.any?
+ msg += "\n\n"
+ msg += colorize "=== MESSAGES (#{@messages.count}) ===\n", :green
+ msg += "[...]\n" if @messages.count > lines_count
+ msg += @messages.last(lines_count).map{|m| m.truncate(@status_width)}.join("\n")
+ msg += "\n"*[lines_count-@messages.count, 0].max
+ end
+
+ if @errors.any?
+ msg += "\n\n"
+ msg += colorize "=== ERRORS (#{@errors.count}) ===\n", :red
+ msg += "[...]\n" if @errors.count > lines_count
+ msg += @errors.last(lines_count).map do |j|
+ kind = j[:kind]
+ kind = colorize(kind, kind == :error ? :red : :orange)
+ kind = "[#{kind}]"
+ kind += " "*(25 - kind.size)
+ encode_string("#{kind}L#{j[:line]}\t#{j[:error]}\t\t#{j[:message]}").truncate(@status_width)
+ end.join("\n")
+ end
+ custom_print msg, clear: true
+ end
+
+ def custom_print msg, opts={}
+ return unless @verbose
+ out = ""
+ msg = colorize(msg, opts[:color]) if opts[:color]
+ puts "\e[H\e[2J" if opts[:clear]
+ out += msg
+ print out
+ end
+
+ class FailedRow < RuntimeError
+ end
+
+ class FailedOperation < RuntimeError
+ end
+
+ class Configuration
+ attr_accessor :headers, :separator, :key, :context, :encoding, :ignore_failures, :scope
+ attr_reader :columns
+
+ def initialize import_name, opts={}
+ @import_name = import_name
+ @key = opts[:key] || "id"
+ @headers = opts.has_key?(:headers) ? opts[:headers] : true
+ @separator = opts[:separator] || ","
+ @encoding = opts[:encoding]
+ @columns = opts[:columns] || []
+ @custom_handler = opts[:custom_handler]
+ @before = opts[:before]
+ @after = opts[:after]
+ @ignore_failures = opts[:ignore_failures]
+ @context = opts[:context] || {}
+ @scope = opts[:scope]
+ end
+
+ def on_relation relation_name
+ @current_scope ||= []
+ @current_scope.push relation_name
+ yield
+ @current_scope.pop
+ end
+
+ def duplicate
+ self.class.new @import_name, self.options
+ end
+
+ def options
+ {
+ key: @key,
+ headers: @headers,
+ separator: @separator,
+ encoding: @encoding,
+ columns: @columns.map(&:duplicate),
+ custom_handler: @custom_handler,
+ before: @before,
+ after: @after,
+ ignore_failures: @ignore_failures,
+ context: @context,
+ scope: @scope
+ }
+ end
+
+ def attribute_for_col col_name
+ column = self.columns.find{|c| c.name == col_name}
+ column && column[:attribute] || col_name
+ end
+
+ def record_scope
+ _scope = @scope
+ _scope = instance_exec(&_scope) if _scope.is_a?(Proc)
+ _scope || model
+ end
+
+ def find_record attrs
+ record_scope.find_or_initialize_by(attribute_for_col(@key) => attrs[@key.to_s])
+ end
+
+ def csv_options
+ {
+ headers: self.headers,
+ col_sep: self.separator,
+ encoding: self.encoding
+ }
+ end
+
+ def add_column name, opts={}
+ @current_scope ||= []
+ @columns.push Column.new({name: name.to_s, scope: @current_scope.dup}.update(opts))
+ end
+
+ def add_value attribute, value
+ @columns.push Column.new({attribute: attribute, value: value})
+ end
+
+ def before group=:all, &block
+ @before ||= Hash.new{|h, k| h[k] = []}
+ @before[group].push block
+ end
+
+ def after group=:all, &block
+ @after ||= Hash.new{|h, k| h[k] = []}
+ @after[group].push block
+ end
+
+ def before_actions group=:all
+ @before ||= Hash.new{|h, k| h[k] = []}
+ @before[group]
+ end
+
+ def after_actions group=:all
+ @after ||= Hash.new{|h, k| h[k] = []}
+ @after[group]
+ end
+
+ def custom_handler &block
+ @custom_handler = block
+ end
+
+ def get_custom_handler
+ @custom_handler
+ end
+
+ class Column
+ attr_accessor :name, :attribute
+ def initialize opts={}
+ @name = opts[:name]
+ @options = opts
+ @attribute = @options[:attribute] ||= @name
+ end
+
+ def duplicate
+ Column.new @options.dup
+ end
+
+ def required?
+ !!@options[:required]
+ end
+
+ def omit_nil?
+ !!@options[:omit_nil]
+ end
+
+ def scope
+ @options[:scope] || []
+ end
+
+ def [](key)
+ @options[key]
+ end
+ end
+ end
+end
diff --git a/app/models/simple_json_exporter.rb b/app/models/simple_json_exporter.rb
new file mode 100644
index 000000000..8c44c149a
--- /dev/null
+++ b/app/models/simple_json_exporter.rb
@@ -0,0 +1,185 @@
+class SimpleJsonExporter < SimpleExporter
+
+ def export opts={}
+ configuration.validate!
+
+ init_env opts
+
+ if self.configuration.root
+ @out = {self.configuration.root => []}
+ else
+ @out = []
+ end
+
+ fail_with_error "Unable to write in file: #{self.filepath}" do
+ dir = Pathname.new(self.filepath).dirname
+ FileUtils.mkdir_p dir
+ @file = File.open(self.filepath, 'w', self.configuration.file_options)
+ end
+
+ self.configuration.before_actions(:parsing).each do |action| action.call self end
+
+ @statuses = ""
+
+ if ENV["NO_TRANSACTION"]
+ process_collection
+ else
+ ActiveRecord::Base.transaction do
+ process_collection
+ end
+ end
+ self.status ||= :success
+ rescue SimpleInterface::FailedOperation
+ self.status = :failed
+ ensure
+ if @file
+ @file.write @out.to_json
+ @file.close
+ end
+ self.save!
+ end
+
+ protected
+ def root
+ if self.configuration.root
+ @out[self.configuration.root]
+ else
+ @out
+ end
+ end
+
+ def process_collection
+ self.configuration.before_actions(:all).each do |action| action.call self end
+ log "Starting export ...", color: :green
+ log "Export will be written in #{filepath}", color: :green
+
+ if collection.is_a?(ActiveRecord::Relation) && collection.model.column_names.include?("id")
+ ids = collection.pluck :id
+ ids.in_groups_of(configuration.batch_size).each do |batch_ids|
+ collection.where(id: batch_ids).each do |item|
+ handle_item item
+ end
+ end
+ else
+ collection.each{|item| handle_item item }
+ end
+ print_state
+ end
+
+ def resolve_node item, node
+ vals = []
+ scoped_item = node.scope.inject(item){|tmp, scope| tmp.send(scope)}
+
+ [scoped_item.send(node.attribute)].flatten.each do |node_item|
+ item_val = {}
+ apply_configuration node_item, node.configuration, item_val
+ vals.push item_val
+ end
+ node.multiple ? vals : vals.first
+ end
+
+ def apply_configuration item, configuration, output
+ configuration.columns.each do |col|
+ val = resolve_value item, col
+ output[col.name] = val unless val.nil? && col.omit_nil?
+ end
+
+ configuration.nodes.each do |node|
+ val = resolve_node item, node
+ output[node.name] = val
+ end
+ end
+
+ def handle_item item
+ number_of_lines = @number_of_lines
+ @current_item = item
+ map_item_to_rows(item).each_with_index do |item, i|
+ @number_of_lines = number_of_lines + i
+ serialized_item = {}
+ @current_row = item.attributes
+ @current_row = @current_row.slice(*configuration.logged_attributes) if configuration.logged_attributes.present?
+ @new_status = nil
+
+ apply_configuration item, self.configuration, serialized_item
+
+ @new_status ||= colorize("✓", :green)
+
+ push_in_journal({event: :success, kind: :log})
+ @statuses += @new_status
+ print_state if @current_line % 20 == 0 || i > 0
+ @current_line += 1
+ append_item serialized_item
+ end
+ end
+
+ def append_item serialized_item
+ root.push serialized_item
+ end
+
+ class Configuration < SimpleExporter::Configuration
+ attr_reader :nodes
+ attr_accessor :root
+
+ alias_method :add_field, :add_column
+
+ def initialize import_name, opts={}
+ super import_name, opts
+ @collection = opts[:collection]
+ @nodes = opts[:nodes] || []
+ @root = opts[:root]
+ end
+
+ def options
+ super.update({
+ nodes: @nodes,
+ root: @root,
+ })
+ end
+
+ def add_node name, opts={}
+ @nodes ||= []
+ @current_scope ||= []
+ node = Node.new({name: name.to_s, scope: @current_scope.dup}.update(opts))
+ yield node.configuration
+ @nodes.push node
+ end
+
+ def add_nodes name, opts={}, &block
+ self.add_node name, opts.update({multiple: true}), &block
+ end
+
+ def file_options
+ {
+ encoding: self.encoding
+ }
+ end
+ end
+
+ class NodeConfiguration < Configuration
+ def initialize node
+ super
+ end
+ end
+
+ class Node
+ attr_accessor :name, :configuration
+
+ def initialize opts={}
+ @name = opts[:name]
+ @options = opts
+ @configuration = NodeConfiguration.new self
+ end
+
+ def attribute
+ @options[:attribute] || name
+ end
+
+ def multiple
+ !!@options[:multiple]
+ end
+
+ def scope
+ @options[:scope] || []
+ end
+ end
+end
diff --git a/config/initializers/apartment.rb b/config/initializers/apartment.rb
index a996549fd..f5fb8cd5e 100644
--- a/config/initializers/apartment.rb
+++ b/config/initializers/apartment.rb
@@ -81,7 +81,9 @@ Apartment.configure do |config|
'ComplianceCheckMessage',
'Merge',
'CustomField',
+ 'SimpleInterface',
'SimpleImporter',
+ 'SimpleExporter',
]
# use postgres schemas?
diff --git a/db/migrate/20180301142531_create_simple_exporters.rb b/db/migrate/20180301142531_create_simple_exporters.rb
new file mode 100644
index 000000000..c007546c2
--- /dev/null
+++ b/db/migrate/20180301142531_create_simple_exporters.rb
@@ -0,0 +1,7 @@
+class CreateSimpleExporters < ActiveRecord::Migration
+ def change
+ rename_table :simple_importers, :simple_interfaces
+ add_column :simple_interfaces, :type, :string
+ SimpleInterface.update_all type: :SimpleImporter
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 927c80b15..27e38e1b7 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20180227151937) do
+ActiveRecord::Schema.define(version: 20180301142531) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -93,7 +93,6 @@ ActiveRecord::Schema.define(version: 20180227151937) do
t.integer "workgroup_id", limit: 8
t.integer "int_day_types"
t.date "excluded_dates", array: true
- t.integer "workgroup_id", limit: 8
end
add_index "calendars", ["organisation_id"], name: "index_calendars_on_organisation_id", using: :btree
@@ -726,11 +725,12 @@ ActiveRecord::Schema.define(version: 20180227151937) do
t.integer "line_id", limit: 8
end
- create_table "simple_importers", id: :bigserial, force: :cascade do |t|
+ create_table "simple_interfaces", id: :bigserial, force: :cascade do |t|
t.string "configuration_name"
t.string "filepath"
t.string "status"
t.json "journal"
+ t.string "type"
end
create_table "stop_area_referential_memberships", id: :bigserial, force: :cascade do |t|
diff --git a/lib/tasks/exports.rake b/lib/tasks/exports.rake
new file mode 100644
index 000000000..547388b35
--- /dev/null
+++ b/lib/tasks/exports.rake
@@ -0,0 +1,97 @@
+require 'csv'
+require 'tasks/helpers/simple_interfaces'
+
+namespace :export do
+ desc "Notify parent imports when children finish"
+ task notify_parent: :environment do
+ ParentNotifier.new(Import).notify_when_finished
+ end
+
+ desc "Mark old unfinished Netex imports as 'aborted'"
+ task netex_abort_old: :environment do
+ NetexImport.abort_old
+ end
+
+ desc "export companies in the give LineReferential using the given exporter"
+ task :companies, [:referential_id, :configuration_name, :filepath, :logs_output_dir] => :environment do |t, args|
+ args.with_defaults(filepath: "./companies.csv", logs_output_dir: "./log/exporters/")
+ FileUtils.mkdir_p args[:logs_output_dir]
+
+ referential = LineReferential.find args[:referential_id]
+ exporter = SimpleExporter.create configuration_name: args[:configuration_name], filepath: args[:filepath]
+ exporter.configure do |config|
+ config.collection = referential.companies.order(:name)
+ end
+
+ SimpleInterfacesHelper.run_interface_controlling_interruption exporter, :export, args
+ end
+
+ desc "export lines in the give LineReferential using the given exporter"
+ task :lines, [:referential_id, :configuration_name, :filepath, :logs_output_dir] => :environment do |t, args|
+ args.with_defaults(filepath: "./companies.csv", logs_output_dir: "./log/exporters/")
+ FileUtils.mkdir_p args[:logs_output_dir]
+
+ referential = LineReferential.find args[:referential_id]
+ exporter = SimpleExporter.create configuration_name: args[:configuration_name], filepath: args[:filepath]
+ exporter.configure do |config|
+ config.collection = referential.lines.order(:name)
+ end
+
+ SimpleInterfacesHelper.run_interface_controlling_interruption exporter, :export, args
+ end
+
+ desc "export a complete offer from the given referential in the given X next days"
+ task :full_offer, [:referential_id, :configuration_name, :timelapse, :output_dir, :logs_output_dir] => :environment do |t, args|
+ referential = Referential.find args[:referential_id]
+ args.with_defaults(output_dir: "#{referential.name.parameterize}/#{Time.now.strftime "%y%m%d%H%M"}", logs_output_dir: "./log/exporters/", timelapse: 90)
+
+ referential.switch
+
+ journeys = Chouette::VehicleJourney.with_matching_timetable (Time.now.to_date..args[:timelapse].to_i.days.from_now.to_date)
+ if journeys.count == 0
+ puts "No maching journeys were found".red
+ else
+ exporter = SimpleJsonExporter.create configuration_name: "#{args[:configuration_name]}_companies", filepath: "#{args[:output_dir]}/#{args[:configuration_name]}_companies.json"
+ ids = journeys.pluck :company_id
+ ids += journeys.joins(route: :line).pluck :"lines.company_id"
+
+ exporter.configure do |config|
+ config.collection = Chouette::Company.where(id: ids.uniq).order('name')
+ end
+
+ SimpleInterfacesHelper.run_interface_controlling_interruption exporter, :export, args
+ break if exporter.status == :error
+
+ exporter = SimpleJsonExporter.create configuration_name: "#{args[:configuration_name]}_schedules", filepath: "#{args[:output_dir]}/#{args[:configuration_name]}_schedules.json"
+ exporter.configure do |config|
+ config.collection = journeys
+ end
+
+ SimpleInterfacesHelper.run_interface_controlling_interruption exporter, :export, args
+ break if exporter.status == :error
+
+ exporter = SimpleJsonExporter.create configuration_name: "#{args[:configuration_name]}_routes", filepath: "#{args[:output_dir]}/#{args[:configuration_name]}_routes.json"
+ exporter.configure do |config|
+ config.collection = Chouette::JourneyPattern.where(id: journeys.pluck(:journey_pattern_id).uniq)
+ end
+
+ SimpleInterfacesHelper.run_interface_controlling_interruption exporter, :export, args
+ break if exporter.status == :error
+
+ exporter = SimpleJsonExporter.create configuration_name: "#{args[:configuration_name]}_stops", filepath: "#{args[:output_dir]}/#{args[:configuration_name]}_stops.json"
+ exporter.configure do |config|
+ config.collection = Chouette::StopArea.where(id: journeys.joins(:stop_points).pluck(:"stop_points.stop_area_id").uniq).order('parent_id ASC NULLS FIRST')
+ end
+
+ SimpleInterfacesHelper.run_interface_controlling_interruption exporter, :export, args
+ break if exporter.status == :error
+
+ exporter = SimpleJsonExporter.create configuration_name: "#{args[:configuration_name]}_journeys", filepath: "#{args[:output_dir]}/#{args[:configuration_name]}_journeys.json"
+ exporter.configure do |config|
+ config.collection = journeys
+ end
+
+ SimpleInterfacesHelper.run_interface_controlling_interruption exporter, :export, args
+ end
+ end
+end
diff --git a/lib/tasks/helpers/simple_interfaces.rb b/lib/tasks/helpers/simple_interfaces.rb
new file mode 100644
index 000000000..5b593be43
--- /dev/null
+++ b/lib/tasks/helpers/simple_interfaces.rb
@@ -0,0 +1,28 @@
+module SimpleInterfacesHelper
+ def self.interface_output_to_csv interface, output_dir
+ FileUtils.mkdir_p output_dir
+ filepath = File.join output_dir, + "#{interface.configuration_name}_#{Time.now.strftime "%y%m%d%H%M"}_out.csv"
+ cols = %w(line kind event message error)
+ if interface.reload.journal.size > 0 && interface.journal.first["row"].present?
+ keys = interface.journal.first["row"].map(&:first)
+ CSV.open(filepath, "w") do |csv|
+ csv << cols + keys
+ interface.journal.each do |j|
+ csv << cols.map{|c| j[c]} + j["row"].map(&:last)
+ end
+ end
+ puts "Task Output written in #{filepath}"
+ end
+ end
+
+ def self.run_interface_controlling_interruption interface, method, args
+ begin
+ interface.send(method, verbose: true)
+ rescue Interrupt
+ raise
+ ensure
+ puts "\n\e[33m***\e[0m Done, status: " + (interface.status == "success" ? "\e[32m" : "\e[31m" ) + (interface.status || "") + "\e[0m"
+ interface_output_to_csv interface, args[:logs_output_dir]
+ end
+ end
+end
diff --git a/lib/tasks/imports.rake b/lib/tasks/imports.rake
index f01d3f34f..cd9217e5a 100644
--- a/lib/tasks/imports.rake
+++ b/lib/tasks/imports.rake
@@ -1,4 +1,5 @@
require 'csv'
+require 'tasks/helpers/simple_interfaces'
namespace :import do
desc "Notify parent imports when children finish"
@@ -11,25 +12,10 @@ namespace :import do
NetexImport.abort_old
end
- def importer_output_to_csv importer, output_dir
- filepath = File.join output_dir, + "#{importer.configuration_name}_#{Time.now.strftime "%y%m%d%H%M"}_out.csv"
- cols = %w(line kind event message error)
- if importer.reload.journal.size > 0
- keys = importer.journal.first["row"].map(&:first)
- CSV.open(filepath, "w") do |csv|
- csv << cols + keys
- importer.journal.each do |j|
- csv << cols.map{|c| j[c]} + j["row"].map(&:last)
- end
- end
- puts "Import Output written in #{filepath}"
- end
- end
-
desc "import the given file with the corresponding importer"
- task :import, [:configuration_name, :filepath, :referential_id, :output_dir] => :environment do |t, args|
- args.with_defaults(output_dir: "./log/importers/")
- FileUtils.mkdir_p args[:output_dir]
+ task :import, [:configuration_name, :filepath, :referential_id, :logs_output_dir] => :environment do |t, args|
+ args.with_defaults(logs_output_dir: "./log/importers/")
+ FileUtils.mkdir_p args[:logs_output_dir]
importer = SimpleImporter.create configuration_name: args[:configuration_name], filepath: args[:filepath]
@@ -37,46 +23,32 @@ namespace :import do
referential = Referential.find args[:referential_id]
importer.configure do |config|
config.add_value :referential, referential
- config.context = {referential: referential, output_dir: args[:output_dir]}
+ config.context = {referential: referential, logs_output_dir: args[:logs_output_dir]}
end
end
- puts "\e[33m***\e[0m Start importing"
- begin
- importer.import(verbose: true)
- rescue Interrupt
- raise
- ensure
- puts "\n\e[33m***\e[0m Import done, status: " + (importer.status == "success" ? "\e[32m" : "\e[31m" ) + (importer.status || "") + "\e[0m"
- importer_output_to_csv importer, args[:output_dir]
- end
+
+ SimpleInterfacesHelper.run_interface_controlling_interruption importer, :import, args
end
desc "import the given file with the corresponding importer in the given StopAreaReferential"
task :import_in_stop_area_referential, [:referential_id, :configuration_name, :filepath] => :environment do |t, args|
- args.with_defaults(output_dir: "./log/importers/")
- FileUtils.mkdir_p args[:output_dir]
+ args.with_defaults(logs_output_dir: "./log/importers/")
+ FileUtils.mkdir_p args[:logs_output_dir]
referential = StopAreaReferential.find args[:referential_id]
importer = SimpleImporter.create configuration_name: args[:configuration_name], filepath: args[:filepath]
importer.configure do |config|
config.add_value :stop_area_referential, referential
- config.context = {stop_area_referential: referential, output_dir: args[:output_dir]}
- end
- puts "\e[33m***\e[0m Start importing"
- begin
- importer.import(verbose: true)
- rescue Interrupt
- raise
- ensure
- puts "\n\e[33m***\e[0m Import done, status: " + (importer.status == "success" ? "\e[32m" : "\e[31m" ) + (importer.status || "") + "\e[0m"
- importer_output_to_csv importer, args[:output_dir]
+ config.context = {stop_area_referential: referential, logs_output_dir: args[:logs_output_dir]}
end
+
+ SimpleInterfacesHelper.run_interface_controlling_interruption importer, :import, args
end
desc "import the given routes files"
task :import_routes, [:referential_id, :configuration_name, :mapping_filepath, :filepath] => :environment do |t, args|
- args.with_defaults(output_dir: "./log/importers/")
- FileUtils.mkdir_p args[:output_dir]
+ args.with_defaults(logs_output_dir: "./log/importers/")
+ FileUtils.mkdir_p args[:logs_output_dir]
referential = Referential.find args[:referential_id]
referential.switch
@@ -84,38 +56,24 @@ namespace :import do
importer = SimpleImporter.create configuration_name: args[:configuration_name], filepath: args[:filepath]
importer.configure do |config|
config.add_value :stop_area_referential, referential
- config.context = {stop_area_referential: stop_area_referential, mapping_filepath: args[:mapping_filepath], output_dir: args[:output_dir]}
- end
- puts "\e[33m***\e[0m Start importing"
- begin
- importer.import(verbose: true)
- rescue Interrupt
- raise
- ensure
- puts "\n\e[33m***\e[0m Import done, status: " + (importer.status == "success" ? "\e[32m" : "\e[31m" ) + (importer.status || "") + "\e[0m"
- importer_output_to_csv importer, args[:output_dir]
+ config.context = {stop_area_referential: stop_area_referential, mapping_filepath: args[:mapping_filepath], logs_output_dir: args[:logs_output_dir]}
end
+
+ SimpleInterfacesHelper.run_interface_controlling_interruption importer, :import, args
end
desc "import the given file with the corresponding importer in the given LineReferential"
- task :import_in_line_referential, [:referential_id, :configuration_name, :filepath, :output_dir] => :environment do |t, args|
- args.with_defaults(output_dir: "./log/importers/")
- FileUtils.mkdir_p args[:output_dir]
+ task :import_in_line_referential, [:referential_id, :configuration_name, :filepath, :logs_output_dir] => :environment do |t, args|
+ args.with_defaults(logs_output_dir: "./log/importers/")
+ FileUtils.mkdir_p args[:logs_output_dir]
referential = LineReferential.find args[:referential_id]
importer = SimpleImporter.create configuration_name: args[:configuration_name], filepath: args[:filepath]
importer.configure do |config|
config.add_value :line_referential, referential
- config.context = {line_referential: referential, output_dir: args[:output_dir]}
- end
- puts "\e[33m***\e[0m Start importing"
- begin
- importer.import(verbose: true)
- rescue Interrupt
- raise
- ensure
- puts "\n\e[33m***\e[0m Import done, status: " + (importer.status == "success" ? "\e[32m" : "\e[31m" ) + (importer.status || "") + "\e[0m"
- importer_output_to_csv importer, args[:output_dir]
+ config.context = {line_referential: referential, logs_output_dir: args[:logs_output_dir]}
end
+
+ SimpleInterfacesHelper.run_interface_controlling_interruption importer, :import, args
end
end
diff --git a/spec/models/chouette/journey_pattern_spec.rb b/spec/models/chouette/journey_pattern_spec.rb
index 7c767e4d1..dac45d6b5 100644
--- a/spec/models/chouette/journey_pattern_spec.rb
+++ b/spec/models/chouette/journey_pattern_spec.rb
@@ -71,6 +71,23 @@ describe Chouette::JourneyPattern, :type => :model do
end
end
+ describe "distance_to" do
+ let(:journey_pattern) { create :journey_pattern }
+ before do
+ journey_pattern.costs = generate_journey_pattern_costs(10, 10)
+ end
+ subject{ journey_pattern.distance_to stop}
+ context "for the first stop" do
+ let(:stop){ journey_pattern.stop_points.first }
+ it { should eq 0 }
+ end
+
+ context "for the last stop" do
+ let(:stop){ journey_pattern.stop_points.last }
+ it { should eq 40 }
+ end
+ end
+
describe "set_distances" do
let(:journey_pattern) { create :journey_pattern }
let(:distances){ [] }
diff --git a/spec/models/simple_exporter_spec.rb b/spec/models/simple_exporter_spec.rb
new file mode 100644
index 000000000..75051aeb9
--- /dev/null
+++ b/spec/models/simple_exporter_spec.rb
@@ -0,0 +1,118 @@
+RSpec.describe SimpleExporter do
+ describe "#define" do
+ context "with an incomplete configuration" do
+ it "should raise an error" do
+ SimpleExporter.define :foo
+ expect do
+ SimpleExporter.new(configuration_name: :test).export
+ end.to raise_error
+ end
+ end
+ context "with a complete configuration" do
+ before do
+ SimpleExporter.define :foo do |config|
+ config.collection = Chouette::StopArea.all
+ end
+ end
+
+ it "should define an exporter" do
+ expect{SimpleExporter.find_configuration(:foo)}.to_not raise_error
+ expect{SimpleExporter.new(configuration_name: :foo, filepath: "").export}.to_not raise_error
+ expect{SimpleExporter.find_configuration(:bar)}.to raise_error
+ expect{SimpleExporter.new(configuration_name: :bar, filepath: "")}.to raise_error
+ expect{SimpleExporter.new(configuration_name: :bar, filepath: "").export}.to raise_error
+ expect{SimpleExporter.create(configuration_name: :foo, filepath: "")}.to change{SimpleExporter.count}.by 1
+ end
+ end
+
+ context "when defining the same col twice" do
+ it "should raise an error" do
+ expect do
+ SimpleExporter.define :foo do |config|
+ config.collection = Chouette::StopArea.all
+ config.add_column :name
+ config.add_column :name
+ end
+ end.to raise_error
+ end
+ end
+ end
+
+ describe "#export" do
+ let(:exporter){ importer = SimpleExporter.new(configuration_name: :test, filepath: filepath) }
+ let(:filepath){ Rails.root + "tmp/" + filename }
+ let(:filename){ "stop_area.csv" }
+ # let(:stop_area_referential){ create(:stop_area_referential, objectid_format: :stif_netex) }
+
+ context "with one row per item" do
+ before(:each) do
+ @stop_area = create :stop_area
+ SimpleExporter.define :test do |config|
+ config.collection = ->{ Chouette::StopArea.all }
+ config.separator = ";"
+ config.add_column :name
+ config.add_column :lat, attribute: :latitude
+ config.add_column :lng, attribute: [:latitude, :to_i, :next]
+ config.add_column :type, attribute: :area_type
+ config.add_column :street_name, value: "Lil Exporter"
+ config.on_relation :stop_area_referential do
+ config.add_column :stop_area_referential_id, attribute: :id
+ config.add_column :stop_area_referential_id_other, value: ->(item){ item.id }
+ end
+ config.add_column :forty_two, value: 42
+ end
+ end
+
+ it "should export the given file" do
+ expect{exporter.export verbose: false}.to_not raise_error
+ expect(exporter.status).to eq "success"
+ expect(File.exists?(filepath)).to be_truthy
+ csv = CSV.read(filepath, headers: true, col_sep: ";")
+ row = csv.by_row.values_at(0).last
+ expect(row["name"]).to eq @stop_area.name
+ expect(row["lat"]).to eq @stop_area.latitude.to_s
+ expect(row["lng"]).to eq (@stop_area.latitude.to_i + 1).to_s
+ expect(row["street_name"]).to eq "Lil Exporter"
+ expect(row["stop_area_referential_id"]).to eq @stop_area.stop_area_referential_id.to_s
+ expect(row["stop_area_referential_id_other"]).to eq @stop_area.stop_area_referential_id.to_s
+ expect(row["forty_two"]).to eq "42"
+ end
+ end
+
+ context "with several rows for the same object" do
+ before(:each) do
+ @stop_area = create :stop_area
+ @stop_1 = create :stop_point, stop_area: @stop_area
+ @stop_2 = create :stop_point, stop_area: @stop_area
+ SimpleExporter.define :test do |config|
+ config.collection = ->{ Chouette::StopArea.all }
+ config.map_item_to_rows do |stop_area|
+ stop_area.stop_points.map do |sp|
+ {
+ id: sp.id,
+ stop_area: stop_area
+ }
+ end
+ end
+ config.add_column :id
+ config.on_relation :stop_area do
+ config.add_column :stop_area_name, attribute: :name
+ end
+ end
+ end
+
+ it "should export the given file" do
+ expect{exporter.export verbose: false}.to_not raise_error
+ expect(exporter.status).to eq "success"
+ expect(File.exists?(filepath)).to be_truthy
+ csv = CSV.read(filepath, headers: true)
+ row = csv.by_row.values_at(0).last
+ expect(row["id"]).to eq @stop_1.id.to_s
+ expect(row["stop_area_name"]).to eq @stop_area.name
+ row = csv.by_row.values_at(1).last
+ expect(row["id"]).to eq @stop_2.id.to_s
+ expect(row["stop_area_name"]).to eq @stop_area.name
+ end
+ end
+ end
+end
diff --git a/spec/models/simple_importer_spec.rb b/spec/models/simple_importer_spec.rb
index 60d7b7882..5f9eb0651 100644
--- a/spec/models/simple_importer_spec.rb
+++ b/spec/models/simple_importer_spec.rb
@@ -3,8 +3,9 @@ RSpec.describe SimpleImporter do
context "with an incomplete configuration" do
it "should raise an error" do
+ SimpleImporter.define :foo
expect do
- SimpleImporter.define :foo
+ SimpleImporter.new(configuration_name: :foo, filepath: "").import
end.to raise_error
end
end
@@ -18,6 +19,7 @@ RSpec.describe SimpleImporter do
it "should define an importer" do
expect{SimpleImporter.find_configuration(:foo)}.to_not raise_error
expect{SimpleImporter.new(configuration_name: :foo, filepath: "")}.to_not raise_error
+ expect{SimpleImporter.new(configuration_name: :foo, filepath: "").import}.to_not raise_error
expect{SimpleImporter.find_configuration(:bar)}.to raise_error
expect{SimpleImporter.new(configuration_name: :bar, filepath: "")}.to raise_error
expect{SimpleImporter.create(configuration_name: :foo, filepath: "")}.to change{SimpleImporter.count}.by 1
@@ -47,7 +49,7 @@ RSpec.describe SimpleImporter do
end
it "should import the given file" do
- expect{importer.import verbose: false}.to change{Chouette::StopArea.count}.by 1
+ expect{importer.import verbose: true}.to change{Chouette::StopArea.count}.by 1
expect(importer.status).to eq "success"
stop = Chouette::StopArea.last
expect(stop.name).to eq "Nom du Stop"
diff --git a/spec/models/simple_json_exporter_spec.rb b/spec/models/simple_json_exporter_spec.rb
new file mode 100644
index 000000000..4b48dc732
--- /dev/null
+++ b/spec/models/simple_json_exporter_spec.rb
@@ -0,0 +1,95 @@
+RSpec.describe SimpleJsonExporter do
+ describe "#define" do
+ context "with an incomplete configuration" do
+ it "should raise an error" do
+ SimpleJsonExporter.define :foo
+ expect do
+ SimpleJsonExporter.new(configuration_name: :test).export
+ end.to raise_error
+ end
+ end
+ context "with a complete configuration" do
+ before do
+ SimpleJsonExporter.define :foo do |config|
+ config.collection = Chouette::StopArea.all
+ end
+ end
+
+ it "should define an exporter" do
+ expect{SimpleJsonExporter.find_configuration(:foo)}.to_not raise_error
+ expect{SimpleJsonExporter.new(configuration_name: :foo, filepath: "").export}.to_not raise_error
+ expect{SimpleJsonExporter.find_configuration(:bar)}.to raise_error
+ expect{SimpleJsonExporter.new(configuration_name: :bar, filepath: "")}.to raise_error
+ expect{SimpleJsonExporter.new(configuration_name: :bar, filepath: "").export}.to raise_error
+ expect{SimpleJsonExporter.create(configuration_name: :foo, filepath: "")}.to change{SimpleJsonExporter.count}.by 1
+ end
+ end
+
+ context "when defining the same col twice" do
+ it "should raise an error" do
+ expect do
+ SimpleJsonExporter.define :foo do |config|
+ config.collection = Chouette::StopArea.all
+ config.add_field :name
+ config.add_field :name
+ end
+ end.to raise_error
+ end
+ end
+ end
+
+ describe "#export" do
+ let(:exporter){ importer = SimpleJsonExporter.new(configuration_name: :test, filepath: filepath) }
+ let(:filepath){ Rails.root + "tmp/" + filename }
+ let(:filename){ "stop_area.json" }
+ # let(:stop_area_referential){ create(:stop_area_referential, objectid_format: :stif_netex) }
+
+ context "with one row per item" do
+ before(:each) do
+ @stop_area = create :stop_area
+ @stop_1 = create :stop_point, stop_area: @stop_area
+ @stop_2 = create :stop_point, stop_area: @stop_area
+
+ SimpleJsonExporter.define :test do |config|
+ config.collection = ->{ Chouette::StopArea.all }
+ config.root = "stops"
+ config.add_field :name
+ config.add_field :lat, attribute: :latitude
+ config.add_field :lng, attribute: [:latitude, :to_i, :next]
+ config.add_field :type, attribute: :area_type
+ config.add_field :street_name, value: "Lil Exporter"
+ config.add_node :stop_area_referential do |config|
+ config.add_field :id, attribute: :id
+ config.add_field :id_other, value: ->(item){ item.id }
+ end
+
+ config.add_nodes :stop_points do |config|
+ config.add_field :id
+ config.add_node :stop_area do |config|
+ config.add_field :id
+ end
+ end
+ config.add_field :forty_two, value: 42
+ end
+ end
+
+ it "should export the given file" do
+ expect{exporter.export verbose: false}.to_not raise_error
+ expect(exporter.status).to eq "success"
+ expect(File.exists?(filepath)).to be_truthy
+ json = JSON.parse File.read(filepath)
+ row = json["stops"].first
+ expect(row["name"]).to eq @stop_area.name
+ expect(row["lat"]).to eq @stop_area.latitude.to_s
+ expect(row["lng"]).to eq (@stop_area.latitude.to_i + 1)
+ expect(row["street_name"]).to eq "Lil Exporter"
+ expect(row["stop_area_referential"]["id"]).to eq @stop_area.stop_area_referential_id
+ expect(row["stop_area_referential"]["id_other"]).to eq @stop_area.stop_area_referential_id
+ expect(row["stop_points"][0]["id"]).to eq @stop_1.id
+ expect(row["stop_points"][0]["stop_area"]["id"]).to eq @stop_area.id
+ expect(row["stop_points"][1]["id"]).to eq @stop_2.id
+ expect(row["forty_two"]).to eq 42
+ end
+ end
+ end
+end