diff options
| author | Alban Peignier | 2018-03-05 23:08:45 +0100 |
|---|---|---|
| committer | GitHub | 2018-03-05 23:08:45 +0100 |
| commit | 75f50a80a458b1de6d211ee6a98587f1262bc1bf (patch) | |
| tree | 298111da5f2f3dfccb46e9e142c9ec9d82a8c887 | |
| parent | 4a9b3e5868e379fa41b214b35d3ded60d8464a64 (diff) | |
| parent | cfdd12aa6b46331435bc62209c51cc14f470bd38 (diff) | |
| download | chouette-core-75f50a80a458b1de6d211ee6a98587f1262bc1bf.tar.bz2 | |
Merge pull request #359 from af83/6068-simple-exporter
Simple exporter. Refs #6068
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | app/models/chouette/journey_pattern.rb | 13 | ||||
| -rw-r--r-- | app/models/chouette/stop_area.rb | 5 | ||||
| -rw-r--r-- | app/models/chouette/vehicle_journey.rb | 9 | ||||
| -rw-r--r-- | app/models/concerns/application_days_support.rb | 47 | ||||
| -rw-r--r-- | app/models/simple_exporter.rb | 183 | ||||
| -rw-r--r-- | app/models/simple_importer.rb | 298 | ||||
| -rw-r--r-- | app/models/simple_interface.rb | 297 | ||||
| -rw-r--r-- | app/models/simple_json_exporter.rb | 185 | ||||
| -rw-r--r-- | config/initializers/apartment.rb | 2 | ||||
| -rw-r--r-- | db/migrate/20180301142531_create_simple_exporters.rb | 7 | ||||
| -rw-r--r-- | db/schema.rb | 6 | ||||
| -rw-r--r-- | lib/tasks/exports.rake | 97 | ||||
| -rw-r--r-- | lib/tasks/helpers/simple_interfaces.rb | 28 | ||||
| -rw-r--r-- | lib/tasks/imports.rake | 88 | ||||
| -rw-r--r-- | spec/models/chouette/journey_pattern_spec.rb | 17 | ||||
| -rw-r--r-- | spec/models/simple_exporter_spec.rb | 118 | ||||
| -rw-r--r-- | spec/models/simple_importer_spec.rb | 6 | ||||
| -rw-r--r-- | spec/models/simple_json_exporter_spec.rb | 95 |
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 |
