aboutsummaryrefslogtreecommitdiffstats
path: root/app/models
diff options
context:
space:
mode:
Diffstat (limited to 'app/models')
-rw-r--r--app/models/api/v1/api_key.rb6
-rw-r--r--app/models/application_model.rb5
-rw-r--r--app/models/calendar.rb7
-rw-r--r--app/models/calendar/date_value.rb2
-rw-r--r--app/models/calendar/period.rb4
-rw-r--r--app/models/calendar_observer.rb4
-rw-r--r--app/models/chouette/access_link.rb4
-rw-r--r--app/models/chouette/access_point.rb4
-rw-r--r--app/models/chouette/active_record.rb3
-rw-r--r--app/models/chouette/area_type.rb8
-rw-r--r--app/models/chouette/company.rb6
-rw-r--r--app/models/chouette/connection_link.rb4
-rw-r--r--app/models/chouette/for_alighting_enumerations.rb2
-rw-r--r--app/models/chouette/for_boarding_enumerations.rb2
-rw-r--r--app/models/chouette/group_of_line.rb4
-rw-r--r--app/models/chouette/journey_pattern.rb58
-rw-r--r--app/models/chouette/line.rb26
-rw-r--r--app/models/chouette/network.rb4
-rw-r--r--app/models/chouette/pt_link.rb4
-rw-r--r--app/models/chouette/purchase_window.rb23
-rw-r--r--app/models/chouette/route.rb80
-rw-r--r--app/models/chouette/routing_constraint_zone.rb7
-rw-r--r--app/models/chouette/stop_area.rb65
-rw-r--r--app/models/chouette/stop_point.rb11
-rw-r--r--app/models/chouette/time_table.rb11
-rw-r--r--app/models/chouette/time_table_date.rb1
-rw-r--r--app/models/chouette/time_table_period.rb1
-rw-r--r--app/models/chouette/timeband.rb3
-rw-r--r--app/models/chouette/vehicle_journey.rb67
-rw-r--r--app/models/chouette/vehicle_journey_at_stop.rb33
-rw-r--r--app/models/chouette/vehicle_journey_at_stops_day_offset.rb33
-rw-r--r--app/models/clean_up.rb2
-rw-r--r--app/models/clean_up_result.rb2
-rw-r--r--app/models/compliance_check.rb13
-rw-r--r--app/models/compliance_check_block.rb2
-rw-r--r--app/models/compliance_check_message.rb2
-rw-r--r--app/models/compliance_check_message_export.rb6
-rw-r--r--app/models/compliance_check_resource.rb4
-rw-r--r--app/models/compliance_check_set.rb49
-rw-r--r--app/models/compliance_control.rb47
-rw-r--r--app/models/compliance_control_block.rb4
-rw-r--r--app/models/compliance_control_set.rb5
-rw-r--r--app/models/concerns/application_days_support.rb55
-rw-r--r--app/models/concerns/checksum_support.rb1
-rw-r--r--app/models/concerns/compliance_item_support.rb13
-rw-r--r--app/models/concerns/custom_fields_support.rb62
-rw-r--r--app/models/concerns/iev_interfaces/message.rb9
-rw-r--r--app/models/concerns/iev_interfaces/resource.rb9
-rw-r--r--app/models/concerns/iev_interfaces/task.rb121
-rw-r--r--app/models/concerns/metadata_support.rb107
-rw-r--r--app/models/concerns/min_max_values_validation.rb4
-rw-r--r--app/models/custom_field.rb295
-rw-r--r--app/models/dashboard.rb1
-rw-r--r--app/models/export.rb53
-rw-r--r--app/models/export/base.rb135
-rw-r--r--app/models/export/message.rb8
-rw-r--r--app/models/export/netex.rb22
-rw-r--r--app/models/export/resource.rb8
-rw-r--r--app/models/export/simple_exporter/base.rb94
-rw-r--r--app/models/export/workgroup.rb9
-rw-r--r--app/models/export_log_message.rb42
-rw-r--r--app/models/export_report.rb8
-rw-r--r--app/models/export_service.rb23
-rw-r--r--app/models/export_task.rb119
-rw-r--r--app/models/generic_attribute_control/min_max.rb4
-rw-r--r--app/models/gtfs_export.rb47
-rw-r--r--app/models/gtfs_import.rb36
-rw-r--r--app/models/hub_export.rb24
-rw-r--r--app/models/import.rb103
-rw-r--r--app/models/import/base.rb47
-rw-r--r--app/models/import/gtfs.rb309
-rw-r--r--app/models/import/message.rb8
-rw-r--r--app/models/import/message_export.rb (renamed from app/models/import_message_export.rb)10
-rw-r--r--app/models/import/netex.rb63
-rw-r--r--app/models/import/resource.rb8
-rw-r--r--app/models/import/workbench.rb26
-rw-r--r--app/models/import_message.rb8
-rw-r--r--app/models/import_report.rb8
-rw-r--r--app/models/import_resource.rb11
-rw-r--r--app/models/import_service.rb23
-rw-r--r--app/models/import_task.rb141
-rw-r--r--app/models/kml_export.rb24
-rw-r--r--app/models/line_control/lines_scope.rb8
-rw-r--r--app/models/line_referential.rb7
-rw-r--r--app/models/line_referential_membership.rb4
-rw-r--r--app/models/line_referential_sync.rb2
-rw-r--r--app/models/line_referential_sync_message.rb2
-rw-r--r--app/models/merge.rb125
-rw-r--r--app/models/neptune_export.rb27
-rw-r--r--app/models/neptune_import.rb19
-rw-r--r--app/models/netex_export.rb24
-rw-r--r--app/models/netex_import.rb38
-rw-r--r--app/models/organisation.rb8
-rw-r--r--app/models/public_version.rb4
-rw-r--r--app/models/referential.rb60
-rw-r--r--app/models/referential_cloning.rb2
-rw-r--r--app/models/referential_metadata.rb6
-rw-r--r--app/models/referential_suite.rb2
-rw-r--r--app/models/simple_exporter.rb182
-rw-r--r--app/models/simple_importer.rb305
-rw-r--r--app/models/simple_interface.rb372
-rw-r--r--app/models/simple_interfaces_group.rb76
-rw-r--r--app/models/simple_json_exporter.rb183
-rw-r--r--app/models/stop_area_referential.rb33
-rw-r--r--app/models/stop_area_referential_membership.rb4
-rw-r--r--app/models/stop_area_referential_sync.rb2
-rw-r--r--app/models/stop_area_referential_sync_message.rb2
-rw-r--r--app/models/user.rb10
-rw-r--r--app/models/vehicle_journey_control/speed.rb2
-rw-r--r--app/models/vehicle_journey_import.rb6
-rw-r--r--app/models/workbench.rb11
-rw-r--r--app/models/workbench_import.rb7
-rw-r--r--app/models/workgroup.rb8
113 files changed, 2858 insertions, 1409 deletions
diff --git a/app/models/api/v1/api_key.rb b/app/models/api/v1/api_key.rb
index 09c6f77ac..e6ceb977a 100644
--- a/app/models/api/v1/api_key.rb
+++ b/app/models/api/v1/api_key.rb
@@ -1,7 +1,8 @@
module Api
module V1
- class ApiKey < ::ActiveRecord::Base
- has_paper_trail
+ class ApiKey < ::ApplicationModel
+ has_metadata
+
before_create :generate_access_token
belongs_to :referential, :class_name => '::Referential'
belongs_to :organisation, :class_name => '::Organisation'
@@ -47,4 +48,3 @@ module Api
end
end
end
-
diff --git a/app/models/application_model.rb b/app/models/application_model.rb
new file mode 100644
index 000000000..1a2a5099d
--- /dev/null
+++ b/app/models/application_model.rb
@@ -0,0 +1,5 @@
+class ApplicationModel < ::ActiveRecord::Base
+ include MetadataSupport
+
+ self.abstract_class = true
+end
diff --git a/app/models/calendar.rb b/app/models/calendar.rb
index 84b569ab4..39e2b2cff 100644
--- a/app/models/calendar.rb
+++ b/app/models/calendar.rb
@@ -2,18 +2,17 @@ require 'range_ext'
require_relative 'calendar/date_value'
require_relative 'calendar/period'
-class Calendar < ActiveRecord::Base
+class Calendar < ApplicationModel
include DateSupport
include PeriodSupport
include ApplicationDaysSupport
include TimetableSupport
- has_paper_trail class_name: 'PublicVersion'
+ has_metadata
belongs_to :organisation
belongs_to :workgroup
- validates_presence_of :name, :short_name, :organisation, :workgroup
- validates_uniqueness_of :short_name
+ validates_presence_of :name, :organisation, :workgroup
has_many :time_tables
diff --git a/app/models/calendar/date_value.rb b/app/models/calendar/date_value.rb
index a4a405d43..f50b4237c 100644
--- a/app/models/calendar/date_value.rb
+++ b/app/models/calendar/date_value.rb
@@ -1,4 +1,4 @@
-class Calendar < ActiveRecord::Base
+class Calendar < ApplicationModel
class DateValue
include ActiveAttr::Model
diff --git a/app/models/calendar/period.rb b/app/models/calendar/period.rb
index 8b3e4109b..c549a7575 100644
--- a/app/models/calendar/period.rb
+++ b/app/models/calendar/period.rb
@@ -1,4 +1,4 @@
-class Calendar < ActiveRecord::Base
+class Calendar < ApplicationModel
class Period
include ActiveAttr::Model
@@ -16,7 +16,7 @@ class Calendar < ActiveRecord::Base
alias_method :period_end=, :end=
def check_end_greather_than_begin
- if self.begin && self.end && self.begin >= self.end
+ if self.begin && self.end && self.begin > self.end
errors.add(:base, I18n.t('calendars.errors.short_period'))
end
end
diff --git a/app/models/calendar_observer.rb b/app/models/calendar_observer.rb
index c81addff4..0414d01d2 100644
--- a/app/models/calendar_observer.rb
+++ b/app/models/calendar_observer.rb
@@ -3,7 +3,7 @@ class CalendarObserver < ActiveRecord::Observer
def after_update calendar
return unless calendar.shared
- User.with_organisation.each do |user|
+ User.from_workgroup(calendar.workgroup_id).each do |user|
MailerJob.perform_later('CalendarMailer', 'updated', [calendar.id, user.id])
end
end
@@ -11,7 +11,7 @@ class CalendarObserver < ActiveRecord::Observer
def after_create calendar
return unless calendar.shared
- User.with_organisation.each do |user|
+ User.from_workgroup(calendar.workgroup_id).each do |user|
MailerJob.perform_later('CalendarMailer', 'created', [calendar.id, user.id])
end
end
diff --git a/app/models/chouette/access_link.rb b/app/models/chouette/access_link.rb
index 4b99ab5ba..7ab8ca715 100644
--- a/app/models/chouette/access_link.rb
+++ b/app/models/chouette/access_link.rb
@@ -1,9 +1,7 @@
module Chouette
class AccessLink < Chouette::TridentActiveRecord
- has_paper_trail
+ has_metadata
include ObjectidSupport
- # FIXME http://jira.codehaus.org/browse/JRUBY-6358
- self.primary_key = "id"
attr_accessor :access_link_type, :link_orientation_type, :link_key
diff --git a/app/models/chouette/access_point.rb b/app/models/chouette/access_point.rb
index b6f78f239..884460881 100644
--- a/app/models/chouette/access_point.rb
+++ b/app/models/chouette/access_point.rb
@@ -4,9 +4,7 @@ require 'geo_ruby'
module Chouette
class AccessPoint < Chouette::ActiveRecord
- has_paper_trail
- # FIXME http://jira.codehaus.org/browse/JRUBY-6358
- self.primary_key = "id"
+ has_metadata
include Geokit::Mappable
include ProjectionFields
diff --git a/app/models/chouette/active_record.rb b/app/models/chouette/active_record.rb
index c2aab9d50..27f5426b3 100644
--- a/app/models/chouette/active_record.rb
+++ b/app/models/chouette/active_record.rb
@@ -1,7 +1,8 @@
#require "active_record"
require 'deep_cloneable'
module Chouette
- class ActiveRecord < ::ActiveRecord::Base
+ class ActiveRecord < ::ApplicationModel
+
self.abstract_class = true
before_save :nil_if_blank, :set_data_source_ref
diff --git a/app/models/chouette/area_type.rb b/app/models/chouette/area_type.rb
index e17d2ee8d..4feb5c914 100644
--- a/app/models/chouette/area_type.rb
+++ b/app/models/chouette/area_type.rb
@@ -34,9 +34,9 @@ class Chouette::AreaType
@@options = {}
end
- def self.options(kind=:all)
+ def self.options(kind=:all, locale=nil)
@@options ||= {}
- @@options[kind] ||= self.send(kind).map { |c| find(c) }.map { |t| [ t.label, t.code ] }
+ @@options[kind] ||= self.send(kind).map { |c| find(c) }.map { |t| [ t.label(locale), t.code ] }
end
attr_reader :code
@@ -48,8 +48,8 @@ class Chouette::AreaType
all.index(code) <=> all.index(other.code)
end
- def label
- I18n.translate code, scope: 'area_types.label'
+ def label locale=nil
+ I18n.translate code, scope: 'area_types.label', locale: locale
end
end
diff --git a/app/models/chouette/company.rb b/app/models/chouette/company.rb
index 53e412600..9d5737a6c 100644
--- a/app/models/chouette/company.rb
+++ b/app/models/chouette/company.rb
@@ -1,13 +1,15 @@
module Chouette
class Company < Chouette::ActiveRecord
+ has_metadata
+
include CompanyRestrictions
include LineReferentialSupport
include ObjectidSupport
- has_paper_trail class_name: 'PublicVersion'
+ include CustomFieldsSupport
has_many :lines
- validates_format_of :registration_number, :with => %r{\A[0-9A-Za-z_-]+\Z}, :allow_nil => true, :allow_blank => true
+ # validates_format_of :registration_number, :with => %r{\A[0-9A-Za-z_-]+\Z}, :allow_nil => true, :allow_blank => true
validates_presence_of :name
validates_format_of :url, :with => %r{\Ahttps?:\/\/([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?\Z}, :allow_nil => true, :allow_blank => true
diff --git a/app/models/chouette/connection_link.rb b/app/models/chouette/connection_link.rb
index d5ddc606a..fb93e5f90 100644
--- a/app/models/chouette/connection_link.rb
+++ b/app/models/chouette/connection_link.rb
@@ -1,10 +1,8 @@
module Chouette
class ConnectionLink < Chouette::TridentActiveRecord
- has_paper_trail
+ has_metadata
include ObjectidSupport
include ConnectionLinkRestrictions
- # FIXME http://jira.codehaus.org/browse/JRUBY-6358
- self.primary_key = "id"
attr_accessor :connection_link_type
diff --git a/app/models/chouette/for_alighting_enumerations.rb b/app/models/chouette/for_alighting_enumerations.rb
index ab07a670d..2e15fcb58 100644
--- a/app/models/chouette/for_alighting_enumerations.rb
+++ b/app/models/chouette/for_alighting_enumerations.rb
@@ -3,6 +3,6 @@ module Chouette
extend Enumerize
extend ActiveModel::Naming
- enumerize :for_alighting, in: %w[normal forbidden request_stop is_flexible]
+ enumerize :for_alighting, in: %w[normal forbidden request_stop is_flexible], default: :normal
end
end
diff --git a/app/models/chouette/for_boarding_enumerations.rb b/app/models/chouette/for_boarding_enumerations.rb
index 48f8762c2..0190bf805 100644
--- a/app/models/chouette/for_boarding_enumerations.rb
+++ b/app/models/chouette/for_boarding_enumerations.rb
@@ -3,6 +3,6 @@ module Chouette
extend Enumerize
extend ActiveModel::Naming
- enumerize :for_boarding, in: %w[normal forbidden request_stop is_flexible]
+ enumerize :for_boarding, in: %w[normal forbidden request_stop is_flexible], default: :normal
end
end
diff --git a/app/models/chouette/group_of_line.rb b/app/models/chouette/group_of_line.rb
index 75ee1ce73..a30c34ce7 100644
--- a/app/models/chouette/group_of_line.rb
+++ b/app/models/chouette/group_of_line.rb
@@ -1,12 +1,10 @@
module Chouette
class GroupOfLine < Chouette::ActiveRecord
- has_paper_trail
+ has_metadata
include ObjectidSupport
include GroupOfLineRestrictions
include LineReferentialSupport
- # FIXME http://jira.codehaus.org/browse/JRUBY-6358
- self.primary_key = "id"
has_and_belongs_to_many :lines, :class_name => 'Chouette::Line', :order => 'lines.name'
diff --git a/app/models/chouette/journey_pattern.rb b/app/models/chouette/journey_pattern.rb
index 830e985d9..4b4cc2c73 100644
--- a/app/models/chouette/journey_pattern.rb
+++ b/app/models/chouette/journey_pattern.rb
@@ -1,11 +1,10 @@
module Chouette
class JourneyPattern < Chouette::TridentActiveRecord
- has_paper_trail
+ has_metadata
include ChecksumSupport
+ include CustomFieldsSupport
include JourneyPatternRestrictions
include ObjectidSupport
- # FIXME http://jira.codehaus.org/browse/JRUBY-6358
- self.primary_key = "id"
belongs_to :route
has_many :vehicle_journeys, :dependent => :destroy
@@ -17,7 +16,6 @@ module Chouette
validates_presence_of :name
#validates :stop_points, length: { minimum: 2, too_short: :minimum }, on: :update
- enum section_status: { todo: 0, completed: 1, control: 2 }
attr_accessor :control_checked
@@ -55,12 +53,19 @@ module Chouette
end
def self.state_permited_attributes item
- {
+ attrs = {
name: item['name'],
published_name: item['published_name'],
registration_number: item['registration_number'],
costs: item['costs']
}
+ attrs["custom_field_values"] = Hash[
+ *(item["custom_fields"] || {})
+ .map { |k, v| [k, v["value"]] }
+ .flatten
+ ]
+
+ attrs
end
def self.state_create_instance route, item
@@ -81,10 +86,8 @@ module Chouette
def state_stop_points_update item
item['stop_points'].each do |sp|
- exist = stop_area_ids.include?(sp['id'])
- next if exist && sp['checked']
-
- stop_point = route.stop_points.find_by(stop_area_id: sp['id'])
+ stop_point = route.stop_points.find_by(stop_area_id: sp['id'], position: sp['position'])
+ exist = stop_points.include?(stop_point)
if !exist && sp['checked']
stop_points << stop_point
end
@@ -164,13 +167,48 @@ module Chouette
next finish unless start.present?
costs = costs_between(start, finish)
full = false unless costs.present?
- full = false unless costs[:distance] && costs[:distance] > 0
full = false unless costs[:time] && costs[:time] > 0
finish
end
full
end
+ def distance_between start, stop
+ return 0 unless start.position < stop.position
+ val = 0
+ i = stop_points.index(start)
+ _end = start
+ while _end && _end != stop
+ i += 1
+ _start = _end
+ _end = stop_points[i]
+ val += costs_between(_start, _end)[:distance] || 0
+ end
+ val
+ end
+
+ def distance_to stop
+ distance_between stop_points.first, stop
+ end
+
+ def journey_length
+ i = 0
+ j = stop_points.length - 1
+ start = stop_points[i]
+ stop = stop_points[j]
+ while i < j && start.kind == "non_commercial"
+ i+= 1
+ start = stop_points[i]
+ end
+
+ while i < j && stop.kind == "non_commercial"
+ j-= 1
+ stop = stop_points[j]
+ end
+ return 0 unless start && stop
+ distance_between start, stop
+ 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/line.rb b/app/models/chouette/line.rb
index d077d5c9d..4b5d1a68d 100644
--- a/app/models/chouette/line.rb
+++ b/app/models/chouette/line.rb
@@ -1,15 +1,11 @@
module Chouette
class Line < Chouette::ActiveRecord
- has_paper_trail class_name: 'PublicVersion'
+ has_metadata
include LineRestrictions
include LineReferentialSupport
include ObjectidSupport
include StifTransportModeEnumerations
include StifTransportSubmodeEnumerations
- extend ActiveModel::Naming
-
- # FIXME http://jira.codehaus.org/browse/JRUBY-6358
- self.primary_key = "id"
belongs_to :company
belongs_to :network
@@ -33,7 +29,7 @@ module Chouette
# validates_presence_of :network
# validates_presence_of :company
- validates_format_of :registration_number, :with => %r{\A[\d\w_\-]+\Z}, :allow_nil => true, :allow_blank => true
+ # validates_format_of :registration_number, :with => %r{\A[\d\w_\-]+\Z}, :allow_nil => true, :allow_blank => true
validates_format_of :stable_id, :with => %r{\A[\d\w_\-]+\Z}, :allow_nil => true, :allow_blank => true
validates_format_of :url, :with => %r{\Ahttps?:\/\/([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?\Z}, :allow_nil => true, :allow_blank => true
validates_format_of :color, :with => %r{\A[0-9a-fA-F]{6}\Z}, :allow_nil => true, :allow_blank => true
@@ -45,6 +41,24 @@ module Chouette
scope :by_text, ->(text) { where('lower(name) LIKE :t or lower(published_name) LIKE :t or lower(objectid) LIKE :t or lower(comment) LIKE :t or lower(number) LIKE :t',
t: "%#{text.downcase}%") }
+ scope :by_name, ->(name) {
+ joins('LEFT OUTER JOIN public.companies ON companies.id = lines.company_id')
+ .where('
+ lines.number LIKE :q
+ OR lines.name LIKE :q
+ OR companies.name ILIKE :q',
+ q: "%#{sanitize_sql_like(name)}%"
+ )
+ }
+
+ scope :for_organisation, ->(organisation){
+ if objectids = organisation&.lines_scope
+ where(objectid: objectids)
+ else
+ all
+ end
+ }
+
def self.nullable_attributes
[:published_name, :number, :comment, :url, :color, :text_color, :stable_id]
end
diff --git a/app/models/chouette/network.rb b/app/models/chouette/network.rb
index 6843c69ad..4802d7592 100644
--- a/app/models/chouette/network.rb
+++ b/app/models/chouette/network.rb
@@ -1,12 +1,10 @@
module Chouette
class Network < Chouette::ActiveRecord
- has_paper_trail class_name: 'PublicVersion'
+ has_metadata
include NetworkRestrictions
include LineReferentialSupport
include ObjectidSupport
extend Enumerize
- # FIXME http://jira.codehaus.org/browse/JRUBY-6358
- self.primary_key = "id"
has_many :lines
attr_accessor :source_type_name
diff --git a/app/models/chouette/pt_link.rb b/app/models/chouette/pt_link.rb
index d14d5f29c..680632a14 100644
--- a/app/models/chouette/pt_link.rb
+++ b/app/models/chouette/pt_link.rb
@@ -2,9 +2,7 @@ require 'geokit'
module Chouette
class PtLink < Chouette::ActiveRecord
- has_paper_trail
- # FIXME http://jira.codehaus.org/browse/JRUBY-6358
- self.primary_key = "id"
+ has_metadata
include Geokit::Mappable
def geometry
diff --git a/app/models/chouette/purchase_window.rb b/app/models/chouette/purchase_window.rb
index 157390a21..bd7cbb41e 100644
--- a/app/models/chouette/purchase_window.rb
+++ b/app/models/chouette/purchase_window.rb
@@ -11,7 +11,7 @@ module Chouette
enumerize :color, in: %w(#9B9B9B #FFA070 #C67300 #7F551B #41CCE3 #09B09C #3655D7 #6321A0 #E796C6 #DD2DAA)
- has_paper_trail
+ has_metadata
belongs_to :referential
has_and_belongs_to_many :vehicle_journeys, :class_name => 'Chouette::VehicleJourney'
@@ -21,6 +21,13 @@ module Chouette
scope :overlap_dates, ->(date_range) { where('daterange(?, ?) && any (date_ranges)', date_range.first, date_range.last + 1.day) }
scope :matching_dates, ->(date_range) { where('ARRAY[daterange(?, ?)] = date_ranges', date_range.first, date_range.last + 1.day) }
+ # VehicleJourneys include PurchaseWindow checksums in their checksums
+ # OPTIMIZEME
+ def update_vehicle_journey_checksums
+ vehicle_journeys.find_each(&:update_checksum!)
+ end
+ after_commit :update_vehicle_journey_checksums
+
def self.ransackable_scopes(auth_object = nil)
[:contains_date]
end
@@ -35,12 +42,20 @@ module Chouette
def checksum_attributes
attrs = ['name', 'color', 'referential_id']
- ranges_attrs = date_ranges.map{|r| [r.first, r.last]}.flatten.sort
+ ranges_attrs = date_ranges.map{|r| [r.min, r.max]}.flatten.sort
self.slice(*attrs).values + ranges_attrs
end
- # def checksum_attributes
- # end
+ def bounding_dates
+ [
+ date_ranges.map(&:first).min,
+ date_ranges.map(&:last).max,
+ ]
+ end
+ def color
+ _color = read_attribute(:color)
+ _color.present? ? _color : nil
+ end
end
end
diff --git a/app/models/chouette/route.rb b/app/models/chouette/route.rb
index 3729deb7d..928b65f13 100644
--- a/app/models/chouette/route.rb
+++ b/app/models/chouette/route.rb
@@ -1,20 +1,25 @@
-
module Chouette
class Route < Chouette::TridentActiveRecord
- has_paper_trail
+ has_metadata
+
include RouteRestrictions
include ChecksumSupport
include ObjectidSupport
-
extend Enumerize
- extend ActiveModel::Naming
+
+ if ENV["CHOUETTE_ROUTE_POSITION_CHECK"] == "true" || !Rails.env.production?
+ after_commit do
+ positions = stop_points.pluck(:position)
+ Rails.logger.debug "Check positions in Route #{id} : #{positions.inspect}"
+ if positions.size != positions.uniq.size
+ raise "DUPLICATED stop_points positions in Route #{id} : #{positions.inspect}"
+ end
+ end
+ end
enumerize :direction, in: %i(straight_forward backward clockwise counter_clockwise north north_west west south_west south south_east east north_east)
enumerize :wayback, in: %i(outbound inbound), default: :outbound
- # FIXME http://jira.codehaus.org/browse/JRUBY-6358
- self.primary_key = "id"
-
def self.nullable_attributes
[:published_name, :comment, :number, :name, :direction, :wayback]
end
@@ -72,32 +77,41 @@ module Chouette
validates_presence_of :name
validates_presence_of :published_name
validates_presence_of :line
-
- # validates_presence_of :direction
- # validates_presence_of :wayback
-
validates :wayback, inclusion: { in: self.wayback.values }
+ after_commit :calculate_costs!,
+ on: [:create, :update],
+ if: ->() {
+ # Ensure the call back doesn't run during a referential merge
+ !referential.in_referential_suite? &&
+ TomTom.enabled?
+ }
+
+ def duplicate opposite=false
+ overrides = {
+ 'opposite_route_id' => nil,
+ 'name' => I18n.t('activerecord.copy', name: self.name)
+ }
+ keys_for_create = attributes.keys - %w{id objectid created_at updated_at}
+ atts_for_create = attributes
+ .slice(*keys_for_create)
+ .merge(overrides)
+ if opposite
+ atts_for_create[:wayback] = self.opposite_wayback
+ atts_for_create[:name] = I18n.t('routes.opposite', name: self.name)
+ atts_for_create[:published_name] = atts_for_create[:name]
+ atts_for_create[:opposite_route_id] = self.id
+ end
+ new_route = self.class.create!(atts_for_create)
+ duplicate_stop_points(for_route: new_route, opposite: opposite)
+ new_route
+ end
- def duplicate
- overrides = {
- 'opposite_route_id' => nil,
- 'name' => I18n.t('activerecord.copy', name: self.name)
- }
- keys_for_create = attributes.keys - %w{id objectid created_at updated_at}
- atts_for_create = attributes
- .slice(*keys_for_create)
- .merge(overrides)
- new_route = self.class.create!(atts_for_create)
- duplicate_stop_points(for_route: new_route)
- new_route
- end
-
- def duplicate_stop_points(for_route:)
- stop_points.each(&duplicate_stop_point(for_route: for_route))
+ def duplicate_stop_points(for_route:, opposite: false)
+ stop_points.each(&duplicate_stop_point(for_route: for_route, opposite: opposite))
end
- def duplicate_stop_point(for_route:)
+ def duplicate_stop_point(for_route:, opposite: false)
-> stop_point do
- stop_point.duplicate(for_route: for_route)
+ stop_point.duplicate(for_route: for_route, opposite: opposite)
end
end
@@ -134,10 +148,12 @@ module Chouette
values = self.slice(*['name', 'published_name', 'wayback']).values
values.tap do |attrs|
attrs << self.stop_points.sort_by(&:position).map{|sp| [sp.stop_area.user_objectid, sp.for_boarding, sp.for_alighting]}
- attrs << self.routing_constraint_zones.map(&:checksum)
+ attrs << self.routing_constraint_zones.map(&:checksum).sort
end
end
+ has_checksum_children StopPoint
+
def geometry
points = stop_areas.map(&:to_lat_lng).compact.map do |loc|
[loc.lng, loc.lat]
@@ -191,6 +207,10 @@ module Chouette
journey_pattern
end
+ def calculate_costs!
+ RouteWayCostWorker.perform_async(referential.id, id)
+ end
+
protected
def self.vehicle_journeys_timeless(stop_point_id)
diff --git a/app/models/chouette/routing_constraint_zone.rb b/app/models/chouette/routing_constraint_zone.rb
index 58703598e..2cfb60bdd 100644
--- a/app/models/chouette/routing_constraint_zone.rb
+++ b/app/models/chouette/routing_constraint_zone.rb
@@ -1,6 +1,6 @@
module Chouette
class RoutingConstraintZone < Chouette::TridentActiveRecord
- has_paper_trail
+ # has_metadata
include ChecksumSupport
include ObjectidSupport
@@ -30,6 +30,11 @@ module Chouette
]
end
+ def update_route_checksum
+ route.update_checksum!
+ end
+ after_commit :update_route_checksum
+
def stop_points_belong_to_route
return unless route
diff --git a/app/models/chouette/stop_area.rb b/app/models/chouette/stop_area.rb
index 7170dd217..b933e1944 100644
--- a/app/models/chouette/stop_area.rb
+++ b/app/models/chouette/stop_area.rb
@@ -2,11 +2,12 @@ require 'geokit'
require 'geo_ruby'
module Chouette
class StopArea < Chouette::ActiveRecord
- has_paper_trail class_name: 'PublicVersion'
+ has_metadata
include ProjectionFields
include StopAreaRestrictions
include StopAreaReferentialSupport
include ObjectidSupport
+ include CustomFieldsSupport
extend Enumerize
enumerize :area_type, in: Chouette::AreaType::ALL
@@ -32,7 +33,7 @@ module Chouette
after_update :journey_patterns_control_route_sections,
if: Proc.new { |stop_area| ['boarding_position', 'quay'].include? stop_area.stop_area_type }
- validates_format_of :registration_number, :with => %r{\A[\d\w_\-]+\Z}, :allow_blank => true
+ # validates_format_of :registration_number, :with => %r{\A[\d\w_:\-]+\Z}, :allow_blank => true
validates_presence_of :name
validates_presence_of :kind
validates_presence_of :latitude, :if => :longitude
@@ -46,6 +47,11 @@ module Chouette
validates_numericality_of :waiting_time, greater_than_or_equal_to: 0, only_integer: true, if: :waiting_time
validate :parent_area_type_must_be_greater
validate :area_type_of_right_kind
+ validate :registration_number_is_set
+
+ before_validation do
+ self.registration_number = self.stop_area_referential.generate_registration_number unless self.registration_number.present?
+ end
def self.nullable_attributes
[:registration_number, :street_name, :country_code, :fare_code,
@@ -73,6 +79,22 @@ module Chouette
end
end
+ def registration_number_is_set
+ return unless self.stop_area_referential.registration_number_format.present?
+ if self.stop_area_referential.stop_areas.where(registration_number: self.registration_number).\
+ where.not(id: self.id).exists?
+ errors.add(:registration_number, I18n.t('stop_areas.errors.registration_number.already_taken'))
+ end
+
+ unless self.registration_number.present?
+ errors.add(:registration_number, I18n.t('stop_areas.errors.registration_number.cannot_be_empty'))
+ end
+
+ unless self.stop_area_referential.validates_registration_number(self.registration_number)
+ errors.add(:registration_number, I18n.t('stop_areas.errors.registration_number.invalid', mask: self.stop_area_referential.registration_number_format))
+ end
+ end
+
after_update :clean_invalid_access_links
before_save :coordinates_to_lat_lng
@@ -362,14 +384,15 @@ module Chouette
end
def activated?
- deleted_at.nil?
+ !!(deleted_at.nil? && confirmed_at)
end
def deactivated?
- !activated?
+ deleted_at.present?
end
def activate
+ self.confirmed_at = Time.now
self.deleted_at = nil
end
@@ -378,6 +401,7 @@ module Chouette
end
def activate!
+ update_attribute :confirmed_at, Time.now
update_attribute :deleted_at, nil
end
@@ -385,14 +409,45 @@ module Chouette
update_attribute :deleted_at, Time.now
end
+ def status
+ return :deleted if deleted_at
+ return :confirmed if confirmed_at
+
+ :in_creation
+ end
+
+ def status=(status)
+ case status&.to_sym
+ when :deleted
+ deactivate
+ when :confirmed
+ activate
+ when :in_creation
+ self.confirmed_at = self.deleted_at = nil
+ end
+ end
+
+ def self.statuses
+ %i{in_creation confirmed deleted}
+ end
+
def time_zone_offset
return 0 unless time_zone.present?
ActiveSupport::TimeZone[time_zone]&.utc_offset
end
- def country_name
+ def full_time_zone_name
+ return unless time_zone.present?
+ return unless ActiveSupport::TimeZone[time_zone].present?
+ ActiveSupport::TimeZone[time_zone].tzinfo.name
+ end
+
+ 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/stop_point.rb b/app/models/chouette/stop_point.rb
index 3b9eaa2f6..b0906d65f 100644
--- a/app/models/chouette/stop_point.rb
+++ b/app/models/chouette/stop_point.rb
@@ -1,6 +1,6 @@
module Chouette
class StopPoint < Chouette::TridentActiveRecord
- has_paper_trail
+ has_metadata
def self.policy_class
RoutePolicy
end
@@ -9,9 +9,6 @@ module Chouette
include ForAlightingEnumerations
include ObjectidSupport
- # FIXME http://jira.codehaus.org/browse/JRUBY-6358
- self.primary_key = "id"
-
belongs_to :stop_area
belongs_to :route, inverse_of: :stop_points
has_many :vehicle_journey_at_stops, :dependent => :destroy
@@ -19,7 +16,6 @@ module Chouette
acts_as_list :scope => :route, top_of_list: 0
-
validates_presence_of :stop_area
validate :stop_area_id_validation
def stop_area_id_validation
@@ -30,7 +26,7 @@ module Chouette
scope :default_order, -> { order("position") }
- delegate :name, to: :stop_area
+ delegate :name, :registration_number, :kind, :area_type, to: :stop_area
before_destroy :remove_dependent_journey_pattern_stop_points
def remove_dependent_journey_pattern_stop_points
@@ -41,11 +37,12 @@ module Chouette
end
end
- def duplicate(for_route:)
+ def duplicate(for_route:, opposite: false)
keys_for_create = attributes.keys - %w{id objectid created_at updated_at}
atts_for_create = attributes
.slice(*keys_for_create)
.merge('route_id' => for_route.id)
+ atts_for_create["position"] = self.route.stop_points.size - atts_for_create["position"] if opposite
self.class.create!(atts_for_create)
end
diff --git a/app/models/chouette/time_table.rb b/app/models/chouette/time_table.rb
index 20d6e69ac..29e3808e7 100644
--- a/app/models/chouette/time_table.rb
+++ b/app/models/chouette/time_table.rb
@@ -1,14 +1,12 @@
module Chouette
class TimeTable < Chouette::TridentActiveRecord
- has_paper_trail
+ has_metadata
include ChecksumSupport
include TimeTableRestrictions
include ObjectidSupport
include ApplicationDaysSupport
include TimetableSupport
- # FIXME http://jira.codehaus.org/browse/JRUBY-6358
- self.primary_key = "id"
acts_as_taggable
attr_accessor :tag_search
@@ -18,7 +16,7 @@ module Chouette
end
ransacker :unaccented_comment, formatter: ->(val){ val.parameterize } do
- Arel.sql('unaccent(comment)')
+ Arel.sql('unaccent(time_tables.comment)')
end
has_and_belongs_to_many :vehicle_journeys, :class_name => 'Chouette::VehicleJourney'
@@ -83,6 +81,11 @@ module Chouette
chunk.values.delete_if {|dates| dates.count < 2}
end
+ def color
+ _color = read_attribute(:color)
+ _color.present? ? _color : nil
+ end
+
def convert_continuous_dates_to_periods
chunks = self.continuous_dates
diff --git a/app/models/chouette/time_table_date.rb b/app/models/chouette/time_table_date.rb
index 98d8fa765..6a68d7fe1 100644
--- a/app/models/chouette/time_table_date.rb
+++ b/app/models/chouette/time_table_date.rb
@@ -2,7 +2,6 @@ module Chouette
class TimeTableDate < Chouette::ActiveRecord
include ChecksumSupport
- self.primary_key = "id"
belongs_to :time_table, inverse_of: :dates
acts_as_list :scope => 'time_table_id = #{time_table_id}',:top_of_list => 0
diff --git a/app/models/chouette/time_table_period.rb b/app/models/chouette/time_table_period.rb
index d9b707675..6965d828a 100644
--- a/app/models/chouette/time_table_period.rb
+++ b/app/models/chouette/time_table_period.rb
@@ -2,7 +2,6 @@ module Chouette
class TimeTablePeriod < Chouette::ActiveRecord
include ChecksumSupport
- self.primary_key = "id"
belongs_to :time_table, inverse_of: :periods
acts_as_list :scope => 'time_table_id = #{time_table_id}',:top_of_list => 0
diff --git a/app/models/chouette/timeband.rb b/app/models/chouette/timeband.rb
index 6155ffc77..38260b755 100644
--- a/app/models/chouette/timeband.rb
+++ b/app/models/chouette/timeband.rb
@@ -9,8 +9,7 @@ module Chouette
class Timeband < Chouette::TridentActiveRecord
include ObjectidSupport
- has_paper_trail
- self.primary_key = "id"
+ has_metadata
validates :start_time, :end_time, presence: true
validates_with Chouette::TimebandValidator
diff --git a/app/models/chouette/vehicle_journey.rb b/app/models/chouette/vehicle_journey.rb
index 9b94f7f0e..814eeb388 100644
--- a/app/models/chouette/vehicle_journey.rb
+++ b/app/models/chouette/vehicle_journey.rb
@@ -1,13 +1,12 @@
# coding: utf-8
module Chouette
class VehicleJourney < Chouette::TridentActiveRecord
- has_paper_trail
+ has_metadata
include ChecksumSupport
+ include CustomFieldsSupport
include VehicleJourneyRestrictions
include ObjectidSupport
include StifTransportModeEnumerations
- # FIXME http://jira.codehaus.org/browse/JRUBY-6358
- self.primary_key = "id"
enum journey_category: { timed: 0, frequency: 1 }
@@ -16,7 +15,7 @@ module Chouette
attr_reader :time_table_tokens
def self.nullable_attributes
- [:transport_mode, :published_journey_name, :vehicle_type_identifier, :published_journey_identifier, :comment, :status_value]
+ [:transport_mode, :published_journey_name, :vehicle_type_identifier, :published_journey_identifier, :comment]
end
belongs_to :company
@@ -63,7 +62,7 @@ module Chouette
scope :with_ordered_stop_area_ids, ->(first, second){
if first.present? && second.present?
joins(journey_pattern: :stop_points).
- joins('INNER JOIN "journey_patterns" ON "journey_patterns"."id" = "vehicle_journeys"."journey_pattern_id" INNER JOIN "journey_patterns_stop_points" ON "journey_patterns_stop_points"."journey_pattern_id" = "journey_patterns"."id" INNER JOIN "stop_points" as "second_stop_points" ON "stop_points"."id" = "journey_patterns_stop_points"."stop_point_id"').
+ joins('INNER JOIN "journey_patterns" ON "journey_patterns"."id" = "vehicle_journeys"."journey_pattern_id" INNER JOIN "journey_patterns_stop_points" ON "journey_patterns_stop_points"."journey_pattern_id" = "journey_patterns"."id" INNER JOIN "stop_points" as "second_stop_points" ON "second_stop_points"."id" = "journey_patterns_stop_points"."stop_point_id"').
where('stop_points.stop_area_id = ?', first).
where('second_stop_points.stop_area_id = ? and stop_points.position < second_stop_points.position', second)
else
@@ -89,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
@@ -142,9 +140,12 @@ module Chouette
attrs << self.published_journey_identifier
attrs << self.try(:company).try(:get_objectid).try(:local_id)
attrs << self.footnotes.map(&:checksum).sort
+
vjas = self.vehicle_journey_at_stops
vjas += VehicleJourneyAtStop.where(vehicle_journey_id: self.id) unless self.new_record?
attrs << vjas.uniq.sort_by { |s| s.stop_point&.position }.map(&:checksum).sort
+
+ attrs << self.purchase_windows.map(&:checksum).sort if purchase_windows.present?
end
end
@@ -156,6 +157,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
@@ -238,11 +247,13 @@ module Chouette
end
def self.state_update route, state
+ objects = []
transaction do
state.each do |item|
item.delete('errors')
vj = find_by(objectid: item['objectid']) || state_create_instance(route, item)
next if item['deletable'] && vj.persisted? && vj.destroy
+ objects << vj
if vj.state_update_vjas?(item['vehicle_journey_at_stops'])
vj.update_vjas_from_state(item['vehicle_journey_at_stops'])
@@ -270,6 +281,7 @@ module Chouette
item['vehicle_journey_at_stops'].map {|vjas| vjas.delete('new_record') }
end
state.delete_if {|item| item['deletable']}
+ objects
end
def self.state_create_instance route, item
@@ -340,19 +352,31 @@ module Chouette
end
end
- def self.custom_fields
- CustomField.where(resource_type: self.name.split("::").last)
- end
-
-
- def custom_fields
- Hash[*self.class.custom_fields.map do |v|
- [v.code, v.slice(:code, :name, :field_type, :options).update(value: custom_field_value(v.code))]
- end.flatten]
- end
-
- def custom_field_value key
- (custom_field_values || {})[key.to_s]
+ def fill_passing_time_at_borders
+ encountered_borders = []
+ previous_stop = nil
+ vehicle_journey_at_stops.each do |vjas|
+ sp = vjas.stop_point
+ if sp.stop_area.area_type == "border"
+ encountered_borders << vjas
+ else
+ if encountered_borders.any?
+ before_cost = journey_pattern.costs_between previous_stop.stop_point, encountered_borders.first.stop_point
+ after_cost = journey_pattern.costs_between encountered_borders.last.stop_point, sp
+ if before_cost && before_cost[:distance] && after_cost && after_cost[:distance]
+ before_distance = before_cost[:distance].to_f
+ after_distance = after_cost[:distance].to_f
+ time = previous_stop.departure_time + before_distance / (before_distance+after_distance) * (vjas.arrival_time - previous_stop.departure_time)
+ encountered_borders.each do |b|
+ b.update_attribute :arrival_time, time
+ b.update_attribute :departure_time, time
+ end
+ end
+ encountered_borders = []
+ end
+ previous_stop = vjas
+ end
+ end
end
def self.matrix(vehicle_journeys)
@@ -417,10 +441,5 @@ module Chouette
')
.where('"time_tables_vehicle_journeys"."vehicle_journey_id" IS NULL')
end
-
- def self.lines
- lines_query = joins(:route).select("routes.line_id").reorder(nil).except(:group).pluck(:'routes.line_id')
- Chouette::Line.where(id: lines_query)
- end
end
end
diff --git a/app/models/chouette/vehicle_journey_at_stop.rb b/app/models/chouette/vehicle_journey_at_stop.rb
index 3b4f35f13..3f5bd5abf 100644
--- a/app/models/chouette/vehicle_journey_at_stop.rb
+++ b/app/models/chouette/vehicle_journey_at_stop.rb
@@ -4,10 +4,10 @@ module Chouette
include Chouette::ForAlightingEnumerations
include ChecksumSupport
- DAY_OFFSET_MAX = 1
+ DAY_OFFSET_MAX = 2
- # FIXME http://jira.codehaus.org/browse/JRUBY-6358
- self.primary_key = "id"
+ @@day_offset_max = DAY_OFFSET_MAX
+ mattr_accessor :day_offset_max
belongs_to :stop_point
belongs_to :vehicle_journey
@@ -42,7 +42,7 @@ module Chouette
I18n.t(
'vehicle_journey_at_stops.errors.day_offset_must_not_exceed_max',
short_id: vehicle_journey&.get_objectid&.short_id,
- max: DAY_OFFSET_MAX + 1
+ max: Chouette::VehicleJourneyAtStop.day_offset_max + 1
)
)
end
@@ -53,7 +53,7 @@ module Chouette
I18n.t(
'vehicle_journey_at_stops.errors.day_offset_must_not_exceed_max',
short_id: vehicle_journey&.get_objectid&.short_id,
- max: DAY_OFFSET_MAX + 1
+ max: Chouette::VehicleJourneyAtStop.day_offset_max + 1
)
)
end
@@ -64,7 +64,7 @@ module Chouette
# nil offsets. Handle these gracefully by forcing them to a 0 offset.
offset ||= 0
- offset < 0 || offset > DAY_OFFSET_MAX
+ offset < 0 || offset > Chouette::VehicleJourneyAtStop.day_offset_max
end
def checksum_attributes
@@ -84,12 +84,12 @@ module Chouette
format_time arrival_time.utc
end
- def departure_local_time
- local_time departure_time
+ def departure_local_time offset=nil
+ local_time departure_time, offset
end
- def arrival_local_time
- local_time arrival_time
+ def arrival_local_time offset=nil
+ local_time arrival_time, offset
end
def departure_local
@@ -100,12 +100,15 @@ module Chouette
format_time arrival_local_time
end
+ def time_zone_offset
+ return 0 unless stop_point&.stop_area&.time_zone.present?
+ ActiveSupport::TimeZone[stop_point.stop_area.time_zone]&.utc_offset || 0
+ end
+
private
- def local_time time
- return unless time
- return time unless stop_point&.stop_area&.time_zone.present?
- return time unless ActiveSupport::TimeZone[stop_point.stop_area.time_zone].present?
- time + ActiveSupport::TimeZone[stop_point.stop_area.time_zone].utc_offset
+ def local_time time, offset=nil
+ return nil unless time
+ time + (offset || time_zone_offset)
end
def format_time time
diff --git a/app/models/chouette/vehicle_journey_at_stops_day_offset.rb b/app/models/chouette/vehicle_journey_at_stops_day_offset.rb
index 7497cd72c..cfa0e8bfc 100644
--- a/app/models/chouette/vehicle_journey_at_stops_day_offset.rb
+++ b/app/models/chouette/vehicle_journey_at_stops_day_offset.rb
@@ -4,31 +4,32 @@ module Chouette
@at_stops = at_stops
end
- def calculate!
- arrival_offset = 0
- departure_offset = 0
+ def time_from_fake_date fake_date
+ fake_date - fake_date.to_date.to_time
+ end
+ def calculate!
+ offset = 0
+ tz_offset = @at_stops.first&.time_zone_offset
@at_stops.inject(nil) do |prior_stop, stop|
next stop if prior_stop.nil?
# we only compare time of the day, not actual times
- stop_arrival_time = stop.arrival_time - stop.arrival_time.to_date.to_time
- stop_departure_time = stop.departure_time - stop.departure_time.to_date.to_time
- prior_stop_arrival_time = prior_stop.arrival_time - prior_stop.arrival_time.to_date.to_time
- prior_stop_departure_time = prior_stop.departure_time - prior_stop.departure_time.to_date.to_time
-
- if stop_arrival_time < prior_stop_departure_time ||
- stop_arrival_time < prior_stop_arrival_time
- arrival_offset += 1
+ stop_arrival_time = time_from_fake_date stop.arrival_local_time(tz_offset)
+ stop_departure_time = time_from_fake_date stop.departure_local_time(tz_offset)
+ prior_stop_departure_time = time_from_fake_date prior_stop.departure_local_time(tz_offset)
+
+ if stop_arrival_time < prior_stop_departure_time
+ offset += 1
end
- if stop_departure_time < stop_arrival_time ||
- stop_departure_time < prior_stop_departure_time
- departure_offset += 1
+ stop.arrival_day_offset = offset
+
+ if stop_departure_time < stop_arrival_time
+ offset += 1
end
- stop.arrival_day_offset = arrival_offset
- stop.departure_day_offset = departure_offset
+ stop.departure_day_offset = offset
stop
end
diff --git a/app/models/clean_up.rb b/app/models/clean_up.rb
index 7aab7f32e..ec47489e9 100644
--- a/app/models/clean_up.rb
+++ b/app/models/clean_up.rb
@@ -1,4 +1,4 @@
-class CleanUp < ActiveRecord::Base
+class CleanUp < ApplicationModel
extend Enumerize
include AASM
belongs_to :referential
diff --git a/app/models/clean_up_result.rb b/app/models/clean_up_result.rb
index 24d262deb..dff4f5acd 100644
--- a/app/models/clean_up_result.rb
+++ b/app/models/clean_up_result.rb
@@ -1,3 +1,3 @@
-class CleanUpResult < ActiveRecord::Base
+class CleanUpResult < ApplicationModel
belongs_to :clean_up
end
diff --git a/app/models/compliance_check.rb b/app/models/compliance_check.rb
index 55f2ae228..4ef6170e9 100644
--- a/app/models/compliance_check.rb
+++ b/app/models/compliance_check.rb
@@ -1,14 +1,23 @@
-class ComplianceCheck < ActiveRecord::Base
+class ComplianceCheck < ApplicationModel
+ include ComplianceItemSupport
self.inheritance_column = nil
extend Enumerize
belongs_to :compliance_check_set
belongs_to :compliance_check_block
-
+
enumerize :criticity, in: %i(warning error), scope: true, default: :warning
validates :criticity, presence: true
validates :name, presence: true
validates :code, presence: true
validates :origin_code, presence: true
+
+ def control_class
+ compliance_control_name.present? ? compliance_control_name.constantize : nil
+ end
+
+ delegate :predicate, to: :control_class, allow_nil: true
+ delegate :prerequisite, to: :control_class, allow_nil: true
+
end
diff --git a/app/models/compliance_check_block.rb b/app/models/compliance_check_block.rb
index 059547e1b..e4f4c1c37 100644
--- a/app/models/compliance_check_block.rb
+++ b/app/models/compliance_check_block.rb
@@ -1,4 +1,4 @@
-class ComplianceCheckBlock < ActiveRecord::Base
+class ComplianceCheckBlock < ApplicationModel
include StifTransportModeEnumerations
include StifTransportSubmodeEnumerations
diff --git a/app/models/compliance_check_message.rb b/app/models/compliance_check_message.rb
index 738bd4a4b..a4b1062f6 100644
--- a/app/models/compliance_check_message.rb
+++ b/app/models/compliance_check_message.rb
@@ -1,4 +1,4 @@
-class ComplianceCheckMessage < ActiveRecord::Base
+class ComplianceCheckMessage < ApplicationModel
extend Enumerize
belongs_to :compliance_check_set
diff --git a/app/models/compliance_check_message_export.rb b/app/models/compliance_check_message_export.rb
index 04e1a9caa..9b7f90fac 100644
--- a/app/models/compliance_check_message_export.rb
+++ b/app/models/compliance_check_message_export.rb
@@ -22,16 +22,18 @@ class ComplianceCheckMessageExport
end
def column_names
- ["criticity", "message key", "resource objectid", "link", "message"]
+ ["criticity", "message_key", "resource_objectid", "link", "message"].map {|c| ComplianceCheckMessage.tmf(c)}
end
def to_csv(options = {})
- CSV.generate(options.slice(:col_sep, :quote_char, :force_quotes)) do |csv|
+ csv_string = CSV.generate(options.slice(:col_sep, :quote_char, :force_quotes)) do |csv|
csv << column_names
compliance_check_messages.each do |compliance_check_message|
csv << [compliance_check_message.compliance_check.criticity, *compliance_check_message.message_attributes.values_at('test_id', 'source_objectid'), options[:server_url] + compliance_check_message.message_attributes['source_object_path'], I18n.t("compliance_check_messages.#{compliance_check_message.message_key}", compliance_check_message.message_attributes.deep_symbolize_keys)]
end
end
+ # We add a BOM to indicate we use UTF-8
+ "\uFEFF" + csv_string
end
def to_zip(temp_file,options = {})
diff --git a/app/models/compliance_check_resource.rb b/app/models/compliance_check_resource.rb
index 2989bf3cf..d2f782e2b 100644
--- a/app/models/compliance_check_resource.rb
+++ b/app/models/compliance_check_resource.rb
@@ -1,9 +1,9 @@
-class ComplianceCheckResource < ActiveRecord::Base
+class ComplianceCheckResource < ApplicationModel
extend Enumerize
belongs_to :compliance_check_set
- enumerize :status, in: %i(OK ERROR WARNING IGNORED), scope: true
+ enumerize :status, in: %i(OK ERROR WARNING IGNORED)
validates_presence_of :compliance_check_set
end
diff --git a/app/models/compliance_check_set.rb b/app/models/compliance_check_set.rb
index 289fc134f..8b1dbdd68 100644
--- a/app/models/compliance_check_set.rb
+++ b/app/models/compliance_check_set.rb
@@ -1,6 +1,7 @@
-class ComplianceCheckSet < ActiveRecord::Base
+class ComplianceCheckSet < ApplicationModel
extend Enumerize
- has_paper_trail class_name: 'PublicVersion'
+
+ has_metadata
belongs_to :referential
belongs_to :compliance_control_set
@@ -49,39 +50,25 @@ class ComplianceCheckSet < ActiveRecord::Base
end
def update_status
- statuses = compliance_check_resources.map do |resource|
- case resource.status
- when 'ERROR'
- return update(status: 'failed')
- when 'WARNING'
- return update(status: 'warning')
- else
- resource.status
+ status =
+ if compliance_check_resources.where(status: 'ERROR').count > 0
+ 'failed'
+ elsif compliance_check_resources.where(status: ["WARNING", "IGNORED"]).count > 0
+ 'warning'
+ elsif compliance_check_resources.where(status: "OK").count == compliance_check_resources.count
+ 'successful'
end
- end
- if statuses_ok_or_ignored?(statuses)
- return update(status: 'successful')
+ attributes = {
+ status: status
+ }
+
+ if self.class.finished_statuses.include?(status)
+ attributes[:ended_at] = Time.now
end
- true
+ update attributes
end
- private
-
- def statuses_ok_or_ignored?(statuses)
- uniform_statuses = statuses.uniq
-
- (
- # All statuses OK
- uniform_statuses.length == 1 &&
- uniform_statuses.first == 'OK'
- ) ||
- (
- # Statuses OK or IGNORED
- uniform_statuses.length == 2 &&
- uniform_statuses.include?('OK') &&
- uniform_statuses.include?('IGNORED')
- )
- end
+
end
diff --git a/app/models/compliance_control.rb b/app/models/compliance_control.rb
index 298a63ab9..672fb128c 100644
--- a/app/models/compliance_control.rb
+++ b/app/models/compliance_control.rb
@@ -1,18 +1,16 @@
-class ComplianceControl < ActiveRecord::Base
+class ComplianceControl < ApplicationModel
+ include ComplianceItemSupport
class << self
def criticities; %i(warning error) end
def default_code; "" end
- def dynamic_attributes
- stored_attributes[:control_attributes] || []
- end
def policy_class
ComplianceControlPolicy
end
def subclass_patterns
- {
+ {
generic: 'Generic',
journey_pattern: 'JourneyPattern',
line: 'Line',
@@ -30,6 +28,9 @@ class ComplianceControl < ActiveRecord::Base
end
super
end
+
+ def predicate; I18n.t("compliance_controls.#{self.name.underscore}.description") end
+ def prerequisite; I18n.t("compliance_controls.#{self.name.underscore}.prerequisite") end
end
extend Enumerize
@@ -45,26 +46,25 @@ class ComplianceControl < ActiveRecord::Base
validates :compliance_control_set, presence: true
validate def coherent_control_set
- return true if compliance_control_block_id.nil?
- ids = [compliance_control_block.compliance_control_set_id, compliance_control_set_id]
- return true if ids.first == ids.last
- names = ids.map{|id| ComplianceControlSet.find(id).name}
- errors.add(:coherent_control_set,
- I18n.t('compliance_controls.errors.incoherent_control_sets',
- indirect_set_name: names.first,
- direct_set_name: names.last))
-end
-
+ return true if compliance_control_block_id.nil?
+ ids = [compliance_control_block.compliance_control_set_id, compliance_control_set_id]
+ return true if ids.first == ids.last
+ names = ids.map{|id| ComplianceControlSet.find(id).name}
+ errors.add(:coherent_control_set,
+ I18n.t('compliance_controls.errors.incoherent_control_sets',
+ indirect_set_name: names.first,
+ direct_set_name: names.last))
+ end
-def initialize(attributes = {})
- super
- self.name ||= I18n.t("activerecord.models.#{self.class.name.underscore}.one")
- self.code ||= self.class.default_code
- self.origin_code ||= self.class.default_code
-end
+ def initialize(attributes = {})
+ super
+ self.name ||= I18n.t("activerecord.models.#{self.class.name.underscore}.one")
+ self.code ||= self.class.default_code
+ self.origin_code ||= self.class.default_code
+ end
-def predicate; I18n.t("compliance_controls.#{self.class.name.underscore}.description") end
-def prerequisite; I18n.t('compliance_controls.metas.no_prerequisite'); end
+ def predicate; self.class.predicate end
+ def prerequisite; self.class.prerequisite end
end
@@ -76,6 +76,7 @@ require_dependency 'generic_attribute_control/uniqueness'
require_dependency 'journey_pattern_control/duplicates'
require_dependency 'journey_pattern_control/vehicle_journey'
require_dependency 'line_control/route'
+require_dependency 'line_control/lines_scope'
require_dependency 'route_control/duplicates'
require_dependency 'route_control/journey_pattern'
require_dependency 'route_control/minimum_length'
diff --git a/app/models/compliance_control_block.rb b/app/models/compliance_control_block.rb
index d7d84fd06..6a3c8a34e 100644
--- a/app/models/compliance_control_block.rb
+++ b/app/models/compliance_control_block.rb
@@ -1,4 +1,4 @@
-class ComplianceControlBlock < ActiveRecord::Base
+class ComplianceControlBlock < ApplicationModel
include StifTransportModeEnumerations
include StifTransportSubmodeEnumerations
@@ -12,6 +12,8 @@ class ComplianceControlBlock < ActiveRecord::Base
validates :transport_mode, presence: true
validates :compliance_control_set, presence: true
+ validates_uniqueness_of :condition_attributes, scope: :compliance_control_set_id
+
def name
ApplicationController.helpers.transport_mode_text(self)
end
diff --git a/app/models/compliance_control_set.rb b/app/models/compliance_control_set.rb
index c0ea692f2..4f0f86d08 100644
--- a/app/models/compliance_control_set.rb
+++ b/app/models/compliance_control_set.rb
@@ -1,5 +1,6 @@
-class ComplianceControlSet < ActiveRecord::Base
- has_paper_trail class_name: 'PublicVersion'
+class ComplianceControlSet < ApplicationModel
+ has_metadata
+
belongs_to :organisation
has_many :compliance_control_blocks, dependent: :destroy
has_many :compliance_controls, dependent: :destroy
diff --git a/app/models/concerns/application_days_support.rb b/app/models/concerns/application_days_support.rb
index 348436aa4..6086d9580 100644
--- a/app/models/concerns/application_days_support.rb
+++ b/app/models/concerns/application_days_support.rb
@@ -10,48 +10,47 @@ module ApplicationDaysSupport
SUNDAY = 256
EVERYDAY = MONDAY | TUESDAY | WEDNESDAY | THURSDAY | FRIDAY | SATURDAY | SUNDAY
+ ALL_DAYS = %w(monday tuesday wednesday thursday friday saturday sunday).freeze
+
def display_day_types
- %w(monday tuesday wednesday thursday friday saturday sunday).select{ |d| self.send(d) }.map{ |d| self.human_attribute_name(d).first(2)}.join(', ')
+ ALL_DAYS.select{ |d| self.send(d) }.map{ |d| self.human_attribute_name(d).first(2)}.join(', ')
end
def day_by_mask(flag)
- 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)
- end
+ def self.day_by_mask(int_day_types,flag)
+ int_day_types & flag == flag
+ 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)
+ def self.all_days
+ ALL_DAYS
end
end
+ def valid_days
+ self.class.valid_days int_day_types
+ end
+
def monday
day_by_mask(MONDAY)
end
diff --git a/app/models/concerns/checksum_support.rb b/app/models/concerns/checksum_support.rb
index 92103798e..de3a6e16b 100644
--- a/app/models/concerns/checksum_support.rb
+++ b/app/models/concerns/checksum_support.rb
@@ -40,6 +40,7 @@ module ChecksumSupport
def current_checksum_source
source = checksum_replace_nil_or_empty_values(self.checksum_attributes)
+ source += self.custom_fields_checksum if self.respond_to?(:custom_fields_checksum)
source.map{ |item|
if item.kind_of?(Array)
item.map{ |x| x.kind_of?(Array) ? "(#{x.join(',')})" : x }.join(',')
diff --git a/app/models/concerns/compliance_item_support.rb b/app/models/concerns/compliance_item_support.rb
new file mode 100644
index 000000000..f44f5719f
--- /dev/null
+++ b/app/models/concerns/compliance_item_support.rb
@@ -0,0 +1,13 @@
+module ComplianceItemSupport
+ extend ActiveSupport::Concern
+ included do
+
+ end
+
+ module ClassMethods
+ def dynamic_attributes
+ stored_attributes[:control_attributes] || []
+ end
+ end
+
+end
diff --git a/app/models/concerns/custom_fields_support.rb b/app/models/concerns/custom_fields_support.rb
new file mode 100644
index 000000000..c39dfd1fc
--- /dev/null
+++ b/app/models/concerns/custom_fields_support.rb
@@ -0,0 +1,62 @@
+module CustomFieldsSupport
+ extend ActiveSupport::Concern
+
+ included do
+ validate :custom_fields_values_are_valid
+ after_initialize :initialize_custom_fields
+
+ def self.custom_fields workgroup=:all
+ fields = CustomField.where(resource_type: self.name.split("::").last)
+ fields = fields.where(workgroup_id: workgroup&.id) if workgroup != :all
+ fields
+ end
+
+ def self.custom_fields_definitions workgroup=:all
+ Hash[*custom_fields(workgroup).map{|cf| [cf.code, cf]}.flatten]
+ end
+
+ def method_missing method_name, *args
+ if method_name =~ /custom_field_*/ && method_name.to_sym != :custom_field_values && !@custom_fields_initialized
+ initialize_custom_fields
+ send method_name, *args
+ else
+ super method_name, *args
+ end
+ end
+
+ def custom_fields workgroup=:all
+ CustomField::Collection.new self, workgroup
+ end
+
+ def custom_fields_checksum
+ custom_fields.values.map(&:checksum)
+ end
+
+ def custom_field_values= vals
+ out = {}
+ custom_fields.each do |code, field|
+ out[code] = field.preprocess_value_for_assignment(vals.symbolize_keys[code.to_sym])
+ end
+ write_attribute :custom_field_values, out
+ end
+
+ def initialize_custom_fields
+ return unless self.attributes.has_key?("custom_field_values")
+ self.custom_field_values ||= {}
+ custom_fields(:all).values.each &:initialize_custom_field
+ custom_fields(:all).each do |k, v|
+ custom_field_values[k] ||= v.default_value
+ end
+ @custom_fields_initialized = true
+ end
+
+ def custom_field_value key
+ (custom_field_values&.stringify_keys || {})[key.to_s]
+ end
+
+ private
+ def custom_fields_values_are_valid
+ custom_fields(:all).values.all?{|cf| cf.valid?}
+ end
+ end
+end
diff --git a/app/models/concerns/iev_interfaces/message.rb b/app/models/concerns/iev_interfaces/message.rb
new file mode 100644
index 000000000..ad41e98b7
--- /dev/null
+++ b/app/models/concerns/iev_interfaces/message.rb
@@ -0,0 +1,9 @@
+module IevInterfaces::Message
+ extend ActiveSupport::Concern
+
+ included do
+ extend Enumerize
+ enumerize :criticity, in: %i(info warning error)
+ validates :criticity, presence: true
+ end
+end
diff --git a/app/models/concerns/iev_interfaces/resource.rb b/app/models/concerns/iev_interfaces/resource.rb
new file mode 100644
index 000000000..7f8c3eefd
--- /dev/null
+++ b/app/models/concerns/iev_interfaces/resource.rb
@@ -0,0 +1,9 @@
+module IevInterfaces::Resource
+ extend ActiveSupport::Concern
+
+ included do
+ extend Enumerize
+ enumerize :status, in: %i(OK ERROR WARNING IGNORED), scope: true
+ validates_presence_of :name, :resource_type, :reference
+ end
+end
diff --git a/app/models/concerns/iev_interfaces/task.rb b/app/models/concerns/iev_interfaces/task.rb
new file mode 100644
index 000000000..f052b3a8f
--- /dev/null
+++ b/app/models/concerns/iev_interfaces/task.rb
@@ -0,0 +1,121 @@
+module IevInterfaces::Task
+ extend ActiveSupport::Concern
+
+ included do
+ belongs_to :parent, polymorphic: true
+ belongs_to :workbench, class_name: "::Workbench"
+ belongs_to :referential
+
+ mount_uploader :file, ImportUploader
+
+ has_many :children, foreign_key: :parent_id, class_name: self.name, dependent: :destroy
+
+ extend Enumerize
+ enumerize :status, in: %w(new pending successful warning failed running aborted canceled), scope: true, default: :new
+
+ validates :name, presence: true
+ validates_presence_of :workbench, :creator
+
+ has_many :messages, class_name: messages_class_name, dependent: :destroy, foreign_key: "#{messages_class_name.split('::').first.downcase}_id"
+ has_many :resources, class_name: resources_class_name, dependent: :destroy, foreign_key: "#{resources_class_name.split('::').first.downcase}_id"
+
+ scope :where_started_at_in, ->(period_range) do
+ where('started_at BETWEEN :begin AND :end', begin: period_range.begin, end: period_range.end)
+ end
+
+ scope :blocked, -> { where('created_at < ? AND status = ?', 4.hours.ago, 'running') }
+
+ before_save :initialize_fields, on: :create
+ after_save :notify_parent
+ end
+
+ module ClassMethods
+ def launched_statuses
+ %w(new pending)
+ end
+
+ def failed_statuses
+ %w(failed aborted canceled)
+ end
+
+ def finished_statuses
+ %w(successful failed warning aborted canceled)
+ end
+
+ def abort_old
+ where(
+ 'created_at < ? AND status NOT IN (?)',
+ 4.hours.ago,
+ finished_statuses
+ ).update_all(status: 'aborted')
+ end
+ end
+
+ def notify_parent
+ return unless self.class.finished_statuses.include?(status)
+
+ return unless parent.present?
+ return if notified_parent_at
+ parent.child_change
+
+ update_column :notified_parent_at, Time.now
+ end
+
+ def children_succeedeed
+ children.with_status(:successful, :warning).count
+ end
+
+ def update_status
+ Rails.logger.info "update_status for #{inspect}"
+ status =
+ if children.where(status: self.class.failed_statuses).count > 0
+ 'failed'
+ elsif children.where(status: "warning").count > 0
+ 'warning'
+ elsif children.where(status: "successful").count == children.count
+ 'successful'
+ else
+ 'running'
+ end
+
+ attributes = {
+ current_step: children.count,
+ status: status
+ }
+
+ if self.class.finished_statuses.include?(status)
+ attributes[:ended_at] = Time.now
+ end
+
+ update attributes
+ end
+
+ def child_change
+ return if self.class.finished_statuses.include?(status)
+ update_status
+ end
+
+ def call_iev_callback
+ return if self.class.finished_statuses.include?(status)
+ threaded_call_boiv_iev
+ end
+
+ private
+
+ def threaded_call_boiv_iev
+ Thread.new(&method(:call_boiv_iev))
+ end
+
+ def call_boiv_iev
+ Rails.logger.error("Begin IEV call for import")
+ Net::HTTP.get iev_callback_url
+ Rails.logger.error("End IEV call for import")
+ rescue Exception => e
+ logger.error "IEV server error : #{e.message}"
+ logger.error e.backtrace.inspect
+ end
+
+ private
+ def initialize_fields
+ end
+end
diff --git a/app/models/concerns/metadata_support.rb b/app/models/concerns/metadata_support.rb
new file mode 100644
index 000000000..c4bedbcda
--- /dev/null
+++ b/app/models/concerns/metadata_support.rb
@@ -0,0 +1,107 @@
+module MetadataSupport
+ extend ActiveSupport::Concern
+
+ included do
+ class << self
+ def has_metadata?
+ !!@has_metadata
+ end
+
+ def has_metadata opts={}
+ @has_metadata = true
+
+ define_method :metadata do
+ attr_name = opts[:attr_name] || :metadata
+ @wrapped_metadata ||= begin
+ wrapped = MetadataSupport::MetadataWrapper.new self.read_attribute(attr_name)
+ wrapped.attribute_name = attr_name
+ wrapped.owner = self
+ wrapped
+ end
+ end
+
+ define_method :metadata= do |val|
+ @wrapped_metadata = nil
+ super val
+ end
+
+ define_method :set_metadata! do |name, value|
+ self.metadata.send "#{name}=", value
+ self.save!
+ end
+ end
+ end
+ end
+
+ def has_metadata?
+ self.class.has_metadata?
+ end
+
+ def merge_metadata_from source
+ return unless source.has_metadata?
+ source_metadata = source.metadata
+ res = {}
+ self.metadata.each do |k, v|
+ unless self.metadata.is_timestamp_attr?(k)
+ ts = self.metadata.timestamp_attr(k)
+ if source_metadata[ts] && source_metadata[ts] > self.metadata[ts]
+ res[k] = source_metadata[k]
+ else
+ res[k] = v
+ end
+ end
+ end
+ self.metadata = res
+ self
+ end
+
+ class MetadataWrapper < OpenStruct
+ attr_accessor :attribute_name, :owner
+
+ def is_timestamp_attr? name
+ name =~ /_updated_at$/
+ end
+
+ def timestamp_attr name
+ "#{name}_updated_at".to_sym
+ end
+
+ def method_missing(mid, *args)
+ out = super(mid, *args)
+ owner.send :write_attribute, attribute_name, @table
+ out = out&.to_time if args.length == 0 && is_timestamp_attr?(mid)
+ out
+ end
+
+ def each
+ @table.each do |k,v|
+ yield k, v
+ end
+ end
+
+ def new_ostruct_member name
+ unless is_timestamp_attr?(name)
+ timestamp_attr_name = timestamp_attr(name)
+ end
+
+ name = name.to_sym
+ unless respond_to?(name)
+ if timestamp_attr_name
+ define_singleton_method(timestamp_attr_name) { @table[timestamp_attr_name]&.to_time }
+ define_singleton_method(name) { @table[name] }
+ else
+ # we are defining an accessor for a timestamp
+ define_singleton_method(name) { @table[name]&.to_time }
+ end
+
+ define_singleton_method("#{name}=") do |x|
+ modifiable[timestamp_attr_name] = Time.now if timestamp_attr_name
+ modifiable[name] = x
+ owner.send :write_attribute, attribute_name, @table
+ end
+ modifiable[timestamp_attr_name] = Time.now if timestamp_attr_name
+ end
+ name
+ end
+ end
+end
diff --git a/app/models/concerns/min_max_values_validation.rb b/app/models/concerns/min_max_values_validation.rb
index eff779d81..a79f5ec85 100644
--- a/app/models/concerns/min_max_values_validation.rb
+++ b/app/models/concerns/min_max_values_validation.rb
@@ -3,11 +3,13 @@ module MinMaxValuesValidation
included do
validates_presence_of :minimum, :maximum
+ validates_numericality_of :minimum, :maximum, allow_nil: true, greater_than_or_equal_to: 0
+ validates_format_of :minimum, :maximum, with: %r{\A\d+(\.\d+)?\Z}
validate :min_max_values_validation
end
def min_max_values_validation
- return true if (minimum && maximum) && (minimum.to_i < maximum.to_i)
+ return true if (minimum && maximum) && (minimum.to_f < maximum.to_f)
errors.add(:minimum, I18n.t('compliance_controls.min_max_values', min: minimum, max: maximum))
end
end
diff --git a/app/models/custom_field.rb b/app/models/custom_field.rb
index 774c8b0f6..88783b5b4 100644
--- a/app/models/custom_field.rb
+++ b/app/models/custom_field.rb
@@ -1,9 +1,298 @@
-class CustomField < ActiveRecord::Base
+class CustomField < ApplicationModel
extend Enumerize
belongs_to :workgroup
- enumerize :field_type, in: %i{list}
+ enumerize :field_type, in: %i{list integer string attachment}
validates :name, uniqueness: {scope: [:resource_type, :workgroup_id]}
- validates :code, uniqueness: {scope: [:resource_type, :workgroup_id], case_sensitive: false}
+ validates :code, uniqueness: {scope: [:resource_type, :workgroup_id], case_sensitive: false}, presence: true
+
+ class Collection < HashWithIndifferentAccess
+ def initialize object, workgroup=:all
+ vals = object.class.custom_fields(workgroup).map do |v|
+ [v.code, CustomField::Instance.new(object, v, object.custom_field_value(v.code))]
+ end
+ super Hash[*vals.flatten]
+ end
+
+ def to_hash
+ HashWithIndifferentAccess[*self.map{|k, v| [k, v.to_hash]}.flatten(1)]
+ end
+ end
+
+ class Instance
+ def self.new owner, custom_field, value
+ field_type = custom_field.field_type
+ klass_name = field_type && "CustomField::Instance::#{field_type.classify}"
+ klass = klass_name.safe_constantize || CustomField::Instance::Base
+ klass.new owner, custom_field, value
+ end
+
+ class Base
+ def initialize owner, custom_field, value
+ @custom_field = custom_field
+ @raw_value = value
+ @owner = owner
+ @errors = []
+ @validated = false
+ @valid = false
+ end
+
+ attr_accessor :owner, :custom_field
+
+ delegate :code, :name, :field_type, to: :@custom_field
+
+ def default_value
+ options["default"]
+ end
+
+ def options
+ @custom_field.options || {}
+ end
+
+ def validate
+ @valid = true
+ end
+
+ def valid?
+ validate unless @validated
+ @valid
+ end
+
+ def value
+ @raw_value
+ end
+
+ def checksum
+ @raw_value
+ end
+
+ def input form_helper
+ @input ||= begin
+ klass_name = field_type && "CustomField::Instance::#{field_type.classify}::Input"
+ klass = klass_name.safe_constantize || CustomField::Instance::Base::Input
+ klass.new self, form_helper
+ end
+ end
+
+ def errors_key
+ "custom_fields.#{code}"
+ end
+
+ def to_hash
+ HashWithIndifferentAccess[*%w(code name field_type options value).map{|k| [k, send(k)]}.flatten(1)]
+ end
+
+ def display_value
+ value
+ end
+
+ def initialize_custom_field
+ end
+
+ def preprocess_value_for_assignment val
+ val
+ end
+
+ def render_partial
+ ActionView::Base.new(Rails.configuration.paths["app/views"].first).render(
+ :partial => "shared/custom_fields/#{field_type}",
+ :locals => { field: self}
+ )
+ end
+
+ class Input
+ def initialize instance, form_helper
+ @instance = instance
+ @form_helper = form_helper
+ end
+
+ def custom_field
+ @instance.custom_field
+ end
+
+ delegate :custom_field, :value, :options, to: :@instance
+ delegate :code, :name, :field_type, to: :custom_field
+
+ def to_s
+ out = form_input
+ out.html_safe
+ end
+
+ protected
+
+ def form_input_id
+ "custom_field_#{code}"
+ end
+
+ def form_input_name
+ "#{@form_helper.object_name}[custom_field_values][#{code}]"
+ end
+
+ def form_input_options
+ {
+ input_html: {value: value, name: form_input_name},
+ label: name
+ }
+ end
+
+ def form_input
+ @form_helper.input form_input_id, form_input_options
+ end
+ end
+ end
+
+ class Integer < Base
+ def value
+ @raw_value&.to_i
+ end
+
+ def validate
+ @valid = true
+ return if @raw_value.is_a?(Fixnum) || @raw_value.is_a?(Float)
+ unless @raw_value.to_s =~ /\A\d*\Z/
+ @owner.errors.add errors_key, "'#{@raw_value}' is not a valid integer"
+ @valid = false
+ end
+ end
+ end
+
+ class List < Integer
+ def validate
+ super
+ return unless value.present?
+ unless value >= 0 && value < options["list_values"].size
+ @owner.errors.add errors_key, "'#{@raw_value}' is not a valid value"
+ @valid = false
+ end
+ end
+
+ def display_value
+ return unless value
+ k = options["list_values"].is_a?(Hash) ? value.to_s : value.to_i
+ options["list_values"][k]
+ end
+
+ class Input < Base::Input
+ def form_input_options
+ collection = options["list_values"]
+ collection = collection.each_with_index.to_a if collection.is_a?(Array)
+ collection = collection.map(&:reverse) if collection.is_a?(Hash)
+ super.update({
+ selected: value,
+ collection: collection
+ })
+ end
+ end
+ end
+
+ class Attachment < Base
+ def initialize_custom_field
+ custom_field_code = self.code
+ _attr_name = attr_name
+ _uploader_name = uploader_name
+ _digest_name = digest_name
+ owner.send :define_singleton_method, "read_uploader" do |attr|
+ if attr.to_s == _attr_name
+ custom_field_values[custom_field_code] && custom_field_values[custom_field_code]["path"]
+ else
+ read_attribute attr
+ end
+ end
+
+ owner.send :define_singleton_method, "write_uploader" do |attr, val|
+ if attr.to_s == _attr_name
+ self.custom_field_values[custom_field_code] ||= {}
+ self.custom_field_values[custom_field_code]["path"] = val
+ self.custom_field_values[custom_field_code]["digest"] = self.send _digest_name
+ else
+ write_attribute attr, val
+ end
+ end
+
+ owner.send :define_singleton_method, "#{_attr_name}_will_change!" do
+ self.send "#{_digest_name}=", nil
+ custom_field_values_will_change!
+ end
+
+ owner.send :define_singleton_method, _digest_name do
+ val = instance_variable_get "@#{_digest_name}"
+ if val.nil? && (file = send(_uploader_name)).present?
+ val = CustomField::Instance::Attachment.digest(file)
+ instance_variable_set "@#{_digest_name}", val
+ end
+ val
+ end
+
+ _extension_whitelist = options["extension_whitelist"]
+
+ owner.send :define_singleton_method, "#{_uploader_name}_extension_whitelist" do
+ _extension_whitelist
+ end
+
+ unless owner.class.uploaders.has_key? _uploader_name.to_sym
+ owner.class.mount_uploader _uploader_name, CustomFieldAttachmentUploader, mount_on: "custom_field_#{code}_raw_value"
+ owner.class.send :attr_accessor, _digest_name
+ end
+
+ digest = @raw_value && @raw_value["digest"]
+ owner.send "#{_digest_name}=", digest
+ end
+
+ def self.digest file
+ Digest::SHA256.file(file.path).hexdigest
+ end
+
+ def preprocess_value_for_assignment val
+ if val.present?
+ owner.send "#{uploader_name}=", val
+ else
+ @raw_value
+ end
+ end
+
+ def checksum
+ owner.send digest_name
+ end
+
+ def value
+ owner.send "custom_field_#{code}"
+ end
+
+ def raw_value
+ @raw_value
+ end
+
+ def attr_name
+ "custom_field_#{code}_raw_value"
+ end
+
+ def uploader_name
+ "custom_field_#{code}"
+ end
+
+ def digest_name
+ "#{uploader_name}_digest"
+ end
+
+ def display_value
+ render_partial
+ end
+
+ class Input < Base::Input
+ def form_input_options
+ super.update({
+ as: :file,
+ wrapper: :horizontal_file_input
+ })
+ end
+ end
+ end
+
+ class String < Base
+ def value
+ "#{@raw_value}"
+ end
+ end
+ end
end
diff --git a/app/models/dashboard.rb b/app/models/dashboard.rb
index 46c621266..e0857dca3 100644
--- a/app/models/dashboard.rb
+++ b/app/models/dashboard.rb
@@ -27,4 +27,5 @@ class Dashboard
def current_organisation
context.send(:current_organisation)
end
+
end
diff --git a/app/models/export.rb b/app/models/export.rb
deleted file mode 100644
index 8c38d6684..000000000
--- a/app/models/export.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-class Export
- include JobConcern
-
- def initialize( response )
- @datas = response
- end
-
- def report?
- links["action_report"].present?
- end
-
- def report
- Rails.cache.fetch("#{cache_key}/action_report", expires_in: cache_expiration) do
- report_path = links["action_report"]
- if report_path
- response = Ievkit.get(report_path)
- ExportReport.new(response)
- else
- nil
- end
- end
- end
-
- def destroy
- delete_path = links["delete"]
- cancel_path = links["cancel"]
-
- if delete_path
- Ievkit.delete(delete_path)
- elsif cancel_path
- Ievkit.delete(cancel_path)
- else
- nil
- end
- end
-
- def file_path?
- links["data"].present?
- end
-
- def file_path
- links["data"]
- end
-
- def filename
- File.basename(file_path) if file_path
- end
-
- def filename_extension
- File.extname(filename).gsub(".", "") if filename
- end
-
-end
diff --git a/app/models/export/base.rb b/app/models/export/base.rb
new file mode 100644
index 000000000..c65539635
--- /dev/null
+++ b/app/models/export/base.rb
@@ -0,0 +1,135 @@
+require 'net/http/post/multipart'
+
+class Export::Base < ActiveRecord::Base
+ include Rails.application.routes.url_helpers
+
+ self.table_name = "exports"
+
+ belongs_to :referential
+
+ validates :type, :referential_id, presence: true
+
+ def self.messages_class_name
+ "Export::Message"
+ end
+
+ def self.resources_class_name
+ "Export::Resource"
+ end
+
+ def self.human_name
+ I18n.t("export.#{self.name.demodulize.underscore}")
+ end
+
+ def self.file_extension_whitelist
+ %w(zip csv json)
+ end
+
+ def upload_file file
+ url = URI.parse upload_workbench_export_url(self.workbench_id, self.id, host: Rails.application.config.rails_host)
+ res = nil
+ filename = File.basename(file.path)
+ content_type = MIME::Types.type_for(filename).first&.content_type
+ File.open(file.path) do |file_content|
+ req = Net::HTTP::Post::Multipart.new url.path,
+ file: UploadIO.new(file_content, content_type, filename),
+ token: self.token_upload
+ res = Net::HTTP.start(url.host, url.port) do |http|
+ http.request(req)
+ end
+ end
+ res
+ end
+
+ if Rails.env.development?
+ def self.force_load_descendants
+ path = Rails.root.join 'app/models/export'
+ Dir.chdir path do
+ Dir['**/*.rb'].each do |src|
+ next if src =~ /^base/
+ klass_name = "Export::#{src[0..-4].camelize}"
+ Rails.logger.info "Loading #{klass_name}"
+ begin
+ klass_name.constantize
+ rescue => e
+ Rails.logger.info "Failed: #{e.message}"
+ nil
+ end
+ end
+ end
+ end
+ end
+
+ def self.user_visible?
+ false
+ end
+
+ def self.inherited child
+ super child
+ child.instance_eval do
+ def self.user_visible?
+ true
+ end
+ end
+ end
+
+ def self.option name, opts={}
+ store_accessor :options, name
+
+ if opts[:serialize]
+ define_method name do
+ JSON.parse(options[name.to_s]) rescue opts[:serialize].new
+ end
+ end
+
+ if !!opts[:required]
+ validates name, presence: true
+ end
+ @options ||= {}
+ @options[name] = opts
+ end
+
+ def self.options
+ @options ||= {}
+ end
+
+ def self.options= options
+ @options = options
+ end
+
+ include IevInterfaces::Task
+
+ def self.model_name
+ ActiveModel::Name.new Export::Base, Export::Base, "Export"
+ end
+
+ def self.user_visible_descendants
+ descendants.select &:user_visible?
+ end
+
+ def self.user_visible?
+ true
+ end
+
+ def visible_options
+ options.select{|k, v| ! k.match /^_/}
+ end
+
+ def display_option_value option_name, context
+ option = self.class.options[option_name.to_sym]
+ val = self.options[option_name.to_s]
+ if option[:display]
+ context.instance_exec(val, &option[:display])
+ else
+ val
+ end
+ end
+
+ private
+
+ def initialize_fields
+ super
+ self.token_upload = SecureRandom.urlsafe_base64
+ end
+
+end
diff --git a/app/models/export/message.rb b/app/models/export/message.rb
new file mode 100644
index 000000000..223429900
--- /dev/null
+++ b/app/models/export/message.rb
@@ -0,0 +1,8 @@
+class Export::Message < ApplicationModel
+ self.table_name = :export_messages
+
+ include IevInterfaces::Message
+
+ belongs_to :export, class_name: Export::Base
+ belongs_to :resource, class_name: Export::Resource
+end
diff --git a/app/models/export/netex.rb b/app/models/export/netex.rb
new file mode 100644
index 000000000..069ec2209
--- /dev/null
+++ b/app/models/export/netex.rb
@@ -0,0 +1,22 @@
+class Export::Netex < Export::Base
+ after_commit :call_iev_callback, on: :create
+ option :export_type, collection: %w(line full), required: true
+ option :duration, type: :integer, default_value: 90, required: true
+ option :line_code
+
+ private
+
+ def iev_callback_url
+ URI("#{Rails.configuration.iev_url}/boiv_iev/referentials/exporter/new?id=#{id}")
+ end
+
+ # def self.user_visible?
+ # false
+ # end
+
+ def destroy_non_ready_referential
+ if referential && !referential.ready
+ referential.destroy
+ end
+ end
+end
diff --git a/app/models/export/resource.rb b/app/models/export/resource.rb
new file mode 100644
index 000000000..2a63c14a8
--- /dev/null
+++ b/app/models/export/resource.rb
@@ -0,0 +1,8 @@
+class Export::Resource < ApplicationModel
+ self.table_name = :export_resources
+
+ include IevInterfaces::Resource
+
+ belongs_to :export, class_name: Export::Base
+ has_many :messages, class_name: "ExportMessage", foreign_key: :resource_id
+end
diff --git a/app/models/export/simple_exporter/base.rb b/app/models/export/simple_exporter/base.rb
new file mode 100644
index 000000000..e77e23468
--- /dev/null
+++ b/app/models/export/simple_exporter/base.rb
@@ -0,0 +1,94 @@
+class Export::SimpleExporter::Base < Export::Base
+ after_commit :call_exporter_async, on: :create
+
+ def self.user_visible?
+ false
+ end
+
+ def self.inherited child
+ super child
+ child.options = @options
+ child.instance_eval do
+ def self.user_visible?
+ true
+ end
+ end
+ end
+
+ def call_exporter_async
+ SimpleExportWorker.perform_async(id)
+ end
+
+ def simple_exporter_configuration_name
+
+ end
+
+ def exporter
+ @exporter ||= begin
+ if options[:_exporter_id]
+ exporter = SimpleJsonExporter.find options[:_exporter_id]
+ else
+ exporter = SimpleJsonExporter.create configuration_name: simple_exporter_configuration_name
+ options[:_exporter_id] = exporter.id
+ end
+ exporter
+ end
+ end
+
+ def configure_exporter config
+ end
+
+ def call_exporter
+ tmp = Tempfile.new [simple_exporter_configuration_name.to_s, ".json"]
+ referential.switch
+ exporter.configure do |config|
+ configure_exporter config
+ end
+ exporter.filepath = tmp.path
+ exporter.export
+ set_status_from_exporter
+ convert_exporter_journal_to_messages
+ self.save!
+ upload_file tmp
+ end
+
+ def set_status_from_exporter
+ if exporter.status.to_s == "error"
+ self.status = :failed
+ elsif exporter.status.to_s == "success"
+ self.status = :successful
+ else
+ self.status = :warning
+ end
+ end
+
+ def convert_exporter_journal_to_messages
+ self.messages.destroy_all
+ exporter.journal.each do |journal_item|
+ journal_item.symbolize_keys!
+ vals = {}
+
+ if journal_item[:kind].to_s == "warning"
+ vals[:criticity] = :warning
+ elsif journal_item[:kind].to_s == "error"
+ vals[:criticity] = :error
+ else
+ vals[:criticity] = :info
+ if journal_item[:event].to_s == "success"
+ vals[:message_key] = :success
+ end
+ end
+ vals[:resource_attributes] = journal_item[:row]
+
+ if journal_item[:message].present?
+ vals[:message_key] = :full_text
+ vals[:message_attributes] = {
+ text: journal_item[:message]
+ }
+ end
+ vals[:message_attributes] ||= {}
+ vals[:message_attributes][:line] = journal_item[:line]
+ self.messages.build vals
+ end
+ end
+end
diff --git a/app/models/export/workgroup.rb b/app/models/export/workgroup.rb
new file mode 100644
index 000000000..3430596c7
--- /dev/null
+++ b/app/models/export/workgroup.rb
@@ -0,0 +1,9 @@
+class Export::Workgroup < Export::Base
+ after_commit :launch_worker, :on => :create
+
+ option :duration, required: true, type: :integer, default_value: 90
+
+ def launch_worker
+ WorkgroupExportWorker.perform_async(id)
+ end
+end
diff --git a/app/models/export_log_message.rb b/app/models/export_log_message.rb
deleted file mode 100644
index 4bb9d3cc7..000000000
--- a/app/models/export_log_message.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-class ExportLogMessage < ActiveRecord::Base
- belongs_to :export
-
- acts_as_list :scope => :export
-
- validates_presence_of :key
- validates_inclusion_of :severity, :in => %w{info warning error ok uncheck fatal}
-
- def arguments=(arguments)
- write_attribute :arguments, (arguments.to_json if arguments.present?)
- end
-
- def arguments
- @decoded_arguments ||=
- begin
- if (stored_arguments = raw_attributes).present?
- ActiveSupport::JSON.decode stored_arguments
- else
- {}
- end
- end
- end
-
- def raw_attributes
- read_attribute(:arguments)
- end
-
- before_validation :define_default_attributes, :on => :create
- def define_default_attributes
- self.severity ||= "info"
- end
-
- def full_message
- last_key=key.rpartition("|").last
- begin
- I18n.translate last_key, arguments.symbolize_keys.merge(:scope => "export_log_messages.messages").merge(:default => :undefined).merge(:key => last_key)
- rescue => e
- Rails.logger.error "missing arguments for message "+last_key
- I18n.translate "WRONG_DATA",{"0"=>last_key}.symbolize_keys.merge(:scope => "export_log_messages.messages").merge(:default => :undefined).merge(:key => "WRONG_DATA")
- end
- end
-end
diff --git a/app/models/export_report.rb b/app/models/export_report.rb
deleted file mode 100644
index 3c0788106..000000000
--- a/app/models/export_report.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-class ExportReport
- #include ReportConcern
-
- def initialize( response )
- @datas = response.action_report
- end
-
-end
diff --git a/app/models/export_service.rb b/app/models/export_service.rb
deleted file mode 100644
index 2dbe0d7b3..000000000
--- a/app/models/export_service.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-class ExportService
-
- attr_reader :referential
-
- def initialize(referential)
- @referential = referential
- end
-
- # Find an export whith his id
- def find(id)
- Export.new( Ievkit.scheduled_job(referential.slug, id, { :action => "exporter" }) )
- end
-
- # Find all exports
- def all
- [].tap do |jobs|
- Ievkit.jobs(referential.slug, { :action => "exporter" }).each do |job|
- jobs << Export.new( job )
- end
- end
- end
-
-end
diff --git a/app/models/export_task.rb b/app/models/export_task.rb
deleted file mode 100644
index f02cb914e..000000000
--- a/app/models/export_task.rb
+++ /dev/null
@@ -1,119 +0,0 @@
-class ExportTask
- extend Enumerize
- extend ActiveModel::Naming
- extend ActiveModel::Translation
- extend ActiveModel::Callbacks
- include ActiveModel::Validations
- include ActiveModel::Conversion
-
- attr_accessor :start_date, :end_date
-
- define_model_callbacks :initialize, only: :after
-
- enumerize :data_format, in: %w( neptune netex gtfs hub kml )
- attr_accessor :referential_id, :user_id, :user_name, :references_type, :data_format, :name, :projection_type, :reference_ids
-
- validates_presence_of :referential_id
- validates_presence_of :user_id
- validates_presence_of :user_name
- validates_presence_of :name
- validates_presence_of :data_format
-
- validate :period_validation
-
- after_initialize :init_period
-
- def initialize( params = {} )
- run_callbacks :initialize do
- params.each {|k,v| send("#{k}=",v)}
- end
- end
-
- def period_validation
- st_date = start_date.is_a?(String) ? Date.parse(start_date) : start_date
- ed_date = end_date.is_a?(String) ? Date.parse(end_date) : end_date
-
- unless Chouette::TimeTable.start_validity_period.nil? || st_date.nil?
- tt_st_date = Chouette::TimeTable.start_validity_period
- errors.add(:start_date, ExportTask.human_attribute_name("start_date_greater_than" , {:tt_st_date => tt_st_date})) unless tt_st_date <= st_date
- end
- unless st_date.nil? || ed_date.nil?
- errors.add(:end_date, ExportTask.human_attribute_name("end_date_greater_than_start_date")) unless st_date <= ed_date
- end
- unless ed_date.nil? || Chouette::TimeTable.end_validity_period.nil?
- tt_ed_date = Chouette::TimeTable.end_validity_period
- errors.add(:end_date, ExportTask.human_attribute_name("end_date_less_than", {:tt_ed_date => tt_ed_date})) unless ed_date <= tt_ed_date
- end
- end
-
- def init_period
- unless Chouette::TimeTable.start_validity_period.nil?
- if start_date.nil?
- self.start_date = Chouette::TimeTable.start_validity_period
- end
- if end_date.nil?
- self.end_date = Chouette::TimeTable.end_validity_period
- end
- end
- end
-
- def referential
- Referential.find(referential_id)
- end
-
- def organisation
- referential.organisation
- end
-
- def save
- if self.valid?
- # Call Iev Server
- begin
- Ievkit.create_job( referential.slug, "exporter", data_format, {
- :file1 => params_io,
- } )
- rescue Exception => exception
- raise exception
- end
- true
- else
- false
- end
- end
-
- def self.data_formats
- self.data_format.values
- end
-
- def self.references_types
- self.references_type.values
- end
-
- def params
- {}.tap do |h|
- h["parameters"] = action_params
- end
- end
-
- def action_params
- {}
- end
-
- def params_io
- file = StringIO.new( params.to_json )
- Faraday::UploadIO.new(file, "application/json", "parameters.json")
- end
-
- def self.optional_attributes(references_type)
- []
- end
-
- def optional_attributes
- self.class.optional_attributes(references_type.to_s)
- end
-
- def optional_attribute?(attribute)
- optional_attributes.include? attribute.to_sym
- end
-
-end
diff --git a/app/models/generic_attribute_control/min_max.rb b/app/models/generic_attribute_control/min_max.rb
index 18873b683..bab900f0e 100644
--- a/app/models/generic_attribute_control/min_max.rb
+++ b/app/models/generic_attribute_control/min_max.rb
@@ -1,9 +1,7 @@
module GenericAttributeControl
class MinMax < ComplianceControl
store_accessor :control_attributes, :minimum, :maximum, :target
-
- validates_numericality_of :minimum, allow_nil: true, greater_than_or_equal_to: 0
- validates_numericality_of :maximum, allow_nil: true, greater_than_or_equal_to: 0
+
validates :target, presence: true
include MinMaxValuesValidation
diff --git a/app/models/gtfs_export.rb b/app/models/gtfs_export.rb
deleted file mode 100644
index d0b9fc4f9..000000000
--- a/app/models/gtfs_export.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-class GtfsExport < ExportTask
-
- validates_presence_of :time_zone, unless: Proc.new { |e| e.optional_attribute? :time_zone }
- attr_accessor :object_id_prefix, :time_zone
-
- enumerize :references_type, in: %w( network line company group_of_line stop_area )
-
- after_initialize :init_params
-
- def init_params
- if time_zone.nil?
- self.time_zone = "Paris"
- end
- end
-
- def real_time_zone
- ActiveSupport::TimeZone.find_tzinfo(time_zone).name
- end
-
- def action_params
- {
- "gtfs-export" => {
- "name" => name,
- "references_type" => references_type,
- "reference_ids" => reference_ids,
- "user_name" => user_name,
- "organisation_name" => organisation.name,
- "referential_name" => referential.name,
- "time_zone" => real_time_zone,
- "object_id_prefix" => object_id_prefix,
- "start_date" => start_date,
- "end_date" => end_date
- }
- }
- end
-
- def self.optional_attributes(references_type)
- super.tap do |optional_attributes|
- optional_attributes.push :time_zone, :start_date, :end_date if references_type == "stop_area"
- end
- end
-
- def data_format
- "gtfs"
- end
-
-end
diff --git a/app/models/gtfs_import.rb b/app/models/gtfs_import.rb
deleted file mode 100644
index 1d7b5c6f5..000000000
--- a/app/models/gtfs_import.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-class GtfsImport < ImportTask
-
- enumerize :references_type, in: %w( stop_area )
-
- attr_accessor :object_id_prefix, :max_distance_for_commercial, :ignore_last_word, :ignore_end_chars, :max_distance_for_connection_link, :references_type
-
- validates_presence_of :object_id_prefix
-
- def references_types
- self.references_type.values
- end
-
- def action_params
- {
- "gtfs-import" => {
- "no_save" => no_save,
- "user_name" => user_name,
- "name" => name,
- "organisation_name" => organisation.name,
- "referential_name" => referential.name,
- "object_id_prefix" => object_id_prefix,
- "max_distance_for_commercial" => max_distance_for_commercial,
- "ignore_last_word" => ignore_last_word,
- "ignore_end_chars" => ignore_end_chars,
- "max_distance_for_connection_link" => max_distance_for_connection_link,
- "references_type" => references_type
- }
- }
- end
-
-
- def data_format
- "gtfs"
- end
-
-end
diff --git a/app/models/hub_export.rb b/app/models/hub_export.rb
deleted file mode 100644
index 802600692..000000000
--- a/app/models/hub_export.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-class HubExport < ExportTask
-
- enumerize :references_type, in: %w( network line company group_of_line )
-
- def action_params
- {
- "hub-export" => {
- "name" => name,
- "references_type" => references_type,
- "reference_ids" => reference_ids,
- "user_name" => user_name,
- "organisation_name" => organisation.name,
- "referential_name" => referential.name,
- "start_date" => start_date,
- "end_date" => end_date
- }
- }
- end
-
- def data_format
- "hub"
- end
-
-end
diff --git a/app/models/import.rb b/app/models/import.rb
deleted file mode 100644
index 29aadcd56..000000000
--- a/app/models/import.rb
+++ /dev/null
@@ -1,103 +0,0 @@
-class Import < ActiveRecord::Base
- mount_uploader :file, ImportUploader
- belongs_to :workbench
- belongs_to :referential
-
- belongs_to :parent, polymorphic: true
-
- has_many :messages, class_name: "ImportMessage", dependent: :destroy
- has_many :resources, class_name: "ImportResource", dependent: :destroy
- has_many :children, foreign_key: :parent_id, class_name: "Import", dependent: :destroy
-
- scope :where_started_at_in, ->(period_range) do
- where('started_at BETWEEN :begin AND :end', begin: period_range.begin, end: period_range.end)
- end
-
- scope :blocked, -> { where('created_at < ? AND status = ?', 4.hours.ago, 'running') }
-
- extend Enumerize
- enumerize :status, in: %w(new pending successful warning failed running aborted canceled), scope: true, default: :new
-
- validates :name, presence: true
- validates :file, presence: true
- validates_presence_of :workbench, :creator
-
- before_create :initialize_fields
-
- def self.model_name
- ActiveModel::Name.new Import, Import, "Import"
- end
-
- def children_succeedeed
- children.with_status(:successful, :warning).count
- end
-
- def self.launched_statuses
- %w(new pending)
- end
-
- def self.failed_statuses
- %w(failed aborted canceled)
- end
-
- def self.finished_statuses
- %w(successful failed warning aborted canceled)
- end
-
- def self.abort_old
- where(
- 'created_at < ? AND status NOT IN (?)',
- 4.hours.ago,
- finished_statuses
- ).update_all(status: 'aborted')
- end
-
- def notify_parent
- parent.child_change
- update(notified_parent_at: DateTime.now)
- end
-
- def child_change
- return if self.class.finished_statuses.include?(status)
-
- update_status
- update_referentials
- end
-
- def update_status
- status =
- if children.where(status: self.class.failed_statuses).count > 0
- 'failed'
- elsif children.where(status: "warning").count > 0
- 'warning'
- elsif children.where(status: "successful").count == children.count
- 'successful'
- end
-
- attributes = {
- current_step: children.count,
- status: status
- }
-
- if self.class.finished_statuses.include?(status)
- attributes[:ended_at] = Time.now
- end
-
- update attributes
- end
-
- def update_referentials
- return unless self.class.finished_statuses.include?(status)
-
- children.each do |import|
- import.referential.update(ready: true) if import.referential
- end
- end
-
- private
-
- def initialize_fields
- self.token_download = SecureRandom.urlsafe_base64
- end
-
-end
diff --git a/app/models/import/base.rb b/app/models/import/base.rb
new file mode 100644
index 000000000..f98e359d4
--- /dev/null
+++ b/app/models/import/base.rb
@@ -0,0 +1,47 @@
+class Import::Base < ApplicationModel
+ self.table_name = "imports"
+ validates :file, presence: true
+
+ def self.messages_class_name
+ "Import::Message"
+ end
+
+ def self.resources_class_name
+ "Import::Resource"
+ end
+
+ def self.file_extension_whitelist
+ %w(zip)
+ end
+
+ include IevInterfaces::Task
+
+ def self.model_name
+ ActiveModel::Name.new Import::Base, Import::Base, "Import"
+ end
+
+ def child_change
+ Rails.logger.info "child_change for #{inspect}"
+ return if self.class.finished_statuses.include?(status)
+
+ super
+ update_referentials
+ end
+
+ def update_referentials
+ Rails.logger.info "update_referentials for #{inspect}"
+ return unless self.class.finished_statuses.include?(status)
+
+ children.each do |import|
+ import.referential.update(ready: true) if import.referential
+ end
+ end
+
+ private
+
+ def initialize_fields
+ super
+ self.token_download ||= SecureRandom.urlsafe_base64
+ end
+
+end
diff --git a/app/models/import/gtfs.rb b/app/models/import/gtfs.rb
new file mode 100644
index 000000000..a20c468c1
--- /dev/null
+++ b/app/models/import/gtfs.rb
@@ -0,0 +1,309 @@
+class Import::Gtfs < Import::Base
+ after_commit :launch_worker, :on => :create
+
+ def launch_worker
+ GtfsImportWorker.perform_async id
+ end
+
+ def import
+ update status: 'running', started_at: Time.now
+
+ import_without_status
+ update status: 'successful', ended_at: Time.now
+ rescue Exception => e
+ update status: 'failed', ended_at: Time.now
+ Rails.logger.error "Error in GTFS import: #{e} #{e.backtrace.join('\n')}"
+ ensure
+ notify_parent
+ referential&.update ready: true
+ end
+
+ def self.accept_file?(file)
+ Zip::File.open(file) do |zip_file|
+ zip_file.glob('agency.txt').size == 1
+ end
+ rescue Exception => e
+ Rails.logger.debug "Error in testing GTFS file: #{e}"
+ return false
+ end
+
+ def create_referential
+ self.referential ||= Referential.create!(
+ name: "GTFS Import",
+ organisation_id: workbench.organisation_id,
+ workbench_id: workbench.id,
+ metadatas: [referential_metadata]
+ )
+ end
+
+ def referential_metadata
+ registration_numbers = source.routes.map(&:id)
+ line_ids = line_referential.lines.where(registration_number: registration_numbers).pluck(:id)
+
+ start_dates, end_dates = source.calendars.map { |c| [c.start_date, c.end_date ] }.transpose
+ excluded_dates = source.calendar_dates.select { |d| d.exception_type == "2" }.map(&:date)
+
+ min_date = Date.parse (start_dates + [excluded_dates.min]).compact.min
+ max_date = Date.parse (end_dates + [excluded_dates.max]).compact.max
+
+ ReferentialMetadata.new line_ids: line_ids, periodes: [min_date..max_date]
+ end
+
+ attr_accessor :local_file
+ def local_file
+ @local_file ||= download_local_file
+ end
+
+ attr_accessor :download_host
+ def download_host
+ @download_host ||= Rails.application.config.rails_host
+ end
+
+ def local_temp_directory
+ @local_temp_directory ||=
+ begin
+ directory = Rails.application.config.try(:import_temporary_directory) || Rails.root.join('tmp', 'imports')
+ FileUtils.mkdir_p directory
+ directory
+ end
+ end
+
+ def local_temp_file(&block)
+ Tempfile.open("chouette-import", local_temp_directory) do |file|
+ file.binmode
+ yield file
+ end
+ end
+
+ def download_path
+ Rails.application.routes.url_helpers.download_workbench_import_path(workbench, id, token: token_download)
+ end
+
+ def download_uri
+ @download_uri ||=
+ begin
+ host = download_host
+ host = "http://#{host}" unless host =~ %r{https?://}
+ URI.join(host, download_path)
+ end
+ end
+
+ def download_local_file
+ local_temp_file do |file|
+ begin
+ Net::HTTP.start(download_uri.host, download_uri.port) do |http|
+ http.request_get(download_uri.request_uri) do |response|
+ response.read_body do |segment|
+ file.write segment
+ end
+ end
+ end
+ ensure
+ file.close
+ end
+
+ file.path
+ end
+ end
+
+ def source
+ @source ||= ::GTFS::Source.build local_file
+ end
+
+ delegate :line_referential, :stop_area_referential, to: :workbench
+
+ def prepare_referential
+ import_agencies
+ import_stops
+ import_routes
+
+ create_referential
+ referential.switch
+ end
+
+ def import_without_status
+ prepare_referential
+
+ import_calendars
+ import_trips
+ import_stop_times
+ end
+
+ def import_agencies
+ Chouette::Company.transaction do
+ source.agencies.each do |agency|
+ company = line_referential.companies.find_or_initialize_by(registration_number: agency.id)
+ company.attributes = { name: agency.name }
+
+ save_model company
+ end
+ end
+ end
+
+ def import_stops
+ Chouette::StopArea.transaction do
+ source.stops.each do |stop|
+ stop_area = stop_area_referential.stop_areas.find_or_initialize_by(registration_number: stop.id)
+
+ stop_area.name = stop.name
+ stop_area.area_type = stop.location_type == "1" ? "zdlp" : "zdep"
+ stop_area.parent = stop_area_referential.stop_areas.find_by!(registration_number: stop.parent_station) if stop.parent_station.present?
+ stop_area.latitude, stop_area.longitude = stop.lat, stop.lon
+ stop_area.kind = "commercial"
+
+ # TODO correct default timezone
+
+ save_model stop_area
+ end
+ end
+ end
+
+ def import_routes
+ Chouette::Line.transaction do
+ source.routes.each do |route|
+ line = line_referential.lines.find_or_initialize_by(registration_number: route.id)
+ line.name = route.long_name.presence || route.short_name
+ line.number = route.short_name
+ line.published_name = route.long_name
+
+ line.company = line_referential.companies.find_by(registration_number: route.agency_id) if route.agency_id.present?
+
+ # TODO transport mode
+
+ line.comment = route.desc
+
+ # TODO colors
+
+ line.url = route.url
+
+ save_model line
+ end
+ end
+ end
+
+ def vehicle_journey_by_trip_id
+ @vehicle_journey_by_trip_id ||= {}
+ end
+
+ def import_trips
+ source.trips.each_slice(100) do |slice|
+ slice.each do |trip|
+ Chouette::Route.transaction do
+ line = line_referential.lines.find_by registration_number: trip.route_id
+
+ route = referential.routes.build line: line
+ route.wayback = (trip.direction_id == "0" ? :outbound : :inbound)
+ # TODO better name ?
+ name = route.published_name = trip.short_name.presence || trip.headsign.presence || route.wayback.to_s.capitalize
+ route.name = name
+ save_model route
+
+ journey_pattern = route.journey_patterns.build name: name
+ save_model journey_pattern
+
+ vehicle_journey = journey_pattern.vehicle_journeys.build route: route
+ vehicle_journey.published_journey_name = trip.headsign.presence || trip.id
+ save_model vehicle_journey
+
+ time_table = referential.time_tables.find_by(id: time_tables_by_service_id[trip.service_id]) if time_tables_by_service_id[trip.service_id]
+ if time_table
+ vehicle_journey.time_tables << time_table
+ else
+ messages.create! criticity: "warning", message_key: "gtfs.trips.unkown_service_id", message_attributes: {service_id: trip.service_id}
+ end
+
+ vehicle_journey_by_trip_id[trip.id] = vehicle_journey.id
+ end
+ end
+ end
+ end
+
+ def import_stop_times
+ source.stop_times.group_by(&:trip_id).each_slice(50) do |slice|
+ slice.each do |trip_id, stop_times|
+ Chouette::VehicleJourneyAtStop.transaction do
+ vehicle_journey = referential.vehicle_journeys.find vehicle_journey_by_trip_id[trip_id]
+ journey_pattern = vehicle_journey.journey_pattern
+ route = journey_pattern.route
+
+ stop_times.sort_by! { |s| s.stop_sequence.to_i }
+
+ stop_times.each do |stop_time|
+ stop_area = stop_area_referential.stop_areas.find_by(registration_number: stop_time.stop_id)
+
+ stop_point = route.stop_points.build stop_area: stop_area
+ save_model stop_point
+
+ journey_pattern.stop_points << stop_point
+
+ # JourneyPattern#vjas_add creates automaticaly VehicleJourneyAtStop
+ vehicle_journey_at_stop = journey_pattern.vehicle_journey_at_stops.find_by(stop_point_id: stop_point.id)
+
+ departure_time = GTFS::Time.parse(stop_time.departure_time)
+ arrival_time = GTFS::Time.parse(stop_time.arrival_time)
+
+ vehicle_journey_at_stop.departure_time = departure_time.time
+ vehicle_journey_at_stop.arrival_time = arrival_time.time
+ vehicle_journey_at_stop.departure_day_offset = departure_time.day_offset
+ vehicle_journey_at_stop.arrival_day_offset = arrival_time.day_offset
+
+ # TODO offset
+
+ save_model vehicle_journey_at_stop
+ end
+ end
+ end
+ end
+ end
+
+ def time_tables_by_service_id
+ @time_tables_by_service_id ||= {}
+ end
+
+ def import_calendars
+ source.calendars.each_slice(500) do |slice|
+ Chouette::TimeTable.transaction do
+ slice.each do |calendar|
+ time_table = referential.time_tables.build comment: "Calendar #{calendar.service_id}"
+ Chouette::TimeTable.all_days.each do |day|
+ time_table.send("#{day}=", calendar.send(day))
+ end
+ time_table.periods.build period_start: calendar.start_date, period_end: calendar.end_date
+
+ save_model time_table
+
+ time_tables_by_service_id[calendar.service_id] = time_table.id
+ end
+ end
+ end
+ end
+
+ def import_calendar_dates
+ source.calendar_dates.each_slice(500) do |slice|
+ Chouette::TimeTable.transaction do
+ slice.each do |calendar_date|
+ time_table = referential.time_tables.find time_tables_by_service_id[calendar_date.service_id]
+ date = time_table.dates.build date: Date.parse(calendar_date.date), in_out: calendar_date.exception_type == "1"
+
+ save_model date
+ end
+ end
+ end
+ end
+
+ def save_model(model)
+ unless model.save
+ Rails.logger.info "Can't save #{model.class.name} : #{model.errors.inspect}"
+ raise ActiveRecord::RecordNotSaved.new("Invalid #{model.class.name} : #{model.errors.inspect}")
+ end
+ Rails.logger.debug "Created #{model.inspect}"
+ end
+
+ def notify_parent
+ return unless parent.present?
+ return if notified_parent_at
+ parent.child_change
+ update_column :notified_parent_at, Time.now
+ end
+
+end
diff --git a/app/models/import/message.rb b/app/models/import/message.rb
new file mode 100644
index 000000000..30b76ec5c
--- /dev/null
+++ b/app/models/import/message.rb
@@ -0,0 +1,8 @@
+class Import::Message < ApplicationModel
+ self.table_name = :import_messages
+
+ include IevInterfaces::Message
+
+ belongs_to :import, class_name: Import::Base
+ belongs_to :resource, class_name: Import::Resource
+end
diff --git a/app/models/import_message_export.rb b/app/models/import/message_export.rb
index 05f8a2cc7..7d03783ed 100644
--- a/app/models/import_message_export.rb
+++ b/app/models/import/message_export.rb
@@ -2,7 +2,7 @@
require "csv"
require "zip"
-class ImportMessageExport
+class Import::MessageExport
include ActiveModel::Validations
include ActiveModel::Conversion
extend ActiveModel::Naming
@@ -22,16 +22,18 @@ class ImportMessageExport
end
def column_names
- ["criticity", "message key", "message", "file name", "line", "column"]
+ ["criticity", "message_key", "message", "filename", "line", "column"].map {|c| Import::Message.tmf(c)}
end
def to_csv(options = {})
- CSV.generate(options) do |csv|
+ csv_string = CSV.generate(options) do |csv|
csv << column_names
import_messages.each do |import_message|
- csv << [import_message.criticity, import_message.message_key, I18n.t("import_messages.#{import_message.message_key}", import_message.message_attributes.deep_symbolize_keys), *import_message.resource_attributes.values_at("filename", "line_number", "column_number") ]
+ csv << [import_message.criticity, import_message.message_attributes['test_id'], I18n.t("import_messages.#{import_message.message_key}", import_message.message_attributes.deep_symbolize_keys), *import_message.resource_attributes.values_at("filename", "line_number", "column_number") ]
end
end
+ # We add a BOM to indicate we use UTF-8
+ "\uFEFF" + csv_string
end
def to_zip(temp_file,options = {})
diff --git a/app/models/import/netex.rb b/app/models/import/netex.rb
new file mode 100644
index 000000000..f19fde435
--- /dev/null
+++ b/app/models/import/netex.rb
@@ -0,0 +1,63 @@
+require 'net/http'
+class Import::Netex < Import::Base
+ before_destroy :destroy_non_ready_referential
+
+ after_commit :call_iev_callback, on: :create
+
+ before_save def abort_unless_referential
+ self.status = 'aborted' unless referential
+ end
+
+ validates_presence_of :parent
+
+ def create_with_referential!
+ self.referential =
+ Referential.new(
+ name: self.name,
+ organisation_id: workbench.organisation_id,
+ workbench_id: workbench.id,
+ metadatas: [referential_metadata]
+ )
+ self.referential.save
+ if self.referential.invalid?
+ Rails.logger.info "Can't create referential for import #{self.id}: #{referential.inspect} #{referential.metadatas.inspect} #{referential.errors.messages}"
+ if referential.metadatas.all?{|m| m.line_ids.present? && m.line_ids.empty?}
+ parent.messages.create criticity: :error, message_key: "referential_creation_missing_lines", message_attributes: {referential_name: referential.name}
+ else
+ parent.messages.create criticity: :error, message_key: "referential_creation", message_attributes: {referential_name: referential.name}
+ end
+ else
+ save!
+ end
+ end
+
+ private
+
+ def iev_callback_url
+ URI("#{Rails.configuration.iev_url}/boiv_iev/referentials/importer/new?id=#{id}")
+ end
+
+ def destroy_non_ready_referential
+ if referential && !referential.ready
+ referential.destroy
+ end
+ end
+
+ def referential_metadata
+ metadata = ReferentialMetadata.new
+
+ if self.file && self.file.path
+ netex_file = STIF::NetexFile.new(self.file.path)
+ frame = netex_file.frames.first
+
+ if frame
+ metadata.periodes = frame.periods
+
+ line_objectids = frame.line_refs.map { |ref| "STIF:CODIFLIGNE:Line:#{ref}" }
+ metadata.line_ids = workbench.lines.where(objectid: line_objectids).pluck(:id)
+ end
+ end
+
+ metadata
+ end
+end
diff --git a/app/models/import/resource.rb b/app/models/import/resource.rb
new file mode 100644
index 000000000..1951daacd
--- /dev/null
+++ b/app/models/import/resource.rb
@@ -0,0 +1,8 @@
+class Import::Resource < ApplicationModel
+ self.table_name = :import_resources
+
+ include IevInterfaces::Resource
+
+ belongs_to :import, class_name: Import::Base
+ has_many :messages, class_name: "Import::Message", foreign_key: :resource_id
+end
diff --git a/app/models/import/workbench.rb b/app/models/import/workbench.rb
new file mode 100644
index 000000000..124b9b0d8
--- /dev/null
+++ b/app/models/import/workbench.rb
@@ -0,0 +1,26 @@
+class Import::Workbench < Import::Base
+ after_commit :launch_worker, :on => :create
+
+ def launch_worker
+ unless Import::Gtfs.accept_file?(file.path)
+ WorkbenchImportWorker.perform_async(id)
+ else
+ import_gtfs
+ end
+ end
+
+ def import_gtfs
+ update_column :status, 'running'
+ update_column :started_at, Time.now
+
+ Import::Gtfs.create! parent_id: self.id, workbench: workbench, file: File.new(file.path), name: "Import GTFS", creator: "Web service"
+
+ update_column :status, 'successful'
+ update_column :ended_at, Time.now
+ rescue Exception => e
+ Rails.logger.error "Error while processing GTFS file: #{e}"
+
+ update_column :status, 'failed'
+ update_column :ended_at, Time.now
+ end
+end
diff --git a/app/models/import_message.rb b/app/models/import_message.rb
deleted file mode 100644
index de70c35d1..000000000
--- a/app/models/import_message.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-class ImportMessage < ActiveRecord::Base
- extend Enumerize
- belongs_to :import
- belongs_to :resource, class_name: ImportResource
- enumerize :criticity, in: %i(info warning error)
-
- validates :criticity, presence: true
-end
diff --git a/app/models/import_report.rb b/app/models/import_report.rb
deleted file mode 100644
index ba13f0118..000000000
--- a/app/models/import_report.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-class ImportReport
- #include ReportConcern
-
- def initialize( response )
- @datas = response.action_report
- end
-
-end
diff --git a/app/models/import_resource.rb b/app/models/import_resource.rb
deleted file mode 100644
index 55e752e74..000000000
--- a/app/models/import_resource.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-class ImportResource < ActiveRecord::Base
- belongs_to :import
-
- extend Enumerize
- enumerize :status, in: %i(OK ERROR WARNING IGNORED), scope: true
-
- validates_presence_of :name, :resource_type, :reference
-
- has_many :messages, class_name: "ImportMessage", foreign_key: :resource_id
-
-end
diff --git a/app/models/import_service.rb b/app/models/import_service.rb
deleted file mode 100644
index 2e3c1012b..000000000
--- a/app/models/import_service.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-class ImportService
-
- attr_reader :referential
-
- def initialize( referential )
- @referential = referential
- end
-
- # Find an import whith his id
- def find(id)
- Import.new( Ievkit.scheduled_job(referential.slug, id, { :action => "importer" }) )
- end
-
- # Find all imports
- def all
- [].tap do |jobs|
- Ievkit.jobs(referential.slug, { :action => "importer" }).each do |job|
- jobs << Import.new( job )
- end
- end
- end
-
-end
diff --git a/app/models/import_task.rb b/app/models/import_task.rb
deleted file mode 100644
index 7dfa2c644..000000000
--- a/app/models/import_task.rb
+++ /dev/null
@@ -1,141 +0,0 @@
-require "zip"
-
-class ImportTask
- extend Enumerize
- extend ActiveModel::Naming
- extend ActiveModel::Translation
- include ActiveModel::Validations
- include ActiveModel::Conversion
-
- # TODO : Move in configuration
- @@root = "#{Rails.root}/tmp/imports"
- cattr_accessor :root
-
- enumerize :data_format, in: %w( neptune netex gtfs )
- attr_accessor :referential_id, :user_id, :user_name, :data_format, :resources, :name, :no_save
-
- validates_presence_of :referential_id
- validates_presence_of :resources
- validates_presence_of :user_id
- validates_presence_of :user_name
- validates_presence_of :name
-
- validate :validate_file_size, :validate_file_content
-
- def initialize( params = {} )
- params.each {|k,v| send("#{k}=",v)}
- end
-
- def referential
- Referential.find(referential_id)
- end
-
- def organisation
- referential.organisation
- end
-
- def save
- if valid?
- # Save resources
- save_resources
-
- # Call Iev Server
- begin
- Ievkit.create_job(referential.slug, "importer", data_format, {
- :file1 => params_io,
- :file2 => transport_data_io
- }
-
- )
-
- # Delete resources
- delete_resources
- rescue Exception => exception
- # If iev server has an error must delete resources before
- delete_resources
-
- raise exception
- end
- true
- else
- false
- end
- end
-
- def params
- {}.tap do |h|
- h["parameters"] = {}
- end
- end
-
- def self.data_formats
- self.data_format.values
- end
-
- def params_io
- file = StringIO.new( params.to_json )
- Faraday::UploadIO.new(file, "application/json", "parameters.json")
- end
-
- def transport_data_io
- file = File.new(saved_resources_path, "r")
- if file_extname == ".zip"
- Faraday::UploadIO.new(file, "application/zip", original_filename )
- elsif file_extname == ".xml"
- Faraday::UploadIO.new(file, "application/xml", original_filename )
- end
- end
-
- def save_resources
- FileUtils.mkdir_p root
- FileUtils.cp resources.path, saved_resources_path
- end
-
- def delete_resources
- FileUtils.rm saved_resources_path if File.exists? saved_resources_path
- end
-
- def original_filename
- resources.original_filename
- end
-
- def file_extname
- File.extname(original_filename) if original_filename
- end
-
- def saved_resources_path
- @saved_resources_path ||= "#{root}/#{Time.now.to_i}#{file_extname}"
- end
-
- @@maximum_file_size = 80.megabytes
- cattr_accessor :maximum_file_size
-
- def validate_file_size
- return unless resources.present? and resources.path.present? and File.exists? resources.path
-
- if File.size(resources.path) > maximum_file_size
- message = I18n.t("activemodel.errors.models.import_task.attributes.resources.maximum_file_size", file_size: ActionController::Base.helpers.number_to_human_size(File.size(resources.path)), maximum_file_size: ActionController::Base.helpers.number_to_human_size(maximum_file_size))
- errors.add(:resources, message)
- end
- end
-
- @@valid_mime_types = {
- neptune: %w{application/zip application/xml},
- netex: %w{application/zip},
- gtfs: %w{application/zip text/plain}
- }
- cattr_accessor :valid_mime_types
-
- def validate_file_content
- return unless resources.present? and resources.path.present? and File.exists? resources.path
-
- mime_type = (File.open(resources.path) { |f| MimeMagic.by_magic f }).try :type
- expected_mime_types = valid_mime_types[data_format.to_sym]
-
- unless expected_mime_types.include? mime_type
- message = I18n.t("activemodel.errors.models.import_task.attributes.resources.invalid_mime_type", mime_type: mime_type)
- errors.add(:resources, message)
- end
- end
-
-end
diff --git a/app/models/kml_export.rb b/app/models/kml_export.rb
deleted file mode 100644
index f6db77172..000000000
--- a/app/models/kml_export.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-class KmlExport < ExportTask
-
- enumerize :references_type, in: %w( network line company group_of_line )
-
- def action_params
- {
- "kml-export" => {
- "name" => name,
- "references_type" => references_type,
- "reference_ids" => reference_ids,
- "user_name" => user_name,
- "organisation_name" => organisation.name,
- "referential_name" => referential.name,
- "start_date" => start_date,
- "end_date" => end_date
- }
- }
- end
-
- def data_format
- "kml"
- end
-
-end
diff --git a/app/models/line_control/lines_scope.rb b/app/models/line_control/lines_scope.rb
new file mode 100644
index 000000000..4210a10dd
--- /dev/null
+++ b/app/models/line_control/lines_scope.rb
@@ -0,0 +1,8 @@
+module LineControl
+ class LinesScope < ComplianceControl
+
+ def self.default_code; "3-Line-2" end
+
+ def prerequisite; I18n.t("compliance_controls.#{self.class.name.underscore}.prerequisite") end
+ end
+end
diff --git a/app/models/line_referential.rb b/app/models/line_referential.rb
index 15b2f6276..08193c960 100644
--- a/app/models/line_referential.rb
+++ b/app/models/line_referential.rb
@@ -1,7 +1,7 @@
-class LineReferential < ActiveRecord::Base
+class LineReferential < ApplicationModel
include ObjectidFormatterSupport
extend StifTransportModeEnumerations
-
+
has_many :line_referential_memberships
has_many :organisations, through: :line_referential_memberships
has_many :lines, class_name: 'Chouette::Line'
@@ -10,10 +10,11 @@ class LineReferential < ActiveRecord::Base
has_many :networks, class_name: 'Chouette::Network'
has_many :line_referential_syncs, -> { order created_at: :desc }
has_many :workbenches
+ has_one :workgroup
def add_member(organisation, options = {})
attributes = options.merge organisation: organisation
- line_referential_memberships.build attributes
+ line_referential_memberships.build attributes unless organisations.include?(organisation)
end
validates :name, presence: true
diff --git a/app/models/line_referential_membership.rb b/app/models/line_referential_membership.rb
index b49d1b5b1..8371bdc32 100644
--- a/app/models/line_referential_membership.rb
+++ b/app/models/line_referential_membership.rb
@@ -1,4 +1,6 @@
-class LineReferentialMembership < ActiveRecord::Base
+class LineReferentialMembership < ApplicationModel
belongs_to :organisation
belongs_to :line_referential
+
+ validates :organisation_id, presence: true, uniqueness: { scope: :line_referential }
end
diff --git a/app/models/line_referential_sync.rb b/app/models/line_referential_sync.rb
index 75c1e48a2..39e3846f0 100644
--- a/app/models/line_referential_sync.rb
+++ b/app/models/line_referential_sync.rb
@@ -1,4 +1,4 @@
-class LineReferentialSync < ActiveRecord::Base
+class LineReferentialSync < ApplicationModel
include AASM
belongs_to :line_referential
has_many :line_referential_sync_messages, :dependent => :destroy
diff --git a/app/models/line_referential_sync_message.rb b/app/models/line_referential_sync_message.rb
index 3b6cf3367..00a2b58a3 100644
--- a/app/models/line_referential_sync_message.rb
+++ b/app/models/line_referential_sync_message.rb
@@ -1,4 +1,4 @@
-class LineReferentialSyncMessage < ActiveRecord::Base
+class LineReferentialSyncMessage < ApplicationModel
belongs_to :line_referential_sync
enum criticity: [:info, :warning, :error]
diff --git a/app/models/merge.rb b/app/models/merge.rb
index 62bf581d6..7df7aa590 100644
--- a/app/models/merge.rb
+++ b/app/models/merge.rb
@@ -1,4 +1,4 @@
-class Merge < ActiveRecord::Base
+class Merge < ApplicationModel
extend Enumerize
belongs_to :workbench
@@ -50,7 +50,7 @@ class Merge < ActiveRecord::Base
new =
if workbench.output.current
Rails.logger.debug "Clone current output"
- Referential.new_from(workbench.output.current, fixme_functional_scope).tap do |clone|
+ Referential.new_from(workbench.output.current, workbench.organisation).tap do |clone|
clone.inline_clone = true
end
else
@@ -135,10 +135,18 @@ class Merge < ActiveRecord::Base
referential_stop_points_by_route = referential_stop_points.group_by(&:route_id)
+ referential_routes_constraint_zones = referential.switch do
+ referential.routing_constraint_zones.each_with_object(Hash.new { |h,k| h[k] = [] }) do |routing_constraint_zone, hash|
+ hash[routing_constraint_zone.route_id] << routing_constraint_zone
+ end
+ end
+
new.switch do
referential_routes.each do |route|
existing_route = new.routes.find_by line_id: route.line_id, checksum: route.checksum
- unless existing_route
+ if existing_route
+ existing_route.merge_metadata_from route
+ else
objectid = Chouette::Route.where(objectid: route.objectid).exists? ? nil : route.objectid
attributes = route.attributes.merge(
id: nil,
@@ -152,21 +160,50 @@ class Merge < ActiveRecord::Base
route_stop_points = referential_stop_points_by_route[route.id]
# Stop Points
- route_stop_points.each do |stop_point|
+ route_stop_points.sort_by(&:position).each do |stop_point|
objectid = Chouette::StopPoint.where(objectid: stop_point.objectid).exists? ? nil : stop_point.objectid
attributes = stop_point.attributes.merge(
id: nil,
route_id: nil,
objectid: objectid,
)
-
new_route.stop_points.build attributes
end
+ # We need to create StopPoints to known new primary keys
+ new_route.save!
+
+ old_stop_point_ids = route_stop_points.sort_by(&:position).map(&:id)
+ new_stop_point_ids = new_route.stop_points.sort_by(&:position).map(&:id)
+
+ stop_point_ids_mapping = Hash[[old_stop_point_ids, new_stop_point_ids].transpose]
+
+ # RoutingConstraintZones
+ routes_constraint_zones = referential_routes_constraint_zones[route.id]
+
+ routes_constraint_zones.each do |routing_constraint_zone|
+ objectid = new.routing_constraint_zones.where(objectid: routing_constraint_zone.objectid).exists? ? nil : routing_constraint_zone.objectid
+ stop_point_ids = routing_constraint_zone.stop_point_ids.map { |id| stop_point_ids_mapping[id] }.compact
+
+ if stop_point_ids.size != routing_constraint_zone.stop_point_ids.size
+ raise "Can't find all required StopPoints for RoutingConstraintZone #{routing_constraint_zone.inspect}"
+ end
+
+ attributes = routing_constraint_zone.attributes.merge(
+ id: nil,
+ route_id: nil,
+ objectid: objectid,
+ stop_point_ids: stop_point_ids,
+ )
+ new_route.routing_constraint_zones.build attributes
+
+ # No checksum check. RoutingConstraintZones are always recreated
+ end
+
new_route.save!
if new_route.checksum != route.checksum
- raise "Checksum has changed: #{route.inspect} #{new_route.inspect}"
+ raise "Checksum has changed: \"#{route.checksum}\", \"#{route.checksum_source}\" -> \"#{new_route.checksum}\", \"#{new_route.checksum_source}\""
end
end
end
@@ -197,7 +234,9 @@ class Merge < ActiveRecord::Base
existing_journey_pattern = new.journey_patterns.find_by route_id: existing_associated_route.id, checksum: journey_pattern.checksum
- unless existing_journey_pattern
+ if existing_journey_pattern
+ existing_journey_pattern.merge_metadata_from journey_pattern
+ else
objectid = Chouette::JourneyPattern.where(objectid: journey_pattern.objectid).exists? ? nil : journey_pattern.objectid
attributes = journey_pattern.attributes.merge(
id: nil,
@@ -221,7 +260,7 @@ class Merge < ActiveRecord::Base
new_journey_pattern = new.journey_patterns.create! attributes
if new_journey_pattern.checksum != journey_pattern.checksum
- raise "Checksum has changed for #{journey_pattern.inspect}: #{journey_pattern.checksum_source} #{new_journey_pattern.checksum_source} "
+ raise "Checksum has changed for #{journey_pattern.inspect} (to #{new_journey_pattern.inspect}): \"#{journey_pattern.checksum_source}\" -> \"#{new_journey_pattern.checksum_source}\""
end
end
end
@@ -233,16 +272,37 @@ class Merge < ActiveRecord::Base
referential.vehicle_journeys.includes(:vehicle_journey_at_stops).all.to_a
end
+ referential_purchase_windows_by_checksum, referential_vehicle_journey_purchase_window_checksums = referential.switch do
+ purchase_windows_by_checksum = referential.purchase_windows.each_with_object({}) do |purchase_window, hash|
+ hash[purchase_window.checksum] = purchase_window
+ end
+
+ vehicle_journey_purchase_window_checksums = Hash.new { |h,k| h[k] = [] }
+ referential.purchase_windows.joins(:vehicle_journeys).pluck("vehicle_journeys.id", :checksum).each do |vehicle_journey_id, checksum|
+ vehicle_journey_purchase_window_checksums[vehicle_journey_id] << checksum
+ end
+
+ [purchase_windows_by_checksum, vehicle_journey_purchase_window_checksums]
+ end
+
+ new_vehicle_journey_ids = {}
+
new.switch do
referential_vehicle_journeys.each do |vehicle_journey|
# find parent journey pattern by checksum
# TODO add line_id for security
+ associated_route_checksum = referential_routes_checksums[vehicle_journey.route_id]
associated_journey_pattern_checksum = referential_journey_patterns_checksums[vehicle_journey.journey_pattern_id]
- existing_associated_journey_pattern = new.journey_patterns.find_by checksum: associated_journey_pattern_checksum
+
+ existing_associated_route = new.routes.find_by checksum: associated_route_checksum
+ existing_associated_journey_pattern = existing_associated_route.journey_patterns.find_by checksum: associated_journey_pattern_checksum
existing_vehicle_journey = new.vehicle_journeys.find_by journey_pattern_id: existing_associated_journey_pattern.id, checksum: vehicle_journey.checksum
- unless existing_vehicle_journey
+ if existing_vehicle_journey
+ existing_vehicle_journey.merge_metadata_from vehicle_journey
+ new_vehicle_journey_ids[vehicle_journey.id] = existing_vehicle_journey.id
+ else
objectid = Chouette::VehicleJourney.where(objectid: vehicle_journey.objectid).exists? ? nil : vehicle_journey.objectid
attributes = vehicle_journey.attributes.merge(
id: nil,
@@ -264,11 +324,39 @@ class Merge < ActiveRecord::Base
new_vehicle_journey.vehicle_journey_at_stops.build at_stop_attributes
end
+ # Associate (and create if needed) PurchaseWindows
+
+ referential_vehicle_journey_purchase_window_checksums[vehicle_journey.id].each do |purchase_window_checksum|
+ associated_purchase_window = new.purchase_windows.find_by(checksum: purchase_window_checksum)
+
+ unless associated_purchase_window
+ purchase_window = referential_purchase_windows_by_checksum[purchase_window_checksum]
+
+ objectid = new.purchase_windows.where(objectid: purchase_window.objectid).exists? ? nil : purchase_window.objectid
+ attributes = purchase_window.attributes.merge(
+ id: nil,
+ objectid: objectid
+ )
+ new_purchase_window = new.purchase_windows.build attributes
+ new_purchase_window.save!
+
+ if new_purchase_window.checksum != purchase_window.checksum
+ raise "Checksum has changed: #{purchase_window.checksum_source} #{new_purchase_window.checksum_source}"
+ end
+
+ associated_purchase_window = new_purchase_window
+ end
+
+ new_vehicle_journey.purchase_windows << associated_purchase_window
+ end
+
new_vehicle_journey.save!
if new_vehicle_journey.checksum != vehicle_journey.checksum
raise "Checksum has changed: #{vehicle_journey.checksum_source} #{new_vehicle_journey.checksum_source}"
end
+
+ new_vehicle_journey_ids[vehicle_journey.id] = new_vehicle_journey.id
end
end
@@ -280,14 +368,14 @@ class Merge < ActiveRecord::Base
time_tables_by_id = Hash[referential.time_tables.includes(:dates, :periods).all.to_a.map { |t| [t.id, t] }]
time_tables_with_associated_lines =
- referential.time_tables.joins(vehicle_journeys: {route: :line}).pluck("lines.id", :id, "vehicle_journeys.checksum")
+ referential.time_tables.joins(vehicle_journeys: {route: :line}).pluck("lines.id", :id, "vehicle_journeys.id")
# Because TimeTables will be modified according metadata periods
# we're loading timetables per line (line is associated to a period list)
#
# line_id: [ { time_table.id, vehicle_journey.checksum } ]
time_tables_by_lines = time_tables_with_associated_lines.inject(Hash.new { |h,k| h[k] = [] }) do |hash, row|
- hash[row.shift] << {id: row.first, vehicle_journey_checksum: row.second}
+ hash[row.shift] << {id: row.first, vehicle_journey_id: row.second}
hash
end
@@ -339,7 +427,9 @@ class Merge < ActiveRecord::Base
existing_time_table = line.time_tables.find_by checksum: candidate_time_table.checksum
- unless existing_time_table
+ if existing_time_table
+ existing_time_table.merge_metadata_from candidate_time_table
+ else
objectid = Chouette::TimeTable.where(objectid: time_table.objectid).exists? ? nil : time_table.objectid
candidate_time_table.objectid = objectid
@@ -355,7 +445,12 @@ class Merge < ActiveRecord::Base
# associate VehicleJourney
- associated_vehicle_journey = line.vehicle_journeys.find_by!(checksum: properties[:vehicle_journey_checksum])
+ new_vehicle_journey_id = new_vehicle_journey_ids[properties[:vehicle_journey_id]]
+ unless new_vehicle_journey_id
+ raise "TimeTable #{existing_time_table.inspect} associated to a not-merged VehicleJourney: #{properties[:vehicle_journey_id]}"
+ end
+
+ associated_vehicle_journey = line.vehicle_journeys.find(new_vehicle_journey_id)
associated_vehicle_journey.time_tables << existing_time_table
end
end
@@ -364,7 +459,7 @@ class Merge < ActiveRecord::Base
def save_current
output.update current: new, new: nil
- output.current.update referential_suite: output
+ output.current.update referential_suite: output, ready: true
referentials.update_all merged_at: created_at, archived_at: created_at
end
diff --git a/app/models/neptune_export.rb b/app/models/neptune_export.rb
deleted file mode 100644
index f25db69c0..000000000
--- a/app/models/neptune_export.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-class NeptuneExport < ExportTask
-
- attr_accessor :extensions, :export_type
- enumerize :references_type, in: %w( network line company group_of_line )
-
- def action_params
- {
- "neptune-export" => {
- "name" => name,
- "references_type" => references_type,
- "reference_ids" => reference_ids,
- "user_name" => user_name,
- "organisation_name" => organisation.name,
- "referential_name" => referential.name,
- "projection_type" => projection_type || "",
- "add_extension" => extensions,
- "start_date" => start_date,
- "end_date" => end_date
- }
- }
- end
-
- def data_format
- "neptune"
- end
-
-end
diff --git a/app/models/neptune_import.rb b/app/models/neptune_import.rb
deleted file mode 100644
index 1f0bdaa13..000000000
--- a/app/models/neptune_import.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-class NeptuneImport < ImportTask
-
- def action_params
- {
- "neptune-import" => {
- "no_save" => no_save,
- "user_name" => user_name,
- "name" => name,
- "organisation_name" => organisation.name,
- "referential_name" => referential.name,
- }
- }
- end
-
- def data_format
- "neptune"
- end
-
-end
diff --git a/app/models/netex_export.rb b/app/models/netex_export.rb
deleted file mode 100644
index a4c3e2454..000000000
--- a/app/models/netex_export.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-class NetexExport < ExportTask
-
- enumerize :references_type, in: %w( network line company group_of_line )
-
- def action_params
- {
- "netex-export" => {
- "name" => name,
- "references_type" => references_type,
- "reference_ids" => reference_ids,
- "user_name" => user_name,
- "organisation_name" => organisation.name,
- "referential_name" => referential.name,
- "start_date" => start_date,
- "end_date" => end_date
- }
- }
- end
-
- def data_format
- "netex"
- end
-
-end
diff --git a/app/models/netex_import.rb b/app/models/netex_import.rb
deleted file mode 100644
index b21af3408..000000000
--- a/app/models/netex_import.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-require 'net/http'
-class NetexImport < Import
- before_destroy :destroy_non_ready_referential
-
- after_commit :launch_java_import, on: :create
- before_save def abort_unless_referential
- self.status = 'aborted' unless referential
- end
-
- validates_presence_of :parent
-
- def launch_java_import
- return if self.class.finished_statuses.include?(status)
- threaded_call_boiv_iev
- end
-
- private
-
- def destroy_non_ready_referential
- if referential && !referential.ready
- referential.destroy
- end
- end
-
- def threaded_call_boiv_iev
- Thread.new(&method(:call_boiv_iev))
- end
-
- def call_boiv_iev
- Rails.logger.error("Begin IEV call for import")
- Net::HTTP.get(URI("#{Rails.configuration.iev_url}/boiv_iev/referentials/importer/new?id=#{id}"))
- Rails.logger.error("End IEV call for import")
- rescue Exception => e
- logger.error "IEV server error : #{e.message}"
- logger.error e.backtrace.inspect
- end
-
-end
diff --git a/app/models/organisation.rb b/app/models/organisation.rb
index e8fb4e060..5742c81e8 100644
--- a/app/models/organisation.rb
+++ b/app/models/organisation.rb
@@ -1,5 +1,5 @@
# coding: utf-8
-class Organisation < ActiveRecord::Base
+class Organisation < ApplicationModel
include DataFormatEnumerations
has_many :users, :dependent => :destroy
@@ -13,6 +13,8 @@ class Organisation < ActiveRecord::Base
has_many :line_referentials, through: :line_referential_memberships
has_many :workbenches
+ has_many :workgroups, through: :workbenches
+
has_many :calendars
has_many :api_keys, class_name: 'Api::V1::ApiKey'
@@ -84,4 +86,8 @@ class Organisation < ActiveRecord::Base
workbenches.default
end
+ def lines_scope
+ functional_scope = sso_attributes.try(:[], "functional_scope")
+ JSON.parse(functional_scope) if functional_scope
+ end
end
diff --git a/app/models/public_version.rb b/app/models/public_version.rb
deleted file mode 100644
index 4dbf6ce27..000000000
--- a/app/models/public_version.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-class PublicVersion < PaperTrail::Version
- # custom behaviour, e.g:
- self.table_name = :'public.versions'
-end
diff --git a/app/models/referential.rb b/app/models/referential.rb
index 91a88d02d..78b719fab 100644
--- a/app/models/referential.rb
+++ b/app/models/referential.rb
@@ -1,5 +1,5 @@
# coding: utf-8
-class Referential < ActiveRecord::Base
+class Referential < ApplicationModel
include DataFormatEnumerations
include ObjectidFormatterSupport
@@ -78,31 +78,29 @@ class Referential < ActiveRecord::Base
alias_method_chain :save, :table_lock_timeout
- if Rails.env.development?
- def self.force_register_models_with_checksum
- paths = Rails.application.paths['app/models'].to_a
- Rails.application.railties.each do |tie|
- next unless tie.respond_to? :paths
- paths += tie.paths['app/models'].to_a
- end
+ def self.force_register_models_with_checksum
+ paths = Rails.application.paths['app/models'].to_a
+ Rails.application.railties.each do |tie|
+ next unless tie.respond_to? :paths
+ paths += tie.paths['app/models'].to_a
+ end
- paths.each do |path|
- next unless File.directory?(path)
- Dir.chdir path do
- Dir['**/*.rb'].each do |src|
- next if src =~ /^concerns/
- # thanks for inconsistent naming ...
- if src == "route_control/zdl_stop_area.rb"
- RouteControl::ZDLStopArea
- next
- end
- Rails.logger.info "Loading #{src}"
- begin
- src[0..-4].classify.safe_constantize
- rescue => e
- Rails.logger.info "Failed: #{e.message}"
- nil
- end
+ paths.each do |path|
+ next unless File.directory?(path)
+ Dir.chdir path do
+ Dir['**/*.rb'].each do |src|
+ next if src =~ /^concerns/
+ # thanks for inconsistent naming ...
+ if src == "route_control/zdl_stop_area.rb"
+ RouteControl::ZDLStopArea
+ next
+ end
+ Rails.logger.info "Loading #{src}"
+ begin
+ src[0..-4].classify.safe_constantize
+ rescue => e
+ Rails.logger.info "Failed: #{e.message}"
+ nil
end
end
end
@@ -168,6 +166,10 @@ class Referential < ActiveRecord::Base
Chouette::TimeTable.all
end
+ def time_table_dates
+ Chouette::TimeTableDate.all
+ end
+
def timebands
Chouette::Timeband.all
end
@@ -184,6 +186,10 @@ class Referential < ActiveRecord::Base
Chouette::VehicleJourneyFrequency.all
end
+ def vehicle_journey_at_stops
+ Chouette::VehicleJourneyAtStop.all
+ end
+
def routing_constraint_zones
Chouette::RoutingConstraintZone.all
end
@@ -233,7 +239,7 @@ class Referential < ActiveRecord::Base
end
end
- def self.new_from(from, functional_scope)
+ def self.new_from(from, organisation)
Referential.new(
name: I18n.t("activerecord.copy", name: from.name),
slug: "#{from.slug}_clone",
@@ -244,7 +250,7 @@ class Referential < ActiveRecord::Base
stop_area_referential: from.stop_area_referential,
created_from: from,
objectid_format: from.objectid_format,
- metadatas: from.metadatas.map { |m| ReferentialMetadata.new_from(m, functional_scope) }
+ metadatas: from.metadatas.map { |m| ReferentialMetadata.new_from(m, organisation) }
)
end
diff --git a/app/models/referential_cloning.rb b/app/models/referential_cloning.rb
index d4b74bd52..f2c81009a 100644
--- a/app/models/referential_cloning.rb
+++ b/app/models/referential_cloning.rb
@@ -1,4 +1,4 @@
-class ReferentialCloning < ActiveRecord::Base
+class ReferentialCloning < ApplicationModel
include AASM
belongs_to :source_referential, class_name: 'Referential'
belongs_to :target_referential, class_name: 'Referential'
diff --git a/app/models/referential_metadata.rb b/app/models/referential_metadata.rb
index 393dc70d3..7a8a01774 100644
--- a/app/models/referential_metadata.rb
+++ b/app/models/referential_metadata.rb
@@ -1,7 +1,7 @@
require 'activeattr_ext.rb'
require 'range_ext'
-class ReferentialMetadata < ActiveRecord::Base
+class ReferentialMetadata < ApplicationModel
belongs_to :referential, touch: true
belongs_to :referential_source, class_name: 'Referential'
has_array_of :lines, class_name: 'Chouette::Line'
@@ -155,10 +155,10 @@ class ReferentialMetadata < ActiveRecord::Base
end
private :clear_periods
- def self.new_from(from, functional_scope)
+ def self.new_from(from, organisation)
from.dup.tap do |metadata|
metadata.referential_source_id = from.referential_id
- metadata.line_ids = from.referential.lines.where(id: metadata.line_ids, objectid: functional_scope).collect(&:id)
+ metadata.line_ids = from.referential.lines.where(id: metadata.line_ids).for_organisation(organisation).pluck(:id)
metadata.referential_id = nil
end
end
diff --git a/app/models/referential_suite.rb b/app/models/referential_suite.rb
index 4f825628c..f4a72f22c 100644
--- a/app/models/referential_suite.rb
+++ b/app/models/referential_suite.rb
@@ -1,4 +1,4 @@
-class ReferentialSuite < ActiveRecord::Base
+class ReferentialSuite < ApplicationModel
belongs_to :new, class_name: 'Referential'
validate def validate_consistent_new
return true if new_id.nil? || new.nil?
diff --git a/app/models/simple_exporter.rb b/app/models/simple_exporter.rb
new file mode 100644
index 000000000..1fcb76a29
--- /dev/null
+++ b/app/models/simple_exporter.rb
@@ -0,0 +1,182 @@
+# 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 = ""
+
+ process_collection
+
+ self.status ||= :success
+ rescue SimpleInterface::FailedOperation
+ self.status = :failed
+ ensure
+ @csv&.close
+ task_finished
+ 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 ..."
+ log "Export will be written in #{filepath}"
+ @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
+ instance_exec(item, &configuration.item_to_rows_mapping).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
+ @new_status ||= colorize("✓", :orange)
+ 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
+ elsif v.is_a? ActiveRecord::Base
+ flatten_hash(v.attributes).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..4cfe90cff 100644
--- a/app/models/simple_importer.rb
+++ b/app/models/simple_importer.rb
@@ -1,38 +1,5 @@
-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
-
+# coding: utf-8
+class SimpleImporter < SimpleInterface
def resolve col_name, value, &block
val = block.call(value)
return val if val.present?
@@ -41,20 +8,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,34 +30,19 @@ 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
+ task_finished
end
def encode_string s
+ return if s.nil?
s.encode("utf-8").force_encoding("utf-8")
end
def dump_csv_from_context
- dir = context[:output_dir] || "log/importers"
+ dir = context[:logs_output_dir] || "log/importers"
filepath = File.join dir, "#{self.configuration_name}_#{Time.now.strftime "%y%m%d%H%M"}.csv"
# for some reason, context[:csv].to_csv does not work
CSV.open(filepath, 'w') do |csv|
@@ -109,16 +56,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
@@ -144,6 +81,7 @@ class SimpleImporter < ActiveRecord::Base
if self.configuration.ignore_failures
unless @current_record.save
@new_status = colorize("x", :red)
+ self.status = :success_with_errors
push_in_journal({message: "errors: #{@current_record.errors.messages}", error: "invalid record", event: :error, kind: :error})
end
else
@@ -154,23 +92,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 +117,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 +153,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..7b04a07df
--- /dev/null
+++ b/app/models/simple_interface.rb
@@ -0,0 +1,372 @@
+class SimpleInterface < ApplicationModel
+ attr_accessor :configuration, :interfaces_group
+
+ 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 "#{self.name} 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 configuration
+ @configuration ||= self.class.find_configuration self.configuration_name
+ end
+
+ def init_env opts
+ @verbose = opts.delete :verbose
+
+ @_errors = []
+ @messages = []
+ @padding = 1
+ @current_line = -1
+ @number_of_lines ||= 1
+ @padding = [1, Math.log([@number_of_lines, 1].max, 10).ceil()].max
+ @output_dir = opts[:output_dir] || Rails.root.join('tmp', self.class.name.tableize)
+ @start_time = Time.now
+ 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)
+ self.status = :success_with_errors
+ 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]
+ @start_time ||= Time.now
+ time = Time.now - @start_time
+ @messages ||= []
+ if opts[:append]
+ _time, _msg = @messages.pop || []
+ _time ||= time
+ _msg ||= ""
+ @messages.push [_time, _msg+msg]
+ elsif opts[:replace]
+ @messages.pop
+ @messages << [time, msg]
+ else
+ @messages << [time, msg]
+ end
+ print_state true
+ end
+
+ def output_filepath
+ @output_filepath ||= File.join @output_dir, "#{self.configuration_name}_#{Time.now.strftime "%y%m%d%H%M"}_out.csv"
+ end
+
+ def write_output_to_csv
+ cols = %i(line kind event message error)
+ if self.journal.size > 0 && self.journal.first[:row].present?
+ log "Writing output log"
+ FileUtils.mkdir_p @output_dir
+ keys = self.journal.first[:row].map(&:first)
+ CSV.open(output_filepath, "w") do |csv|
+ csv << cols + keys
+ self.journal.each do |j|
+ csv << cols.map{|c| j[c]} + j[:row].map(&:last)
+ end
+ end
+ log "Output written in #{output_filepath}", replace: true
+ end
+ end
+
+ protected
+
+ def task_finished
+ log "Saving..."
+ self.save!
+ log "Saved", replace: true
+ write_output_to_csv
+ log "FINISHED, status: "
+ log status, color: SimpleInterface.status_color(status), append: true
+ print_state true
+ end
+
+ 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 self.colorize txt, color
+ color = {
+ red: "31",
+ green: "32",
+ orange: "33",
+ }[color] || "33"
+ "\e[#{color}m#{txt}\e[0m"
+ end
+
+ def self.status_color status
+ color = :green
+ color = :orange if status.to_s == "success_with_warnings"
+ color = :red if status.to_s == "success_with_errors"
+ color = :red if status.to_s == "error"
+ color
+ end
+
+ def colorize txt, color
+ SimpleInterface.colorize txt, color
+ end
+
+ def print_state force=false
+ return unless @verbose
+ return if !@last_repaint.nil? && (Time.now - @last_repaint < 0.1) && !force
+
+ @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
+
+ msg = ""
+
+ if @banner.nil? && interfaces_group.present?
+ @banner = interfaces_group.banner @status_width
+ @status_height -= @banner.lines.count + 2
+ end
+
+ if @banner.present?
+ msg += @banner
+ msg += "\n" + "-"*@term_width + "\n"
+ 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 do |m|
+ "[#{"%.5f" % m[0]}]\t" + m[1].truncate(@status_width - 10)
+ end.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
+ @last_repaint = Time.now
+ 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_interfaces_group.rb b/app/models/simple_interfaces_group.rb
new file mode 100644
index 000000000..808be6570
--- /dev/null
+++ b/app/models/simple_interfaces_group.rb
@@ -0,0 +1,76 @@
+class SimpleInterfacesGroup
+ attr_accessor :name, :shared_options
+
+ def initialize name
+ @name = name
+ @interfaces = []
+ @current_step = 0
+ end
+
+ def add_interface interface, name, action, opts={}
+ @interfaces.push({interface: interface, name: name, action: action, opts: opts})
+ end
+
+ def run
+ @interfaces.each do |interface_def|
+ interface = interface_def[:interface]
+ interface.interfaces_group = self
+ interface.send interface_def[:action], interface_def[:opts].reverse_update(shared_options || {})
+ return if interface.status == :error
+ @current_step += 1
+ end
+
+ print_summary
+ end
+
+ def banner width=nil
+ width ||= @width
+ width ||= 128
+ @width = width
+
+ name = "### #{self.name} ###"
+ centered_name = " " * ([width - name.size, 0].max / 2) + name
+ banner = [centered_name, ""]
+ banner << @interfaces.each_with_index.map do |interface, i|
+ if interface[:interface].status.present?
+ SimpleInterface.colorize interface[:name], SimpleInterface.status_color(interface[:interface].status)
+ elsif i == @current_step
+ "☕︎ #{interface[:name]}"
+ else
+ interface[:name]
+ end
+ end.join(' > ')
+ banner.join("\n")
+ end
+
+ def print_summary
+ puts "\e[H\e[2J"
+ out = [banner]
+ out << "-" * @width
+ out << ""
+ out << SimpleInterface.colorize("=== STATUSES ===", :green)
+ out << ""
+ @interfaces.each do |i|
+ out << "#{i[:name].rjust(@interfaces.map{|i| i[:name].size}.max)}:\t#{SimpleInterface.colorize i[:interface].status, SimpleInterface.status_color(i[:interface].status)}"
+ end
+ out << ""
+ out << SimpleInterface.colorize("=== OUTPUTS ===", :green)
+ out << ""
+ @interfaces.each do |i|
+ if i[:interface].is_a? SimpleExporter
+ out << "#{i[:name].rjust(@interfaces.map{|i| i[:name].size}.max)}:\t#{i[:interface].filepath}"
+ end
+ end
+ out << ""
+ out << ""
+ out << SimpleInterface.colorize("=== DEBUG OUTPUTS ===", :green)
+ out << ""
+ @interfaces.each do |i|
+ out << "#{i[:name].rjust(@interfaces.map{|i| i[:name].size}.max)}:\t#{i[:interface].output_filepath}"
+ end
+ out << ""
+ out << ""
+
+ print out.join("\n")
+ end
+end
diff --git a/app/models/simple_json_exporter.rb b/app/models/simple_json_exporter.rb
new file mode 100644
index 000000000..024d75c97
--- /dev/null
+++ b/app/models/simple_json_exporter.rb
@@ -0,0 +1,183 @@
+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 = ""
+
+ process_collection
+ self.status ||= :success
+ rescue SimpleInterface::FailedOperation
+ self.status = :failed
+ ensure
+ if @file
+ log "Writing to JSON file..."
+ @file.write @out.to_json
+ log "JSON file written", replace: true
+ @file.close
+ end
+ task_finished
+ 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 ..."
+ log "Export will be written in #{filepath}"
+
+ if collection.is_a?(ActiveRecord::Relation) && collection.model.column_names.include?("id")
+ log "Using paginated collection", color: :green
+ ids = collection.pluck :id
+ ids.in_groups_of(configuration.batch_size).each do |batch_ids|
+ collection.where(id: batch_ids.compact).each do |item|
+ handle_item item
+ end
+ end
+ else
+ log "Using non-paginated collection", color: :orange
+ collection.each{|item| handle_item item }
+ end
+ print_state true
+ 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.symbolize_keys
+ @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/app/models/stop_area_referential.rb b/app/models/stop_area_referential.rb
index 54e895cd0..6c339547c 100644
--- a/app/models/stop_area_referential.rb
+++ b/app/models/stop_area_referential.rb
@@ -1,4 +1,6 @@
-class StopAreaReferential < ActiveRecord::Base
+class StopAreaReferential < ApplicationModel
+ validates :registration_number_format, format: { with: /\AX*\z/ }
+
include ObjectidFormatterSupport
has_many :stop_area_referential_memberships
has_many :organisations, through: :stop_area_referential_memberships
@@ -6,13 +8,40 @@ class StopAreaReferential < ActiveRecord::Base
has_many :stop_areas, class_name: 'Chouette::StopArea'
has_many :stop_area_referential_syncs, -> {order created_at: :desc}
has_many :workbenches
+ has_one :workgroup
def add_member(organisation, options = {})
attributes = options.merge organisation: organisation
- stop_area_referential_memberships.build attributes
+ stop_area_referential_memberships.build attributes unless organisations.include?(organisation)
end
def last_sync
stop_area_referential_syncs.last
end
+
+ def generate_registration_number
+ return "" unless registration_number_format.present?
+ last = self.stop_areas.order("registration_number DESC NULLS LAST").limit(1).first&.registration_number
+ if self.stop_areas.count == 26**self.registration_number_format.size
+ raise "NO MORE AVAILABLE VALUES FOR registration_number in referential #{self.name}"
+ end
+
+ return "A" * self.registration_number_format.size unless last
+
+ if last == "Z" * self.registration_number_format.size
+ val = "A" * self.registration_number_format.size
+ while self.stop_areas.where(registration_number: val).exists?
+ val = val.next
+ end
+ val
+ else
+ last.next
+ end
+ end
+
+ def validates_registration_number value
+ return false unless value.size == registration_number_format.size
+ return false unless value =~ /^[A-Z]*$/
+ true
+ end
end
diff --git a/app/models/stop_area_referential_membership.rb b/app/models/stop_area_referential_membership.rb
index 435970961..d507bc50e 100644
--- a/app/models/stop_area_referential_membership.rb
+++ b/app/models/stop_area_referential_membership.rb
@@ -1,4 +1,6 @@
-class StopAreaReferentialMembership < ActiveRecord::Base
+class StopAreaReferentialMembership < ApplicationModel
belongs_to :organisation
belongs_to :stop_area_referential
+
+ validates :organisation_id, presence: true, uniqueness: { scope: :stop_area_referential }
end
diff --git a/app/models/stop_area_referential_sync.rb b/app/models/stop_area_referential_sync.rb
index e6cf2ecbc..8b48d35e6 100644
--- a/app/models/stop_area_referential_sync.rb
+++ b/app/models/stop_area_referential_sync.rb
@@ -1,4 +1,4 @@
-class StopAreaReferentialSync < ActiveRecord::Base
+class StopAreaReferentialSync < ApplicationModel
include AASM
belongs_to :stop_area_referential
has_many :stop_area_referential_sync_messages, :dependent => :destroy
diff --git a/app/models/stop_area_referential_sync_message.rb b/app/models/stop_area_referential_sync_message.rb
index cd2e62405..642ccfc38 100644
--- a/app/models/stop_area_referential_sync_message.rb
+++ b/app/models/stop_area_referential_sync_message.rb
@@ -1,4 +1,4 @@
-class StopAreaReferentialSyncMessage < ActiveRecord::Base
+class StopAreaReferentialSyncMessage < ApplicationModel
belongs_to :stop_area_referential_sync
enum criticity: [:info, :warning, :error]
diff --git a/app/models/user.rb b/app/models/user.rb
index 31e634415..ba166b06f 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1,4 +1,4 @@
-class User < ActiveRecord::Base
+class User < ApplicationModel
# Include default devise modules. Others available are:
# :token_authenticatable, :encryptable, :confirmable, :lockable, :timeoutable and :omniauthable, :database_authenticatable
@@ -9,7 +9,7 @@ class User < ActiveRecord::Base
:recoverable, :rememberable, :trackable, :async, authentication_type
# FIXME https://github.com/nbudin/devise_cas_authenticatable/issues/53
- # Work around :validatable, when database_authenticatable is diabled.
+ # Work around :validatable, when database_authenticatable is disabled.
attr_accessor :password unless authentication_type == :database_authenticatable
# Setup accessible (or protected) attributes for your model
@@ -30,6 +30,8 @@ class User < ActiveRecord::Base
scope :with_organisation, -> { where.not(organisation_id: nil) }
+ scope :from_workgroup, ->(workgroup_id) { joins(:workbenches).where(workbenches: {workgroup_id: workgroup_id}) }
+
# Callback invoked by DeviseCasAuthenticable::Model#authernticate_with_cas_ticket
def cas_extra_attributes=(extra_attributes)
@@ -67,6 +69,10 @@ class User < ActiveRecord::Base
permissions && permissions.include?(permission)
end
+ def can_monitor_sidekiq?
+ has_permission?("sidekiq.monitor")
+ end
+
private
# remove organisation and referentials if last user of it
diff --git a/app/models/vehicle_journey_control/speed.rb b/app/models/vehicle_journey_control/speed.rb
index e5e331b50..c9775e7a3 100644
--- a/app/models/vehicle_journey_control/speed.rb
+++ b/app/models/vehicle_journey_control/speed.rb
@@ -2,8 +2,6 @@ module VehicleJourneyControl
class Speed < ComplianceControl
store_accessor :control_attributes, :minimum, :maximum
- validates_numericality_of :minimum, allow_nil: true, greater_than_or_equal_to: 0
- validates_numericality_of :maximum, allow_nil: true, greater_than_or_equal_to: 0
include MinMaxValuesValidation
def self.default_code; "3-VehicleJourney-2" end
diff --git a/app/models/vehicle_journey_import.rb b/app/models/vehicle_journey_import.rb
index 250f3a9e9..2eb723c29 100644
--- a/app/models/vehicle_journey_import.rb
+++ b/app/models/vehicle_journey_import.rb
@@ -1,7 +1,7 @@
class VehicleJourneyImport
- include ActiveModel::Validations
- include ActiveModel::Conversion
- extend ActiveModel::Naming
+ include ActiveModel::Model
+
+ extend EnhancedModelI18n
attr_accessor :file, :route
attr_accessor :created_vehicle_journey_count,:updated_vehicle_journey_count,:deleted_vehicle_journey_count
diff --git a/app/models/workbench.rb b/app/models/workbench.rb
index eb53af7aa..ef0b2eaa4 100644
--- a/app/models/workbench.rb
+++ b/app/models/workbench.rb
@@ -1,4 +1,4 @@
-class Workbench < ActiveRecord::Base
+class Workbench < ApplicationModel
DEFAULT_WORKBENCH_NAME = "Gestion de l'offre"
include ObjectidFormatterSupport
@@ -13,8 +13,9 @@ class Workbench < ActiveRecord::Base
has_many :companies, through: :line_referential
has_many :group_of_lines, through: :line_referential
has_many :stop_areas, through: :stop_area_referential
- has_many :imports
- has_many :workbench_imports
+ has_many :imports, class_name: Import::Base
+ has_many :exports, class_name: Export::Base
+ has_many :workbench_imports, class_name: Import::Workbench
has_many :compliance_check_sets
has_many :compliance_control_sets
has_many :merges
@@ -42,6 +43,10 @@ class Workbench < ActiveRecord::Base
end
end
+ def calendars
+ workgroup.calendars.where('(organisation_id = ? OR shared = ?)', organisation.id, true)
+ end
+
def self.default
self.last if self.count == 1
where(name: DEFAULT_WORKBENCH_NAME).last
diff --git a/app/models/workbench_import.rb b/app/models/workbench_import.rb
deleted file mode 100644
index 27f53a44f..000000000
--- a/app/models/workbench_import.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-class WorkbenchImport < Import
- after_commit :launch_worker, :on => :create
-
- def launch_worker
- WorkbenchImportWorker.perform_async(id)
- end
-end
diff --git a/app/models/workgroup.rb b/app/models/workgroup.rb
index 3af20ae23..3e8409634 100644
--- a/app/models/workgroup.rb
+++ b/app/models/workgroup.rb
@@ -1,4 +1,4 @@
-class Workgroup < ActiveRecord::Base
+class Workgroup < ApplicationModel
belongs_to :line_referential
belongs_to :stop_area_referential
@@ -11,10 +11,16 @@ class Workgroup < ActiveRecord::Base
validates_presence_of :line_referential_id
validates_presence_of :stop_area_referential_id
+ validates_uniqueness_of :stop_area_referential_id
+ validates_uniqueness_of :line_referential_id
has_many :custom_fields
def custom_fields_definitions
Hash[*custom_fields.map{|cf| [cf.code, cf]}.flatten]
end
+
+ def has_export? export_name
+ export_types.include? export_name
+ end
end