aboutsummaryrefslogtreecommitdiffstats
path: root/app/models
diff options
context:
space:
mode:
Diffstat (limited to 'app/models')
-rw-r--r--app/models/calendar.rb185
-rw-r--r--app/models/calendar/period.rb7
-rw-r--r--app/models/chouette/area_type.rb55
-rw-r--r--app/models/chouette/company.rb2
-rw-r--r--app/models/chouette/journey_pattern.rb33
-rw-r--r--app/models/chouette/line.rb14
-rw-r--r--app/models/chouette/network.rb2
-rw-r--r--app/models/chouette/purchase_window.rb45
-rw-r--r--app/models/chouette/route.rb2
-rw-r--r--app/models/chouette/routing_constraint_zone.rb10
-rw-r--r--app/models/chouette/stop_area.rb77
-rw-r--r--app/models/chouette/time_table.rb254
-rw-r--r--app/models/chouette/time_table_period.rb7
-rw-r--r--app/models/chouette/vehicle_journey.rb106
-rw-r--r--app/models/chouette/vehicle_journey_at_stop.rb39
-rw-r--r--app/models/compliance_check_block.rb6
-rw-r--r--app/models/compliance_check_message_export.rb46
-rw-r--r--app/models/compliance_check_set.rb24
-rw-r--r--app/models/compliance_control.rb8
-rw-r--r--app/models/compliance_control_block.rb6
-rw-r--r--app/models/compliance_control_set.rb2
-rw-r--r--app/models/concerns/application_days_support.rb107
-rw-r--r--app/models/concerns/checksum_support.rb56
-rw-r--r--app/models/concerns/date_support.rb83
-rw-r--r--app/models/concerns/min_max_values_validation.rb6
-rw-r--r--app/models/concerns/objectid_support.rb5
-rw-r--r--app/models/concerns/period_support.rb80
-rw-r--r--app/models/concerns/timetable_support.rb149
-rw-r--r--app/models/custom_field.rb9
-rw-r--r--app/models/generic_attribute_control/min_max.rb6
-rw-r--r--app/models/generic_attribute_control/pattern.rb2
-rw-r--r--app/models/generic_attribute_control/uniqueness.rb2
-rw-r--r--app/models/import.rb11
-rw-r--r--app/models/import_message_export.rb4
-rw-r--r--app/models/line_control/route.rb2
-rw-r--r--app/models/merge.rb435
-rw-r--r--app/models/organisation.rb66
-rw-r--r--app/models/public_version.rb4
-rw-r--r--app/models/referential.rb188
-rw-r--r--app/models/referential_cloning.rb27
-rw-r--r--app/models/referential_suite.rb6
-rw-r--r--app/models/route_control/opposite_route.rb2
-rw-r--r--app/models/route_control/opposite_route_terminus.rb2
-rw-r--r--app/models/user.rb2
-rw-r--r--app/models/vehicle_journey_control/delta.rb5
-rw-r--r--app/models/vehicle_journey_control/speed.rb6
-rw-r--r--app/models/vehicle_journey_control/waiting_time.rb5
-rw-r--r--app/models/workbench.rb9
-rw-r--r--app/models/workgroup.rb20
49 files changed, 1824 insertions, 405 deletions
diff --git a/app/models/calendar.rb b/app/models/calendar.rb
index b2e73929f..84b569ab4 100644
--- a/app/models/calendar.rb
+++ b/app/models/calendar.rb
@@ -3,176 +3,115 @@ require_relative 'calendar/date_value'
require_relative 'calendar/period'
class Calendar < ActiveRecord::Base
- has_paper_trail
+ include DateSupport
+ include PeriodSupport
+ include ApplicationDaysSupport
+ include TimetableSupport
+
+ has_paper_trail class_name: 'PublicVersion'
belongs_to :organisation
- has_many :time_tables
+ belongs_to :workgroup
- validates_presence_of :name, :short_name, :organisation
+ validates_presence_of :name, :short_name, :organisation, :workgroup
validates_uniqueness_of :short_name
- after_initialize :init_dates_and_date_ranges
+ has_many :time_tables
scope :contains_date, ->(date) { where('date ? = any (dates) OR date ? <@ any (date_ranges)', date, date) }
- def init_dates_and_date_ranges
- self.dates ||= []
- self.date_ranges ||= []
- end
+ after_initialize :set_defaults
def self.ransackable_scopes(auth_object = nil)
[:contains_date]
end
- def convert_to_time_table
- Chouette::TimeTable.new.tap do |tt|
- self.dates.each do |d|
- tt.dates << Chouette::TimeTableDate.new(date: d, in_out: true)
- end
- self.periods.each do |p|
- tt.periods << Chouette::TimeTablePeriod.new(period_start: p.begin, period_end: p.end)
- end
- tt.int_day_types = 508
- end
+ def self.state_permited_attributes item
+ {name: item["comment"]}
end
-
- ### Calendar::Period
- # Required by coocon
- def build_period
- Calendar::Period.new
+ def set_defaults
+ self.excluded_dates ||= []
+ self.int_day_types ||= EVERYDAY
end
- def periods
- @periods ||= init_periods
+ def human_attribute_name(*args)
+ self.class.human_attribute_name(*args)
end
- def init_periods
- (date_ranges || [])
- .each_with_index
- .map( &Calendar::Period.method(:from_range) )
+ def shortcuts_update(date=nil)
end
- private :init_periods
-
- validate :validate_periods
- def validate_periods
- periods_are_valid = periods.all?(&:valid?)
-
- periods.each do |period|
- if period.intersect?(periods)
- period.errors.add(:base, I18n.t('calendars.errors.overlapped_periods'))
- periods_are_valid = false
+ def convert_to_time_table
+ Chouette::TimeTable.new.tap do |tt|
+ self.dates.each do |d|
+ tt.dates << Chouette::TimeTableDate.new(date: d, in_out: true)
end
- end
-
- unless periods_are_valid
- errors.add(:periods, :invalid)
+ self.periods.each do |p|
+ tt.periods << Chouette::TimeTablePeriod.new(period_start: p.begin, period_end: p.end)
+ end
+ tt.int_day_types = self.int_day_types
end
end
- def flatten_date_array attributes, key
- date_int = %w(1 2 3).map {|e| attributes["#{key}(#{e}i)"].to_i }
- Date.new(*date_int)
+ def include_in_dates?(day)
+ self.dates.include? day
end
- def periods_attributes=(attributes = {})
- @periods = []
- attributes.each do |index, period_attribute|
- # Convert date_select to date
- ['begin', 'end'].map do |attr|
- period_attribute[attr] = flatten_date_array(period_attribute, attr)
- end
- period = Calendar::Period.new(period_attribute.merge(id: index))
- @periods << period unless period.marked_for_destruction?
- end
-
- date_ranges_will_change!
+ def excluded_date?(day)
+ self.excluded_dates.include? day
end
- before_validation :fill_date_ranges
-
- def fill_date_ranges
- if @periods
- self.date_ranges = @periods.map(&:range).compact.sort_by(&:begin)
+ def update_in_out date, in_out
+ if in_out
+ self.excluded_dates.delete date
+ self.dates << date unless include_in_dates?(date)
+ else
+ self.dates.delete date
+ self.excluded_dates << date unless excluded_date?(date)
end
+ date
end
- after_save :clear_periods
-
- def clear_periods
- @periods = nil
+ def included_days
+ dates
end
- private :clear_periods
-
- ### Calendar::DateValue
-
- # Required by coocon
- def build_date_value
- Calendar::DateValue.new
+ def excluded_days
+ excluded_dates
end
- def date_values
- @date_values ||= init_date_values
+ def saved_dates
+ Hash[*self.dates.each_with_index.to_a.map(&:reverse).flatten]
end
- def init_date_values
- if dates
- dates.each_with_index.map { |d, index| Calendar::DateValue.from_date(index, d) }
- else
- []
+ def all_dates
+ (dates + excluded_dates).sort.each_with_index.map do |d, i|
+ OpenStruct.new(id: i, date: d, in_out: include_in_dates?(d))
end
end
- private :init_date_values
- validate :validate_date_values
-
- def validate_date_values
- date_values_are_valid = date_values.all?(&:valid?)
-
- date_values.each do |date_value|
- if date_values.count { |d| d.value == date_value.value } > 1
- date_value.errors.add(:base, I18n.t('activerecord.errors.models.calendar.attributes.dates.date_in_dates'))
- date_values_are_valid = false
- end
- date_ranges.each do |date_range|
- if date_range.cover? date_value.value
- date_value.errors.add(:base, I18n.t('activerecord.errors.models.calendar.attributes.dates.date_in_date_ranges'))
- date_values_are_valid = false
- end
- end
- end
-
- unless date_values_are_valid
- errors.add(:date_values, :invalid)
- end
+ def find_date_by_id id
+ self.dates[id]
end
- def date_values_attributes=(attributes = {})
- @date_values = []
- attributes.each do |index, date_value_attribute|
- date_value_attribute['value'] = flatten_date_array(date_value_attribute, 'value')
- date_value = Calendar::DateValue.new(date_value_attribute.merge(id: index))
- @date_values << date_value unless date_value.marked_for_destruction?
- end
-
- dates_will_change!
+ def destroy_date date
+ self.dates -= [date]
end
- before_validation :fill_dates
-
- def fill_dates
- if @date_values
- self.dates = @date_values.map(&:value).compact.sort
- end
+ def create_date in_out:, date:
+ update_in_out date, in_out
end
- after_save :clear_date_values
-
- def clear_date_values
- @date_values = nil
+ def find_period_by_id id
+ self.periods.find{|p| p.id == id}
end
- private :clear_date_values
+ def build_period
+ self.periods << Calendar::Period.new(id: self.periods.count + 1)
+ self.periods.last
+ end
+ def destroy_period period
+ @periods = self.periods.select{|p| p.end != period.end || p.begin != period.begin}
+ end
end
diff --git a/app/models/calendar/period.rb b/app/models/calendar/period.rb
index 1c423dfcc..8b3e4109b 100644
--- a/app/models/calendar/period.rb
+++ b/app/models/calendar/period.rb
@@ -1,5 +1,5 @@
class Calendar < ActiveRecord::Base
-
+
class Period
include ActiveAttr::Model
@@ -10,6 +10,11 @@ class Calendar < ActiveRecord::Base
validates_presence_of :begin, :end
validate :check_end_greather_than_begin
+ alias_method :period_start, :begin
+ alias_method :period_end, :end
+ alias_method :period_start=, :begin=
+ alias_method :period_end=, :end=
+
def check_end_greather_than_begin
if self.begin && self.end && self.begin >= self.end
errors.add(:base, I18n.t('calendars.errors.short_period'))
diff --git a/app/models/chouette/area_type.rb b/app/models/chouette/area_type.rb
new file mode 100644
index 000000000..e17d2ee8d
--- /dev/null
+++ b/app/models/chouette/area_type.rb
@@ -0,0 +1,55 @@
+class Chouette::AreaType
+ include Comparable
+
+ COMMERCIAL = %i(zdep zder zdlp zdlr lda gdl).freeze
+ NON_COMMERCIAL = %i(deposit border service_area relief other).freeze
+ ALL = COMMERCIAL + NON_COMMERCIAL
+
+ @@commercial = COMMERCIAL
+ @@non_commercial = NON_COMMERCIAL
+ @@all = ALL
+ mattr_accessor :all, :commercial, :non_commercial
+
+ def self.commercial=(values)
+ @@commercial = COMMERCIAL & values
+ reset_caches!
+ end
+
+ def self.non_commercial=(values)
+ @@non_commercial = NON_COMMERCIAL & values
+ reset_caches!
+ end
+
+ @@instances = {}
+ def self.find(code)
+ return unless code
+
+ code = code.to_sym
+ @@instances[code] ||= new(code) if ALL.include? code
+ end
+
+ def self.reset_caches!
+ @@all = @@commercial + @@non_commercial
+ @@instances = {}
+ @@options = {}
+ end
+
+ def self.options(kind=:all)
+ @@options ||= {}
+ @@options[kind] ||= self.send(kind).map { |c| find(c) }.map { |t| [ t.label, t.code ] }
+ end
+
+ attr_reader :code
+ def initialize(code)
+ @code = code
+ end
+
+ def <=>(other)
+ all.index(code) <=> all.index(other.code)
+ end
+
+ def label
+ I18n.translate code, scope: 'area_types.label'
+ end
+
+end
diff --git a/app/models/chouette/company.rb b/app/models/chouette/company.rb
index 12b21e347..b3d40ab96 100644
--- a/app/models/chouette/company.rb
+++ b/app/models/chouette/company.rb
@@ -3,7 +3,7 @@ module Chouette
include CompanyRestrictions
include LineReferentialSupport
include ObjectidSupport
- has_paper_trail
+ has_paper_trail class_name: 'PublicVersion'
has_many :lines
diff --git a/app/models/chouette/journey_pattern.rb b/app/models/chouette/journey_pattern.rb
index a62da6353..aa9fdb810 100644
--- a/app/models/chouette/journey_pattern.rb
+++ b/app/models/chouette/journey_pattern.rb
@@ -27,7 +27,7 @@ module Chouette
def checksum_attributes
values = self.slice(*['name', 'published_name', 'registration_number']).values
- values << self.stop_points.map(&:stop_area).map(&:user_objectid)
+ values << self.stop_points.sort_by(&:position).map(&:stop_area).map(&:user_objectid)
values.flatten
end
@@ -40,7 +40,8 @@ module Chouette
# Update attributes and stop_points associations
jp.update_attributes(state_permited_attributes(item)) unless item['new_record']
jp.state_stop_points_update(item) if !jp.errors.any? && jp.persisted?
- item['errors'] = jp.errors if jp.errors.any?
+ item['errors'] = jp.errors if jp.errors.any?
+ item['checksum'] = jp.checksum
end
if state.any? {|item| item['errors']}
@@ -57,21 +58,23 @@ module Chouette
{
name: item['name'],
published_name: item['published_name'],
- registration_number: item['registration_number']
+ registration_number: item['registration_number'],
+ costs: item['costs']
}
end
def self.state_create_instance route, item
# Flag new record, so we can unset object_id if transaction rollback
jp = route.journey_patterns.create(state_permited_attributes(item))
-
# FIXME
# DefaultAttributesSupport will trigger some weird validation on after save
# wich will call to valid?, wich will populate errors
# In this case, we mark jp to be valid if persisted? return true
jp.errors.clear if jp.persisted?
+ jp.after_commit_objectid
item['object_id'] = jp.objectid
+ item['short_id'] = jp.get_objectid.short_id
item['new_record'] = true
jp
end
@@ -145,5 +148,27 @@ module Chouette
vjas.destroy
end
end
+
+ def costs
+ read_attribute(:costs) || {}
+ end
+
+ def costs_between start, finish
+ key = "#{start.stop_area_id}-#{finish.stop_area_id}"
+ costs[key]&.symbolize_keys || {}
+ end
+
+ def full_schedule?
+ full = true
+ stop_points.order(:position).inject(nil) do |start, finish|
+ 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
end
end
diff --git a/app/models/chouette/line.rb b/app/models/chouette/line.rb
index 93d4f5e8b..ba2e2755d 100644
--- a/app/models/chouette/line.rb
+++ b/app/models/chouette/line.rb
@@ -1,6 +1,6 @@
module Chouette
class Line < Chouette::ActiveRecord
- has_paper_trail
+ has_paper_trail class_name: 'PublicVersion'
include LineRestrictions
include LineReferentialSupport
include ObjectidSupport
@@ -21,6 +21,7 @@ module Chouette
has_many :journey_patterns, :through => :routes
has_many :vehicle_journeys, :through => :journey_patterns
has_many :routing_constraint_zones, through: :routes
+ has_many :time_tables, -> { distinct }, :through => :vehicle_journeys
has_and_belongs_to_many :group_of_lines, :class_name => 'Chouette::GroupOfLine', :order => 'group_of_lines.name'
@@ -79,5 +80,16 @@ module Chouette
line_referential.companies.where(id: ([company_id] + Array(secondary_company_ids)).compact)
end
+ def deactivate!
+ update_attribute :deactivated, true
+ end
+
+ def activate!
+ update_attribute :deactivated, false
+ end
+
+ def activated?
+ !deactivated
+ end
end
end
diff --git a/app/models/chouette/network.rb b/app/models/chouette/network.rb
index 9b3f2fe29..6843c69ad 100644
--- a/app/models/chouette/network.rb
+++ b/app/models/chouette/network.rb
@@ -1,6 +1,6 @@
module Chouette
class Network < Chouette::ActiveRecord
- has_paper_trail
+ has_paper_trail class_name: 'PublicVersion'
include NetworkRestrictions
include LineReferentialSupport
include ObjectidSupport
diff --git a/app/models/chouette/purchase_window.rb b/app/models/chouette/purchase_window.rb
new file mode 100644
index 000000000..334493015
--- /dev/null
+++ b/app/models/chouette/purchase_window.rb
@@ -0,0 +1,45 @@
+require 'range_ext'
+require_relative '../calendar/period'
+
+module Chouette
+ class PurchaseWindow < Chouette::TridentActiveRecord
+ # include ChecksumSupport
+ include ObjectidSupport
+ include PeriodSupport
+ include ChecksumSupport
+ extend Enumerize
+
+ enumerize :color, in: %w(#9B9B9B #FFA070 #C67300 #7F551B #41CCE3 #09B09C #3655D7 #6321A0 #E796C6 #DD2DAA)
+
+ has_paper_trail
+ belongs_to :referential
+ has_and_belongs_to_many :vehicle_journeys, :class_name => 'Chouette::VehicleJourney'
+
+ validates_presence_of :name, :referential
+
+ scope :contains_date, ->(date) { where('date ? <@ any (date_ranges)', date) }
+ scope :overlap_dates, ->(date_range) { where('daterange(?, ?) && any (date_ranges)', date_range.first, date_range.last + 1.day) }
+
+ def self.ransackable_scopes(auth_object = nil)
+ [:contains_date]
+ end
+
+ def self.colors_i18n
+ Hash[*color.values.map{|c| [I18n.t("enumerize.purchase_window.color.#{c[1..-1]}"), c]}.flatten]
+ end
+
+ def local_id
+ "IBOO-#{self.referential.id}-#{self.id}"
+ end
+
+ def checksum_attributes
+ attrs = ['name', 'color', 'referential_id']
+ ranges_attrs = date_ranges.map{|r| [r.first, r.last]}.flatten.sort
+ self.slice(*attrs).values + ranges_attrs
+ end
+
+ # def checksum_attributes
+ # end
+
+ end
+end
diff --git a/app/models/chouette/route.rb b/app/models/chouette/route.rb
index 5c0ad24a1..5cc5d8b0d 100644
--- a/app/models/chouette/route.rb
+++ b/app/models/chouette/route.rb
@@ -133,7 +133,7 @@ module Chouette
def checksum_attributes
values = self.slice(*['name', 'published_name', 'wayback']).values
values.tap do |attrs|
- attrs << self.stop_points.map{|sp| "#{sp.stop_area.user_objectid}#{sp.for_boarding}#{sp.for_alighting}" }.join
+ 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)
end
end
diff --git a/app/models/chouette/routing_constraint_zone.rb b/app/models/chouette/routing_constraint_zone.rb
index fcf47f154..58703598e 100644
--- a/app/models/chouette/routing_constraint_zone.rb
+++ b/app/models/chouette/routing_constraint_zone.rb
@@ -7,7 +7,7 @@ module Chouette
belongs_to :route
has_array_of :stop_points, class_name: 'Chouette::StopPoint'
- validates_presence_of :name, :stop_points, :route
+ validates_presence_of :name, :stop_points, :route_id
# validates :stop_point_ids, length: { minimum: 2, too_short: I18n.t('activerecord.errors.models.routing_constraint_zone.attributes.stop_points.not_enough_stop_points') }
validate :stop_points_belong_to_route, :not_all_stop_points_selected
@@ -25,14 +25,20 @@ module Chouette
end
def checksum_attributes
- self.stop_points.map(&:stop_area).map(&:user_objectid)
+ [
+ self.stop_points.map(&:stop_area).map(&:user_objectid)
+ ]
end
def stop_points_belong_to_route
+ return unless route
+
errors.add(:stop_point_ids, I18n.t('activerecord.errors.models.routing_constraint_zone.attributes.stop_points.stop_points_not_from_route')) unless stop_points.all? { |sp| route.stop_points.include? sp }
end
def not_all_stop_points_selected
+ return unless route
+
errors.add(:stop_point_ids, I18n.t('activerecord.errors.models.routing_constraint_zone.attributes.stop_points.all_stop_points_selected')) if stop_points.length == route.stop_points.length
end
diff --git a/app/models/chouette/stop_area.rb b/app/models/chouette/stop_area.rb
index cc7170728..bb8747faa 100644
--- a/app/models/chouette/stop_area.rb
+++ b/app/models/chouette/stop_area.rb
@@ -2,14 +2,17 @@ require 'geokit'
require 'geo_ruby'
module Chouette
class StopArea < Chouette::ActiveRecord
- has_paper_trail
+ has_paper_trail class_name: 'PublicVersion'
include ProjectionFields
include StopAreaRestrictions
include StopAreaReferentialSupport
include ObjectidSupport
extend Enumerize
- enumerize :area_type, in: %i(zdep zder zdlp zdlr lda)
+ enumerize :area_type, in: Chouette::AreaType::ALL
+ enumerize :kind, in: %i(commercial non_commercial)
+
+ AVAILABLE_LOCALIZATIONS = %i(gb nl de fr it es)
with_options dependent: :destroy do |assoc|
assoc.has_many :stop_points
@@ -31,6 +34,7 @@ module Chouette
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
validates_presence_of :longitude, :if => :latitude
validates_numericality_of :latitude, :less_than_or_equal_to => 90, :greater_than_or_equal_to => -90, :allow_nil => true
@@ -39,11 +43,36 @@ module Chouette
validates_format_of :coordinates, :with => %r{\A *-?(0?[0-9](\.[0-9]*)?|[0-8][0-9](\.[0-9]*)?|90(\.[0]*)?) *\, *-?(0?[0-9]?[0-9](\.[0-9]*)?|1[0-7][0-9](\.[0-9]*)?|180(\.[0]*)?) *\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_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
+
def self.nullable_attributes
[:registration_number, :street_name, :country_code, :fare_code,
:nearest_topic_name, :comment, :long_lat_type, :zip_code, :city_name, :url, :time_zone]
end
+ def localized_names
+ val = read_attribute(:localized_names) || {}
+ Hash[*AVAILABLE_LOCALIZATIONS.map{|k| [k, val[k.to_s]]}.flatten]
+ end
+
+ def parent_area_type_must_be_greater
+ return unless self.parent
+
+ parent_area_type = Chouette::AreaType.find(self.parent.area_type)
+ if Chouette::AreaType.find(self.area_type) >= parent_area_type
+ errors.add(:parent_id, I18n.t('stop_areas.errors.parent_area_type', area_type: parent_area_type.label))
+ end
+ end
+
+ def area_type_of_right_kind
+ return unless self.kind
+ unless Chouette::AreaType.send(self.kind).map(&:to_s).include?(self.area_type)
+ errors.add(:area_type, I18n.t('stop_areas.errors.incorrect_kind_area_type'))
+ end
+ end
+
after_update :clean_invalid_access_links
before_save :coordinates_to_lat_lng
@@ -72,6 +101,10 @@ module Chouette
end
end
+ def full_name
+ "#{name} #{zip_code} #{city_name} - #{user_objectid}"
+ end
+
def user_objectid
if objectid =~ /^.*:([0-9A-Za-z_-]+):STIF$/
$1
@@ -80,6 +113,8 @@ module Chouette
end
end
+ alias_method :local_id, :user_objectid
+
def children_in_depth
return [] if self.children.empty?
@@ -196,10 +231,12 @@ module Chouette
GeoRuby::SimpleFeatures::Envelope.from_coordinates coordinates
end
+ # DEPRECATED use StopArea#area_type
def stop_area_type
area_type ? area_type : " "
end
+ # DEPRECATED use StopArea#area_type
def stop_area_type=(stop_area_type)
self.area_type = (stop_area_type ? stop_area_type.camelcase : nil)
end
@@ -324,5 +361,41 @@ module Chouette
end
end
+ def activated?
+ deleted_at.nil?
+ end
+
+ def deactivated?
+ !activated?
+ end
+
+ def activate!
+ update_attribute :deleted_at, nil
+ end
+
+ def deactivate!
+ update_attribute :deleted_at, Time.now
+ end
+
+ def time_zone_offset
+ return 0 unless time_zone.present?
+ ActiveSupport::TimeZone[time_zone]&.utc_offset
+ end
+
+ def country_name
+ return unless country_code
+
+ country = ISO3166::Country[country_code]
+ country.translations[I18n.locale.to_s] || country.name
+ end
+
+ def time_zone_formatted_offset
+ return nil unless time_zone.present?
+ ActiveSupport::TimeZone[time_zone]&.formatted_offset
+ end
+
+ def commercial?
+ kind == "commercial"
+ end
end
end
diff --git a/app/models/chouette/time_table.rb b/app/models/chouette/time_table.rb
index 74c20f061..15b22b671 100644
--- a/app/models/chouette/time_table.rb
+++ b/app/models/chouette/time_table.rb
@@ -4,11 +4,13 @@ module Chouette
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 :monday,:tuesday,:wednesday,:thursday,:friday,:saturday,:sunday
attr_accessor :tag_search
def self.ransackable_attributes auth_object = nil
@@ -40,11 +42,18 @@ module Chouette
def checksum_attributes
[].tap do |attrs|
attrs << self.int_day_types
- attrs << self.dates.map(&:checksum).map(&:to_s).sort
- attrs << self.periods.map(&:checksum).map(&:to_s).sort
+ dates = self.dates
+ dates += TimeTableDate.where(time_table_id: self.id)
+ attrs << dates.map(&:checksum).map(&:to_s).sort
+ periods = self.periods
+ periods += TimeTablePeriod.where(time_table_id: self.id)
+ attrs << periods.map(&:checksum).map(&:to_s).sort
end
end
+ has_checksum_children TimeTableDate
+ has_checksum_children TimeTablePeriod
+
def self.object_id_key
"Timetable"
end
@@ -81,72 +90,36 @@ module Chouette
end
end
- def state_update state
- update_attributes(self.class.state_permited_attributes(state))
- self.tag_list = state['tags'].collect{|t| t['name']}.join(', ')
- self.calendar_id = nil unless state['calendar']
-
- days = state['day_types'].split(',')
- Date::DAYNAMES.map(&:underscore).each do |name|
- prefix = human_attribute_name(name).first(2)
- send("#{name}=", days.include?(prefix))
- end
-
- saved_dates = Hash[self.dates.collect{ |d| [d.id, d.date]}]
- cmonth = Date.parse(state['current_periode_range'])
-
- state['current_month'].each do |d|
- date = Date.parse(d['date'])
- checked = d['include_date'] || d['excluded_date']
- in_out = d['include_date'] ? true : false
-
- date_id = saved_dates.key(date)
- time_table_date = self.dates.find(date_id) if date_id
+ def find_date_by_id id
+ self.dates.find id
+ end
- next if !checked && !time_table_date
- # Destroy date if no longer checked
- next if !checked && time_table_date.destroy
+ def destroy_date date
+ date.destroy
+ end
- # Create new date
- unless time_table_date
- time_table_date = self.dates.create({in_out: in_out, date: date})
- end
- # Update in_out
- if in_out != time_table_date.in_out
- time_table_date.update_attributes({in_out: in_out})
- end
+ def update_in_out date, in_out
+ if in_out != date.in_out
+ date.update_attributes({in_out: in_out})
end
-
- self.state_update_periods state['time_table_periods']
- self.save
end
- def state_update_periods state_periods
- state_periods.each do |item|
- period = self.periods.find(item['id']) if item['id']
- next if period && item['deleted'] && period.destroy
- period ||= self.periods.build
-
- period.period_start = Date.parse(item['period_start'])
- period.period_end = Date.parse(item['period_end'])
+ def find_period_by_id id
+ self.periods.find id
+ end
- if period.changed?
- period.save
- item['id'] = period.id
- end
- end
+ def build_period
+ periods.build
+ end
- state_periods.delete_if {|item| item['deleted']}
+ def destroy_period period
+ period.destroy
end
def self.state_permited_attributes item
item.slice('comment', 'color').to_hash
end
- def presenter
- @presenter ||= ::TimeTablePresenter.new( self)
- end
-
def self.start_validity_period
[Chouette::TimeTable.minimum(:start_date)].compact.min
end
@@ -167,20 +140,6 @@ module Chouette
self.save
end
- def month_inspect(date)
- (date.beginning_of_month..date.end_of_month).map do |d|
- {
- day: I18n.l(d, format: '%A'),
- date: d.to_s,
- wday: d.wday,
- wnumber: d.strftime("%W").to_s,
- mday: d.mday,
- include_date: include_in_dates?(d),
- excluded_date: excluded_date?(d)
- }
- end
- end
-
def save_shortcuts
shortcuts_update
self.update_column(:start_date, start_date)
@@ -311,102 +270,9 @@ module Chouette
bounding_max = periods_max_date if periods_max_date &&
(bounding_max.nil? || (bounding_max < periods_max_date))
end
-
[bounding_min, bounding_max].compact
end
- 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(', ')
- end
-
- def day_by_mask(flag)
- int_day_types & flag == flag
- end
-
- def self.day_by_mask(int_day_types,flag)
- int_day_types & flag == flag
- 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
- end
- end
-
- def self.valid_days(int_day_types)
- # Build an array with day of calendar week (1-7, Monday is 1).
- [].tap do |valid_days|
- valid_days << 1 if day_by_mask(int_day_types,4)
- valid_days << 2 if day_by_mask(int_day_types,8)
- valid_days << 3 if day_by_mask(int_day_types,16)
- valid_days << 4 if day_by_mask(int_day_types,32)
- valid_days << 5 if day_by_mask(int_day_types,64)
- valid_days << 6 if day_by_mask(int_day_types,128)
- valid_days << 7 if day_by_mask(int_day_types,256)
- end
- end
-
- def monday
- day_by_mask(4)
- end
- def tuesday
- day_by_mask(8)
- end
- def wednesday
- day_by_mask(16)
- end
- def thursday
- day_by_mask(32)
- end
- def friday
- day_by_mask(64)
- end
- def saturday
- day_by_mask(128)
- end
- def sunday
- day_by_mask(256)
- end
-
- def set_day(day,flag)
- if day == '1' || day == true
- self.int_day_types |= flag
- else
- self.int_day_types &= ~flag
- end
- shortcuts_update
- end
-
- def monday=(day)
- set_day(day,4)
- end
- def tuesday=(day)
- set_day(day,8)
- end
- def wednesday=(day)
- set_day(day,16)
- end
- def thursday=(day)
- set_day(day,32)
- end
- def friday=(day)
- set_day(day,64)
- end
- def saturday=(day)
- set_day(day,128)
- end
- def sunday=(day)
- set_day(day,256)
- end
-
def effective_days_of_period(period,valid_days=self.valid_days)
days = []
period.period_start.upto(period.period_end) do |date|
@@ -453,6 +319,17 @@ module Chouette
days.sort
end
+ def create_date in_out:, date:
+ self.dates.create in_out: in_out, date: date
+ end
+
+ def saved_dates
+ Hash[self.dates.collect{ |d| [d.id, d.date]}]
+ end
+
+ def all_dates
+ dates
+ end
# produce a copy of periods without anyone overlapping or including another
def optimize_overlapping_periods
@@ -569,5 +446,56 @@ module Chouette
tt.comment = I18n.t("activerecord.copy", :name => self.comment)
tt
end
+
+ def intersect_periods!(mask_periods)
+ dates.each do |date|
+ unless mask_periods.any? { |p| p.include? date.date }
+ dates.delete date
+ end
+ end
+
+ periods.each do |period|
+ mask_periods_with_common_part = mask_periods.select { |p| p.intersect? period.range }
+
+ if mask_periods_with_common_part.empty?
+ self.periods.delete period
+ else
+ mask_periods_with_common_part.each do |mask_period|
+ intersection = (mask_period & period.range)
+ period.period_start, period.period_end = intersection.begin, intersection.end
+ end
+ end
+ end
+ end
+
+ def remove_periods!(removed_periods)
+ dates.each do |date|
+ if removed_periods.any? { |p| p.include? date.date }
+ dates.delete date
+ end
+ end
+
+ periods.each do |period|
+ modified_ranges = removed_periods.inject([period.range]) do |period_ranges, removed_period|
+ period_ranges.map { |p| p.remove removed_period }.flatten
+ end
+
+ unless modified_ranges.empty?
+ modified_ranges.each_with_index do |modified_range, index|
+ new_period = index == 0 ? period : periods.build
+
+ new_period.period_start, new_period.period_end =
+ modified_range.min, modified_range.max
+ end
+ else
+ periods.delete period
+ end
+ end
+ end
+
+ def empty?
+ dates.empty? && periods.empty?
+ end
+
end
end
diff --git a/app/models/chouette/time_table_period.rb b/app/models/chouette/time_table_period.rb
index ab3e79d7e..d9b707675 100644
--- a/app/models/chouette/time_table_period.rb
+++ b/app/models/chouette/time_table_period.rb
@@ -42,5 +42,10 @@ module Chouette
def contains?(p)
(p.period_start >= self.period_start && p.period_end <= self.period_end)
end
+
+ def range
+ period_start..period_end
+ end
+
end
-end \ No newline at end of file
+end
diff --git a/app/models/chouette/vehicle_journey.rb b/app/models/chouette/vehicle_journey.rb
index 247c30668..4a6ba3f75 100644
--- a/app/models/chouette/vehicle_journey.rb
+++ b/app/models/chouette/vehicle_journey.rb
@@ -1,3 +1,4 @@
+# coding: utf-8
module Chouette
class VehicleJourney < Chouette::TridentActiveRecord
has_paper_trail
@@ -21,8 +22,10 @@ module Chouette
belongs_to :company
belongs_to :route
belongs_to :journey_pattern
+ has_many :stop_areas, through: :journey_pattern
has_and_belongs_to_many :footnotes, :class_name => 'Chouette::Footnote'
+ has_and_belongs_to_many :purchase_windows, :class_name => 'Chouette::PurchaseWindow'
validates_presence_of :route
validates_presence_of :journey_pattern
@@ -40,6 +43,39 @@ module Chouette
before_validation :set_default_values,
:calculate_vehicle_journey_at_stop_day_offset
+ scope :with_stop_area_ids, ->(ids){
+ _ids = ids.select(&:present?).map(&:to_i)
+ if _ids.present?
+ where("array(SELECT stop_points.stop_area_id::integer FROM stop_points INNER JOIN journey_patterns_stop_points ON journey_patterns_stop_points.stop_point_id = stop_points.id WHERE journey_patterns_stop_points.journey_pattern_id = vehicle_journeys.journey_pattern_id) @> array[?]", _ids)
+ else
+ all
+ 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
+ where("id IN (#{sql})")
+ }
+
+ # We need this for the ransack object in the filters
+ ransacker :purchase_window_date_gt
+ ransacker :stop_area_ids
+
+ # returns VehicleJourneys with at least 1 day in their time_tables
+ # included in the given range
+ def self.with_matching_timetable date_range
+ out = []
+ time_tables = Chouette::TimeTable.where(id: self.joins("INNER JOIN time_tables_vehicle_journeys ON vehicle_journeys.id = time_tables_vehicle_journeys.vehicle_journey_id").pluck('time_tables_vehicle_journeys.time_table_id')).overlapping(date_range)
+ time_tables = time_tables.select do |time_table|
+ range = date_range
+ range = date_range & (time_table.start_date-1.day..time_table.end_date+1.day) || [] if time_table.start_date.present? && time_table.end_date.present?
+ range.any?{|d| time_table.include_day?(d) }
+ end
+ out += time_tables.map{|t| t.vehicle_journey_ids}.flatten
+ where(id: out)
+ end
+
# TODO: Remove this validator
# We've eliminated this validation because it prevented vehicle journeys
# from being saved with at-stops having a day offset greater than 0,
@@ -68,10 +104,14 @@ module Chouette
attrs << self.published_journey_identifier
attrs << self.try(:company).try(:get_objectid).try(:local_id)
attrs << self.footnotes.map(&:checksum).sort
- attrs << self.vehicle_journey_at_stops.map(&:checksum).sort
+ vjas = self.vehicle_journey_at_stops
+ vjas += VehicleJourneyAtStop.where(vehicle_journey_id: self.id)
+ attrs << vjas.uniq.sort_by { |s| s.stop_point&.position }.map(&:checksum).sort
end
end
+ has_checksum_children VehicleJourneyAtStop
+
def set_default_values
if number.nil?
self.number = 0
@@ -117,10 +157,14 @@ module Chouette
def update_vjas_from_state state
state.each do |vjas|
next if vjas["dummy"]
+ stop_point = Chouette::StopPoint.find_by(objectid: vjas['stop_point_objectid'])
+ stop_area = stop_point&.stop_area
+ tz = stop_area&.time_zone
+ tz = tz && ActiveSupport::TimeZone[tz]
params = {}.tap do |el|
['arrival_time', 'departure_time'].each do |field|
time = "#{vjas[field]['hour']}:#{vjas[field]['minute']}"
- el[field.to_sym] = Time.parse("2000-01-01 #{time}:00 UTC")
+ el[field.to_sym] = Time.parse("2000-01-01 #{time}:00 #{tz&.formatted_offset || "UTC"}")
end
end
stop = create_or_find_vjas_from_state(vjas)
@@ -139,7 +183,7 @@ module Chouette
end
def update_has_and_belongs_to_many_from_state item
- ['time_tables', 'footnotes'].each do |assos|
+ ['time_tables', 'footnotes', 'purchase_windows'].each do |assos|
saved = self.send(assos).map(&:id)
(saved - item[assos].map{|t| t['id']}).each do |id|
@@ -168,7 +212,8 @@ module Chouette
vj.update_attributes(state_permited_attributes(item))
vj.update_has_and_belongs_to_many_from_state(item)
- item['errors'] = vj.errors.full_messages.uniq if vj.errors.any?
+ item['errors'] = vj.errors.full_messages.uniq if vj.errors.any?
+ item['checksum'] = vj.checksum
end
# Delete ids of new object from state if we had to rollback
@@ -192,16 +237,33 @@ module Chouette
def self.state_create_instance route, item
# Flag new record, so we can unset object_id if transaction rollback
vj = route.vehicle_journeys.create(state_permited_attributes(item))
- item['objectid'] = vj.objectid
+ vj.after_commit_objectid
+ item['objectid'] = vj.objectid
+ item['short_id'] = vj.get_objectid.short_id
item['new_record'] = true
vj
end
def self.state_permited_attributes item
- attrs = item.slice('published_journey_identifier', 'published_journey_name', 'journey_pattern_id', 'company_id').to_hash
- ['company', 'journey_pattern'].map do |association|
- attrs["#{association}_id"] = item[association]['id'] if item[association]
+ attrs = item.slice(
+ 'published_journey_identifier',
+ 'published_journey_name',
+ 'journey_pattern_id',
+ 'company_id'
+ ).to_hash
+
+ if item['journey_pattern']
+ attrs['journey_pattern_id'] = item['journey_pattern']['id']
end
+
+ attrs['company_id'] = item['company'] ? item['company']['id'] : nil
+
+ attrs["custom_field_values"] = Hash[
+ *(item["custom_fields"] || {})
+ .map { |k, v| [k, v["value"]] }
+ .flatten
+ ]
+
attrs
end
@@ -240,12 +302,25 @@ 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]
+ end
+
def self.matrix(vehicle_journeys)
- {}.tap do |hash|
- vehicle_journeys.map{ |vj|
- vj.vehicle_journey_at_stops.map{ |vjas |hash[ "#{vj.id}-#{vjas.stop_point_id}"] = vjas }
- }
- end
+ Hash[*VehicleJourneyAtStop.where(vehicle_journey_id: vehicle_journeys.pluck(:id)).map do |vjas|
+ [ "#{vjas.vehicle_journey_id}-#{vjas.stop_point_id}", vjas]
+ end.flatten]
end
def self.with_stops
@@ -304,5 +379,10 @@ module Chouette
')
.where('"time_tables_vehicle_journeys"."vehicle_journey_id" IS NULL')
end
+
+ def self.lines
+ lines_query = joins(:route).select("routes.line_id").to_sql
+ Chouette::Line.where("id IN (#{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 6f0119e74..eda711ade 100644
--- a/app/models/chouette/vehicle_journey_at_stop.rb
+++ b/app/models/chouette/vehicle_journey_at_stop.rb
@@ -75,5 +75,42 @@ module Chouette
attrs << self.arrival_day_offset.to_s
end
end
+
+ def departure
+ format_time departure_time.utc
+ end
+
+ def arrival
+ format_time arrival_time.utc
+ end
+
+ def departure_local_time
+ local_time departure_time
+ end
+
+ def arrival_local_time
+ local_time arrival_time
+ end
+
+ def departure_local
+ format_time departure_local_time
+ end
+
+ def arrival_local
+ format_time arrival_local_time
+ 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
+ end
+
+ def format_time time
+ time.strftime "%H:%M" if time
+ end
+
end
-end \ No newline at end of file
+end
diff --git a/app/models/compliance_check_block.rb b/app/models/compliance_check_block.rb
index 05240b428..059547e1b 100644
--- a/app/models/compliance_check_block.rb
+++ b/app/models/compliance_check_block.rb
@@ -6,8 +6,8 @@ class ComplianceCheckBlock < ActiveRecord::Base
has_many :compliance_checks
- hstore_accessor :condition_attributes,
- transport_mode: :string,
- transport_submode: :string
+ store_accessor :condition_attributes,
+ :transport_mode,
+ :transport_submode
end
diff --git a/app/models/compliance_check_message_export.rb b/app/models/compliance_check_message_export.rb
new file mode 100644
index 000000000..04e1a9caa
--- /dev/null
+++ b/app/models/compliance_check_message_export.rb
@@ -0,0 +1,46 @@
+# -*- coding: utf-8 -*-
+require "csv"
+require "zip"
+
+class ComplianceCheckMessageExport
+ include ActiveModel::Validations
+ include ActiveModel::Conversion
+ extend ActiveModel::Naming
+
+ attr_accessor :compliance_check_messages
+
+ def initialize(attributes = {})
+ attributes.each { |name, value| send("#{name}=", value) }
+ end
+
+ def persisted?
+ false
+ end
+
+ def label(name)
+ I18n.t "vehicle_journey_exports.label.#{name}"
+ end
+
+ def column_names
+ ["criticity", "message key", "resource objectid", "link", "message"]
+ end
+
+ def to_csv(options = {})
+ 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
+ end
+
+ def to_zip(temp_file,options = {})
+ ::Zip::OutputStream.open(temp_file) { |zos| }
+ ::Zip::File.open(temp_file.path, ::Zip::File::CREATE) do |zipfile|
+ zipfile.get_output_stream(label("vj_filename")+route.id.to_s+".csv") { |f| f.puts to_csv(options) }
+ zipfile.get_output_stream(label("tt_filename")+".csv") { |f| f.puts time_tables_to_csv(options) }
+ zipfile.get_output_stream(label("ftn_filename")+".csv") { |f| f.puts footnotes_to_csv(options) }
+ end
+ end
+
+end
diff --git a/app/models/compliance_check_set.rb b/app/models/compliance_check_set.rb
index 020100f4a..289fc134f 100644
--- a/app/models/compliance_check_set.rb
+++ b/app/models/compliance_check_set.rb
@@ -1,6 +1,6 @@
class ComplianceCheckSet < ActiveRecord::Base
extend Enumerize
- has_paper_trail
+ has_paper_trail class_name: 'PublicVersion'
belongs_to :referential
belongs_to :compliance_control_set
@@ -19,6 +19,20 @@ class ComplianceCheckSet < ActiveRecord::Base
where('created_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') }
+
+ 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
if parent
# parent.child_change
@@ -26,6 +40,14 @@ class ComplianceCheckSet < ActiveRecord::Base
end
end
+ def organisation
+ workbench.organisation
+ end
+
+ def human_attribute_name(*args)
+ self.class.human_attribute_name(*args)
+ end
+
def update_status
statuses = compliance_check_resources.map do |resource|
case resource.status
diff --git a/app/models/compliance_control.rb b/app/models/compliance_control.rb
index 65e22643d..298a63ab9 100644
--- a/app/models/compliance_control.rb
+++ b/app/models/compliance_control.rb
@@ -3,10 +3,8 @@ class ComplianceControl < ActiveRecord::Base
class << self
def criticities; %i(warning error) end
def default_code; "" end
- def prerequisite; I18n.t('compliance_controls.metas.no_prerequisite'); end
- def predicate; I18n.t("compliance_controls.#{self.name.underscore}.description") end
def dynamic_attributes
- hstore_metadata_for_control_attributes.keys
+ stored_attributes[:control_attributes] || []
end
def policy_class
@@ -39,7 +37,6 @@ class ComplianceControl < ActiveRecord::Base
belongs_to :compliance_control_block
enumerize :criticity, in: criticities, scope: true, default: :warning
- hstore_accessor :control_attributes, {}
validates :criticity, presence: true
validates :name, presence: true
@@ -66,6 +63,9 @@ def initialize(attributes = {})
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
+
end
# Ensure STI subclasses are loaded
diff --git a/app/models/compliance_control_block.rb b/app/models/compliance_control_block.rb
index e27f85ae0..d7d84fd06 100644
--- a/app/models/compliance_control_block.rb
+++ b/app/models/compliance_control_block.rb
@@ -5,9 +5,9 @@ class ComplianceControlBlock < ActiveRecord::Base
belongs_to :compliance_control_set
has_many :compliance_controls, dependent: :destroy
- hstore_accessor :condition_attributes,
- transport_mode: :string,
- transport_submode: :string
+ store_accessor :condition_attributes,
+ :transport_mode,
+ :transport_submode
validates :transport_mode, presence: true
validates :compliance_control_set, presence: true
diff --git a/app/models/compliance_control_set.rb b/app/models/compliance_control_set.rb
index 41076fefc..c0ea692f2 100644
--- a/app/models/compliance_control_set.rb
+++ b/app/models/compliance_control_set.rb
@@ -1,5 +1,5 @@
class ComplianceControlSet < ActiveRecord::Base
- has_paper_trail
+ has_paper_trail class_name: 'PublicVersion'
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
new file mode 100644
index 000000000..348436aa4
--- /dev/null
+++ b/app/models/concerns/application_days_support.rb
@@ -0,0 +1,107 @@
+module ApplicationDaysSupport
+ extend ActiveSupport::Concern
+
+ MONDAY = 4
+ TUESDAY = 8
+ WEDNESDAY = 16
+ THURSDAY = 32
+ FRIDAY = 64
+ SATURDAY = 128
+ SUNDAY = 256
+ EVERYDAY = MONDAY | TUESDAY | WEDNESDAY | THURSDAY | FRIDAY | SATURDAY | SUNDAY
+
+ 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(', ')
+ end
+
+ def day_by_mask(flag)
+ int_day_types & flag == flag
+ end
+
+ def self.day_by_mask(int_day_types,flag)
+ int_day_types & flag == flag
+ 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
+ end
+ end
+
+ def valid_day? wday
+ valid_days.include?(wday)
+ end
+
+ def self.valid_days(int_day_types)
+ # Build an array with day of calendar week (1-7, Monday is 1).
+ [].tap do |valid_days|
+ valid_days << 1 if day_by_mask(int_day_types,MONDAY)
+ valid_days << 2 if day_by_mask(int_day_types,TUESDAY)
+ valid_days << 3 if day_by_mask(int_day_types,WEDNESDAY)
+ valid_days << 4 if day_by_mask(int_day_types,THURSDAY)
+ valid_days << 5 if day_by_mask(int_day_types,FRIDAY)
+ valid_days << 6 if day_by_mask(int_day_types,SATURDAY)
+ valid_days << 7 if day_by_mask(int_day_types,SUNDAY)
+ end
+ end
+
+ def monday
+ day_by_mask(MONDAY)
+ end
+ def tuesday
+ day_by_mask(TUESDAY)
+ end
+ def wednesday
+ day_by_mask(WEDNESDAY)
+ end
+ def thursday
+ day_by_mask(THURSDAY)
+ end
+ def friday
+ day_by_mask(FRIDAY)
+ end
+ def saturday
+ day_by_mask(SATURDAY)
+ end
+ def sunday
+ day_by_mask(SUNDAY)
+ end
+
+ def set_day(day,flag)
+ if day == '1' || day == true
+ self.int_day_types |= flag
+ else
+ self.int_day_types &= ~flag
+ end
+ shortcuts_update
+ end
+
+ def monday=(day)
+ set_day(day,4)
+ end
+ def tuesday=(day)
+ set_day(day,8)
+ end
+ def wednesday=(day)
+ set_day(day,16)
+ end
+ def thursday=(day)
+ set_day(day,32)
+ end
+ def friday=(day)
+ set_day(day,64)
+ end
+ def saturday=(day)
+ set_day(day,128)
+ end
+ def sunday=(day)
+ set_day(day,256)
+ end
+end
diff --git a/app/models/concerns/checksum_support.rb b/app/models/concerns/checksum_support.rb
index c95e23bcf..92103798e 100644
--- a/app/models/concerns/checksum_support.rb
+++ b/app/models/concerns/checksum_support.rb
@@ -3,18 +3,50 @@ module ChecksumSupport
SEPARATOR = '|'
VALUE_FOR_NIL_ATTRIBUTE = '-'
- included do
+ included do |into|
before_save :set_current_checksum_source, :update_checksum
+ Referential.register_model_with_checksum self
+ into.extend ClassMethods
+ end
+
+ module ClassMethods
+ def has_checksum_children klass, opts={}
+ parent_class = self
+ relation = opts[:relation] || self.model_name.singular
+ klass.after_save do
+ parent = self.send(relation)
+ parent&.update_checksum_without_callbacks!
+ end
+ end
end
def checksum_attributes
self.attributes.values
end
+ def checksum_replace_nil_or_empty_values values
+ # Replace empty array by nil & nil by VALUE_FOR_NIL_ATTRIBUTE
+ values
+ .map { |x| x.present? && x || VALUE_FOR_NIL_ATTRIBUTE }
+ .map do |item|
+ item =
+ if item.kind_of?(Array)
+ checksum_replace_nil_or_empty_values(item)
+ else
+ item
+ end
+ end
+ end
+
def current_checksum_source
- source = self.checksum_attributes.map{ |x| x unless x.try(:empty?) }
- source = source.map{ |x| x || VALUE_FOR_NIL_ATTRIBUTE }
- source.map(&:to_s).join(SEPARATOR)
+ source = checksum_replace_nil_or_empty_values(self.checksum_attributes)
+ source.map{ |item|
+ if item.kind_of?(Array)
+ item.map{ |x| x.kind_of?(Array) ? "(#{x.join(',')})" : x }.join(',')
+ else
+ item
+ end
+ }.join(SEPARATOR)
end
def set_current_checksum_source
@@ -26,4 +58,20 @@ module ChecksumSupport
self.checksum = Digest::SHA256.new.hexdigest(self.checksum_source)
end
end
+
+ def update_checksum!
+ set_current_checksum_source
+ if checksum_source_changed?
+ update checksum: Digest::SHA256.new.hexdigest(checksum_source)
+ end
+ end
+
+ def update_checksum_without_callbacks!
+ set_current_checksum_source
+ _checksum = Digest::SHA256.new.hexdigest(checksum_source)
+ if _checksum != self.checksum
+ self.checksum = _checksum
+ self.class.where(id: self.id).update_all(checksum: _checksum) unless self.new_record?
+ end
+ end
end
diff --git a/app/models/concerns/date_support.rb b/app/models/concerns/date_support.rb
new file mode 100644
index 000000000..5c66cb1a9
--- /dev/null
+++ b/app/models/concerns/date_support.rb
@@ -0,0 +1,83 @@
+module DateSupport
+ extend ActiveSupport::Concern
+
+ included do
+ after_initialize :init_dates
+
+ def init_dates
+ self.dates ||= []
+ end
+
+ ### Calendar::DateValue
+ # Required by coocon
+ def build_date_value
+ Calendar::DateValue.new
+ end
+
+ def date_values
+ @date_values ||= init_date_values
+ end
+
+ def init_date_values
+ if dates
+ dates.each_with_index.map { |d, index| Calendar::DateValue.from_date(index, d) }
+ else
+ []
+ end
+ end
+ private :init_date_values
+
+ validate :validate_date_values
+
+ def validate_date_values
+ date_values_are_valid = date_values.all?(&:valid?)
+
+ date_values.each do |date_value|
+ if date_values.count { |d| d.value == date_value.value } > 1
+ date_value.errors.add(:base, I18n.t('activerecord.errors.models.calendar.attributes.dates.date_in_dates'))
+ date_values_are_valid = false
+ end
+ date_ranges.each do |date_range|
+ if date_range.cover?(date_value.value)
+ excluded_day = self.respond_to?(:valid_day?) && !self.valid_day?(date_value.value.wday)
+ unless excluded_day
+ date_value.errors.add(:base, I18n.t('activerecord.errors.models.calendar.attributes.dates.date_in_date_ranges'))
+ date_values_are_valid = false
+ end
+ end
+ end
+ end
+
+ unless date_values_are_valid
+ errors.add(:date_values, :invalid)
+ end
+ end
+
+ def date_values_attributes=(attributes = {})
+ @date_values = []
+ attributes.each do |index, date_value_attribute|
+ date_value_attribute['value'] = flatten_date_array(date_value_attribute, 'value')
+ date_value = Calendar::DateValue.new(date_value_attribute.merge(id: index))
+ @date_values << date_value unless date_value.marked_for_destruction?
+ end
+
+ dates_will_change!
+ end
+
+ before_validation :fill_dates
+
+ def fill_dates
+ if @date_values
+ self.dates = @date_values.map(&:value).compact.sort
+ end
+ end
+
+ after_save :clear_date_values
+
+ def clear_date_values
+ @date_values = nil
+ end
+
+ private :clear_date_values
+ end
+end
diff --git a/app/models/concerns/min_max_values_validation.rb b/app/models/concerns/min_max_values_validation.rb
index c177e55ca..eff779d81 100644
--- a/app/models/concerns/min_max_values_validation.rb
+++ b/app/models/concerns/min_max_values_validation.rb
@@ -2,12 +2,12 @@ module MinMaxValuesValidation
extend ActiveSupport::Concern
included do
+ validates_presence_of :minimum, :maximum
validate :min_max_values_validation
end
def min_max_values_validation
- return true unless minimum && maximum
- return true unless maximum < minimum
- errors.add(:min_max_values, I18n.t('compliance_controls.min_max_values', min: minimum, max: maximum))
+ return true if (minimum && maximum) && (minimum.to_i < maximum.to_i)
+ errors.add(:minimum, I18n.t('compliance_controls.min_max_values', min: minimum, max: maximum))
end
end
diff --git a/app/models/concerns/objectid_support.rb b/app/models/concerns/objectid_support.rb
index cec36678e..5d1f1a1c2 100644
--- a/app/models/concerns/objectid_support.rb
+++ b/app/models/concerns/objectid_support.rb
@@ -26,5 +26,10 @@ module ObjectidSupport
def objectid_class
get_objectid.try(:class)
end
+
+ def raw_objectid
+ read_attribute(:objectid)
+ end
+
end
end
diff --git a/app/models/concerns/period_support.rb b/app/models/concerns/period_support.rb
new file mode 100644
index 000000000..e17451fe4
--- /dev/null
+++ b/app/models/concerns/period_support.rb
@@ -0,0 +1,80 @@
+module PeriodSupport
+ extend ActiveSupport::Concern
+
+ included do
+ after_initialize :init_date_ranges
+
+ def init_date_ranges
+ self.date_ranges ||= []
+ end
+
+ ### Calendar::Period
+ # Required by coocon
+ def build_period
+ Calendar::Period.new
+ end
+
+ def periods
+ @periods ||= init_periods
+ end
+
+ def init_periods
+ (date_ranges || [])
+ .each_with_index
+ .map( &Calendar::Period.method(:from_range) )
+ end
+ private :init_periods
+
+ validate :validate_periods
+
+ def validate_periods
+ periods_are_valid = periods.all?(&:valid?)
+
+ periods.each do |period|
+ if period.intersect?(periods)
+ period.errors.add(:base, I18n.t('calendars.errors.overlapped_periods'))
+ periods_are_valid = false
+ end
+ end
+
+ unless periods_are_valid
+ errors.add(:periods, :invalid)
+ end
+ end
+
+ def flatten_date_array attributes, key
+ date_int = %w(1 2 3).map {|e| attributes["#{key}(#{e}i)"].to_i }
+ Date.new(*date_int)
+ end
+
+ def periods_attributes=(attributes = {})
+ @periods = []
+ attributes.each do |index, period_attribute|
+ # Convert date_select to date
+ ['begin', 'end'].map do |attr|
+ period_attribute[attr] = flatten_date_array(period_attribute, attr)
+ end
+ period = Calendar::Period.new(period_attribute.merge(id: index))
+ @periods << period unless period.marked_for_destruction?
+ end
+
+ date_ranges_will_change!
+ end
+
+ before_validation :fill_date_ranges
+
+ def fill_date_ranges
+ if @periods
+ self.date_ranges = @periods.map(&:range).compact.sort_by(&:begin)
+ end
+ end
+
+ after_save :clear_periods
+
+ def clear_periods
+ @periods = nil
+ end
+
+ private :clear_periods
+ end
+end
diff --git a/app/models/concerns/timetable_support.rb b/app/models/concerns/timetable_support.rb
new file mode 100644
index 000000000..5242abc33
--- /dev/null
+++ b/app/models/concerns/timetable_support.rb
@@ -0,0 +1,149 @@
+module TimetableSupport
+ extend ActiveSupport::Concern
+
+ def presenter
+ @presenter ||= ::TimeTablePresenter.new( self)
+ end
+
+ def periods_max_date
+ return nil if self.periods.empty?
+
+ min_start = self.periods.map(&:period_start).compact.min
+ max_end = self.periods.map(&:period_end).compact.max
+ result = nil
+
+ if max_end && min_start
+ max_end.downto( min_start) do |date|
+ if self.valid_days.include?(date.cwday) && !self.excluded_date?(date)
+ result = date
+ break
+ end
+ end
+ end
+ result
+ end
+
+ def periods_min_date
+ return nil if self.periods.empty?
+
+ min_start = self.periods.map(&:period_start).compact.min
+ max_end = self.periods.map(&:period_end).compact.max
+ result = nil
+
+ if max_end && min_start
+ min_start.upto(max_end) do |date|
+ if self.valid_days.include?(date.cwday) && !self.excluded_date?(date)
+ result = date
+ break
+ end
+ end
+ end
+ result
+ end
+
+ def bounding_dates
+ bounding_min = self.all_dates.select{|d| d.in_out}.map(&:date).compact.min
+ bounding_max = self.all_dates.select{|d| d.in_out}.map(&:date).compact.max
+
+ unless self.periods.empty?
+ bounding_min = periods_min_date if periods_min_date &&
+ (bounding_min.nil? || (periods_min_date < bounding_min))
+
+ bounding_max = periods_max_date if periods_max_date &&
+ (bounding_max.nil? || (bounding_max < periods_max_date))
+ end
+
+ [bounding_min, bounding_max].compact
+ end
+
+ def month_inspect(date)
+ (date.beginning_of_month..date.end_of_month).map do |d|
+ {
+ day: I18n.l(d, format: '%A'),
+ date: d.to_s,
+ wday: d.wday,
+ wnumber: d.strftime("%W").to_s,
+ mday: d.mday,
+ include_date: include_in_dates?(d),
+ excluded_date: excluded_date?(d)
+ }
+ end
+ end
+
+ def include_in_dates?(day)
+ self.dates.any?{ |d| d.date === day && d.in_out == true }
+ end
+
+ def excluded_date?(day)
+ self.dates.any?{ |d| d.date === day && d.in_out == false }
+ end
+
+ def include_in_overlap_dates?(day)
+ return false if self.excluded_date?(day)
+
+ self.all_dates.any?{ |d| d.date === day} \
+ && self.periods.any?{ |period| period.period_start <= day && day <= period.period_end && valid_days.include?(day.cwday) }
+ end
+
+ def include_in_periods?(day)
+ self.periods.any?{ |period| period.period_start <= day &&
+ day <= period.period_end &&
+ valid_days.include?(day.cwday) &&
+ ! excluded_date?(day) }
+ end
+
+ def state_update_periods state_periods
+ state_periods.each do |item|
+ period = self.find_period_by_id(item['id']) if item['id']
+ next if period && item['deleted'] && self.destroy_period(period)
+ period ||= self.build_period
+
+ period.period_start = Date.parse(item['period_start'])
+ period.period_end = Date.parse(item['period_end'])
+
+ period.save if period.is_a?(ActiveRecord::Base) && period.changed?
+
+ item['id'] = period.id
+ end
+
+ state_periods.delete_if {|item| item['deleted']}
+ end
+
+ def state_update state
+ update_attributes(self.class.state_permited_attributes(state))
+ self.tag_list = state['tags'].collect{|t| t['name']}.join(', ') if state['tags']
+ self.calendar_id = nil if self.respond_to?(:calendar_id) && !state['calendar']
+
+ days = state['day_types'].split(',')
+ Date::DAYNAMES.map(&:underscore).each do |name|
+ prefix = human_attribute_name(name).first(2)
+ send("#{name}=", days.include?(prefix))
+ end
+
+ cmonth = Date.parse(state['current_periode_range'])
+
+ state['current_month'].each do |d|
+ date = Date.parse(d['date'])
+ checked = d['include_date'] || d['excluded_date']
+ in_out = d['include_date'] ? true : false
+
+ date_id = saved_dates.key(date)
+ time_table_date = self.find_date_by_id(date_id) if date_id
+
+ next if !checked && !time_table_date
+ # Destroy date if no longer checked
+ next if !checked && destroy_date(time_table_date)
+
+ # Create new date
+ unless time_table_date
+ time_table_date = self.create_date in_out: in_out, date: date
+ end
+ # Update in_out
+ self.update_in_out time_table_date, in_out
+ end
+
+ self.state_update_periods state['time_table_periods']
+ self.save
+ end
+
+end
diff --git a/app/models/custom_field.rb b/app/models/custom_field.rb
new file mode 100644
index 000000000..774c8b0f6
--- /dev/null
+++ b/app/models/custom_field.rb
@@ -0,0 +1,9 @@
+class CustomField < ActiveRecord::Base
+
+ extend Enumerize
+ belongs_to :workgroup
+ enumerize :field_type, in: %i{list}
+
+ validates :name, uniqueness: {scope: [:resource_type, :workgroup_id]}
+ validates :code, uniqueness: {scope: [:resource_type, :workgroup_id], case_sensitive: false}
+end
diff --git a/app/models/generic_attribute_control/min_max.rb b/app/models/generic_attribute_control/min_max.rb
index ab6f546a7..18873b683 100644
--- a/app/models/generic_attribute_control/min_max.rb
+++ b/app/models/generic_attribute_control/min_max.rb
@@ -1,9 +1,9 @@
module GenericAttributeControl
class MinMax < ComplianceControl
- hstore_accessor :control_attributes, minimum: :integer, maximum: :integer, target: :string
+ store_accessor :control_attributes, :minimum, :maximum, :target
- validates :minimum, numericality: true, allow_nil: true
- validates :maximum, numericality: true, allow_nil: true
+ 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/generic_attribute_control/pattern.rb b/app/models/generic_attribute_control/pattern.rb
index 3a4a55d5c..7fc008e28 100644
--- a/app/models/generic_attribute_control/pattern.rb
+++ b/app/models/generic_attribute_control/pattern.rb
@@ -1,6 +1,6 @@
module GenericAttributeControl
class Pattern < ComplianceControl
- hstore_accessor :control_attributes, pattern: :string, target: :string
+ store_accessor :control_attributes, :pattern, :target
validates :target, presence: true
validates :pattern, presence: true
diff --git a/app/models/generic_attribute_control/uniqueness.rb b/app/models/generic_attribute_control/uniqueness.rb
index f707c944b..82b5c0892 100644
--- a/app/models/generic_attribute_control/uniqueness.rb
+++ b/app/models/generic_attribute_control/uniqueness.rb
@@ -1,6 +1,6 @@
module GenericAttributeControl
class Uniqueness < ComplianceControl
- hstore_accessor :control_attributes, target: :string
+ store_accessor :control_attributes, :target
validates :target, presence: true
diff --git a/app/models/import.rb b/app/models/import.rb
index 19e835986..29aadcd56 100644
--- a/app/models/import.rb
+++ b/app/models/import.rb
@@ -13,13 +13,14 @@ class Import < ActiveRecord::Base
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
- validates_format_of :file, with: %r{\.zip\z}i, message: I18n.t('activerecord.errors.models.import.attributes.file.wrong_file_extension')
before_create :initialize_fields
@@ -43,6 +44,14 @@ class Import < ActiveRecord::Base
%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)
diff --git a/app/models/import_message_export.rb b/app/models/import_message_export.rb
index 88d0f27e2..05f8a2cc7 100644
--- a/app/models/import_message_export.rb
+++ b/app/models/import_message_export.rb
@@ -22,14 +22,14 @@ class ImportMessageExport
end
def column_names
- ["criticity", "message key", "message"]
+ ["criticity", "message key", "message", "file name", "line", "column"]
end
def to_csv(options = {})
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.compliance_check_messages.#{import_message.message_key}", import_message.message_attributes.deep_symbolize_keys) ]
+ 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") ]
end
end
end
diff --git a/app/models/line_control/route.rb b/app/models/line_control/route.rb
index b4b2bd9d8..b6c1f3630 100644
--- a/app/models/line_control/route.rb
+++ b/app/models/line_control/route.rb
@@ -3,6 +3,6 @@ module LineControl
def self.default_code; "3-Line-1" end
- def self.prerequisite; I18n.t("compliance_controls.#{self.name.underscore}.prerequisite") end
+ def prerequisite; I18n.t("compliance_controls.#{self.class.name.underscore}.prerequisite") end
end
end
diff --git a/app/models/merge.rb b/app/models/merge.rb
new file mode 100644
index 000000000..62bf581d6
--- /dev/null
+++ b/app/models/merge.rb
@@ -0,0 +1,435 @@
+class Merge < ActiveRecord::Base
+ extend Enumerize
+
+ belongs_to :workbench
+ validates :workbench, presence: true
+
+ enumerize :status, in: %w[new pending successful failed running], default: :new
+
+ has_array_of :referentials, class_name: 'Referential'
+
+ delegate :output, to: :workbench
+
+ after_commit :merge, :on => :create
+
+ def merge
+ MergeWorker.perform_async(id)
+ end
+
+ def name
+ referentials.first(3).map { |r| r.name.truncate(10) }.join(',')
+ end
+
+ def full_names
+ referentials.map(&:name).to_sentence
+ end
+
+ attr_reader :new
+
+ def merge!
+ update started_at: Time.now, status: :running
+
+ prepare_new
+
+ referentials.each do |referential|
+ merge_referential referential
+ end
+
+ save_current
+ rescue => e
+ Rails.logger.error "Merge failed: #{e} #{e.backtrace.join("\n")}"
+ update status: :failed
+ raise e if Rails.env.test?
+ ensure
+ attributes = { ended_at: Time.now }
+ attributes[:status] = :successful if status == :running
+ update attributes
+ end
+
+ def prepare_new
+ new =
+ if workbench.output.current
+ Rails.logger.debug "Clone current output"
+ Referential.new_from(workbench.output.current, fixme_functional_scope).tap do |clone|
+ clone.inline_clone = true
+ end
+ else
+ Rails.logger.debug "Create a new output"
+ # 'empty' one
+ attributes = {
+ workbench: workbench,
+ organisation: workbench.organisation, # TODO could be workbench.organisation by default
+ }
+ workbench.output.referentials.new attributes
+ end
+
+ new.referential_suite = output
+ new.workbench = workbench
+ new.organisation = workbench.organisation
+ new.slug = "output_#{workbench.id}_#{created_at.to_i}"
+ new.name = I18n.t("merges.referential_name", date: I18n.l(created_at))
+
+ unless new.valid?
+ Rails.logger.error "New referential isn't valid : #{new.errors.inspect}"
+ end
+
+ new.save!
+
+ output.update new: new
+ @new = new
+ end
+
+ def merge_referential(referential)
+ Rails.logger.debug "Merge #{referential.slug}"
+
+ metadata_merger = MetadatasMerger.new new, referential
+ metadata_merger.merge
+
+ new.metadatas.delete metadata_merger.empty_metadatas
+
+ new.save!
+
+ line_periods = LinePeriods.from_metadatas(referential.metadatas)
+
+ new.switch do
+ line_periods.each do |line_id, periods|
+ Rails.logger.debug "Clean data for #{line_id} #{periods.inspect}"
+
+ new.lines.find(line_id).time_tables.find_each do |time_table|
+ time_table.remove_periods! periods
+ unless time_table.empty?
+ time_table.save!
+ else
+ time_table.destroy
+ end
+ end
+ end
+ end
+
+ # let's merge data :)
+
+ # Routes
+
+ # Always the same pattern :
+ # - load models from original Referential
+ # - load associated datas (children, checksum for associated models)
+ # - switch to new Referential
+ # - enumerate loaded models
+ # - skip model if its checksum exists "in the same line"
+ # - prepare attributes for a fresh model
+ # - remove all primary keys
+ # - compute an ObjectId (TODO)
+ # - process children models as nested attributes
+ # - associated other models (by line/checksum)
+ # - save! and next one
+
+ referential_routes = referential.switch do
+ referential.routes.all.to_a
+ end
+
+ referential_routes_checksums = Hash[referential_routes.map { |r| [ r.id, r.checksum ] }]
+
+ referential_stop_points = referential.switch do
+ referential.stop_points.all.to_a
+ end
+
+ referential_stop_points_by_route = referential_stop_points.group_by(&:route_id)
+
+ 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
+ objectid = Chouette::Route.where(objectid: route.objectid).exists? ? nil : route.objectid
+ attributes = route.attributes.merge(
+ id: nil,
+ objectid: objectid,
+ # line_id is the same
+ # all other primary must be changed
+ opposite_route_id: nil #FIXME
+ )
+ new_route = new.routes.build attributes
+
+ route_stop_points = referential_stop_points_by_route[route.id]
+
+ # Stop Points
+ route_stop_points.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
+
+ new_route.save!
+
+ if new_route.checksum != route.checksum
+ raise "Checksum has changed: #{route.inspect} #{new_route.inspect}"
+ end
+ end
+ end
+ end
+
+ # JourneyPatterns
+
+ referential_journey_patterns, referential_journey_patterns_stop_areas_objectids = referential.switch do
+ journey_patterns = referential.journey_patterns.includes(stop_points: :stop_area)
+
+ journey_patterns_stop_areas_objectids = Hash[
+ journey_patterns.map do |journey_pattern|
+ [ journey_pattern.id, journey_pattern.stop_points.map(&:stop_area).map(&:raw_objectid)]
+ end
+ ]
+
+ [journey_patterns, journey_patterns_stop_areas_objectids]
+ end
+
+ referential_journey_patterns_checksums = Hash[referential_journey_patterns.map { |j| [ j.id, j.checksum ] }]
+
+ new.switch do
+ referential_journey_patterns.each do |journey_pattern|
+ # find parent route by checksum
+ # TODO add line_id for security
+ associated_route_checksum = referential_routes_checksums[journey_pattern.route_id]
+ existing_associated_route = new.routes.find_by checksum: associated_route_checksum
+
+ existing_journey_pattern = new.journey_patterns.find_by route_id: existing_associated_route.id, checksum: journey_pattern.checksum
+
+ unless existing_journey_pattern
+ objectid = Chouette::JourneyPattern.where(objectid: journey_pattern.objectid).exists? ? nil : journey_pattern.objectid
+ attributes = journey_pattern.attributes.merge(
+ id: nil,
+ objectid: objectid,
+
+ # all other primary must be changed
+ route_id: existing_associated_route.id,
+
+ departure_stop_point_id: nil, # FIXME
+ arrival_stop_point_id: nil
+ )
+
+ stop_areas_objectids = referential_journey_patterns_stop_areas_objectids[journey_pattern.id]
+
+ stop_points = existing_associated_route.stop_points.joins(:stop_area).where("stop_areas.objectid": stop_areas_objectids).order(:position)
+ if stop_points.count != stop_areas_objectids.count
+ raise "Can't find StopPoints for #{stop_areas_objectids} : #{stop_points.inspect} #{existing_associated_route.stop_points.inspect}"
+ end
+
+ attributes.merge!(stop_points: stop_points)
+
+ 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} "
+ end
+ end
+ end
+ end
+
+ # Vehicle Journeys
+
+ referential_vehicle_journeys = referential.switch do
+ referential.vehicle_journeys.includes(:vehicle_journey_at_stops).all.to_a
+ end
+
+ new.switch do
+ referential_vehicle_journeys.each do |vehicle_journey|
+ # find parent journey pattern by checksum
+ # TODO add line_id for security
+ 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_vehicle_journey = new.vehicle_journeys.find_by journey_pattern_id: existing_associated_journey_pattern.id, checksum: vehicle_journey.checksum
+
+ unless existing_vehicle_journey
+ objectid = Chouette::VehicleJourney.where(objectid: vehicle_journey.objectid).exists? ? nil : vehicle_journey.objectid
+ attributes = vehicle_journey.attributes.merge(
+ id: nil,
+ objectid: objectid,
+
+ # all other primary must be changed
+ route_id: existing_associated_journey_pattern.route_id,
+ journey_pattern_id: existing_associated_journey_pattern.id,
+ )
+ new_vehicle_journey = new.vehicle_journeys.build attributes
+
+ # Create VehicleJourneyAtStops
+
+ vehicle_journey.vehicle_journey_at_stops.each_with_index do |vehicle_journey_at_stop, index|
+ at_stop_attributes = vehicle_journey_at_stop.attributes.merge(
+ id: nil,
+ stop_point_id: existing_associated_journey_pattern.stop_points[index].id
+ )
+ new_vehicle_journey.vehicle_journey_at_stops.build at_stop_attributes
+ 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
+ end
+
+ end
+ end
+
+ # Time Tables
+
+ referential_time_tables_by_id, referential_time_tables_with_lines = referential.switch do
+ 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")
+
+ # 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
+ end
+
+ [ time_tables_by_id, time_tables_by_lines ]
+ end
+
+ new.switch do
+ referential_time_tables_with_lines.each do |line_id, time_tables_properties|
+ # Because TimeTables will be modified according metadata periods
+ # we're loading timetables per line (line is associated to a period list)
+ line = workbench.line_referential.lines.find(line_id)
+
+ time_tables_properties.each do |properties|
+ time_table = referential_time_tables_by_id[properties[:id]]
+
+ # we can't test if TimeTable already exist by checksum
+ # because checksum is modified by intersect_periods!
+
+ attributes = time_table.attributes.merge(
+ id: nil,
+ comment: "Ligne #{line.name} - #{time_table.comment}",
+ calendar_id: nil
+ )
+ candidate_time_table = new.time_tables.build attributes
+
+ time_table.dates.each do |date|
+ date_attributes = date.attributes.merge(
+ id: nil,
+ time_table_id: nil
+ )
+ candidate_time_table.dates.build date_attributes
+ end
+ time_table.periods.each do |period|
+ period_attributes = period.attributes.merge(
+ id: nil,
+ time_table_id: nil
+ )
+ candidate_time_table.periods.build period_attributes
+ end
+
+ candidate_time_table.intersect_periods! line_periods.periods(line_id)
+
+ # FIXME
+ candidate_time_table.set_current_checksum_source
+ candidate_time_table.update_checksum
+
+ # after intersect_periods!, the checksum is the expected one
+ # we can search an existing TimeTable
+
+ existing_time_table = line.time_tables.find_by checksum: candidate_time_table.checksum
+
+ unless existing_time_table
+ objectid = Chouette::TimeTable.where(objectid: time_table.objectid).exists? ? nil : time_table.objectid
+ candidate_time_table.objectid = objectid
+
+ candidate_time_table.save!
+
+ # Checksum is changed by #intersect_periods
+ # if new_time_table.checksum != time_table.checksum
+ # raise "Checksum has changed: #{time_table.checksum_source} #{new_time_table.checksum_source}"
+ # end
+
+ existing_time_table = candidate_time_table
+ end
+
+ # associate VehicleJourney
+
+ associated_vehicle_journey = line.vehicle_journeys.find_by!(checksum: properties[:vehicle_journey_checksum])
+ associated_vehicle_journey.time_tables << existing_time_table
+ end
+ end
+ end
+ end
+
+ def save_current
+ output.update current: new, new: nil
+ output.current.update referential_suite: output
+
+ referentials.update_all merged_at: created_at, archived_at: created_at
+ end
+
+ def fixme_functional_scope
+ if attribute = workbench.organisation.sso_attributes.try(:[], "functional_scope")
+ JSON.parse(attribute)
+ end
+ end
+
+ def child_change
+
+ end
+
+ class MetadatasMerger
+
+ attr_reader :merge_metadatas, :referential
+ def initialize(merge_referential, referential)
+ @merge_metadatas = merge_referential.metadatas
+ @referential = referential
+ end
+
+ delegate :metadatas, to: :referential, prefix: :referential
+
+ def merge
+ referential_metadatas.each do |metadata|
+ merge_one metadata
+ end
+ end
+
+ def merged_line_metadatas(line_id)
+ merge_metadatas.select do |m|
+ m.line_ids.include? line_id
+ end
+ end
+
+ def merge_one(metadata)
+ metadata.line_ids.each do |line_id|
+ line_metadatas = merged_line_metadatas(line_id)
+
+ metadata.periodes.each do |period|
+ line_metadatas.each do |m|
+ m.periodes = m.periodes.map do |existing_period|
+ existing_period.remove period
+ end.flatten
+ end
+
+ attributes = {
+ line_ids: [line_id],
+ periodes: [period],
+ referential_source_id: referential.id,
+ created_at: metadata.created_at # TODO check required dates
+ }
+
+ # line_metadatas should not contain conflicted metadatas
+ merge_metadatas << ReferentialMetadata.new(attributes)
+ end
+ end
+ end
+
+ def empty_metadatas
+ merge_metadatas.select { |m| m.periodes.empty? }
+ end
+
+
+ end
+
+end
diff --git a/app/models/organisation.rb b/app/models/organisation.rb
index f6fba2d67..da7d1fcf3 100644
--- a/app/models/organisation.rb
+++ b/app/models/organisation.rb
@@ -1,3 +1,4 @@
+# coding: utf-8
class Organisation < ActiveRecord::Base
include DataFormatEnumerations
@@ -18,36 +19,39 @@ class Organisation < ActiveRecord::Base
validates_presence_of :name
validates_uniqueness_of :code
- def self.portail_api_request
- conf = Rails.application.config.try(:stif_portail_api)
- raise 'Rails.application.config.stif_portail_api configuration is not defined' unless conf
+ class << self
- HTTPService.get_json_resource(
- host: conf[:url],
- path: '/api/v1/organizations',
- token: conf[:key])
- end
+ def portail_api_request
+ conf = Rails.application.config.try(:stif_portail_api)
+ raise 'Rails.application.config.stif_portail_api configuration is not defined' unless conf
+
+ HTTPService.get_json_resource(
+ host: conf[:url],
+ path: '/api/v1/organizations',
+ token: conf[:key])
+ end
- def self.sync_update code, name, scope
- org = Organisation.find_or_initialize_by(code: code)
- if scope
- org.sso_attributes ||= {}
- if org.sso_attributes['functional_scope'] != scope
- org.sso_attributes['functional_scope'] = scope
- # FIXME see #1941
- org.sso_attributes_will_change!
+ def sync_update code, name, scope
+ org = Organisation.find_or_initialize_by(code: code)
+ if scope
+ org.sso_attributes ||= {}
+ if org.sso_attributes['functional_scope'] != scope
+ org.sso_attributes['functional_scope'] = scope
+ # FIXME see #1941
+ org.sso_attributes_will_change!
+ end
end
+ org.name = name
+ org.synced_at = Time.now
+ org.save
+ org
end
- org.name = name
- org.synced_at = Time.now
- org.save
- org
- end
- def self.portail_sync
- self.portail_api_request.each do |el|
- org = self.sync_update el['code'], el['name'], el['functional_scope']
- puts "✓ Organisation #{org.name} has been updated" unless Rails.env.test?
+ def portail_sync
+ portail_api_request.each do |el|
+ org = self.sync_update el['code'], el['name'], el['functional_scope']
+ puts "✓ Organisation #{org.name} has been updated" unless Rails.env.test?
+ end
end
end
@@ -64,4 +68,16 @@ class Organisation < ActiveRecord::Base
raise ActiveRecord::RecordNotFound
end
+ def functional_scope
+ JSON.parse( (sso_attributes || {}).fetch('functional_scope', '[]') )
+ end
+
+ def lines_set
+ STIF::CodifligneLineId.lines_set_from_functional_scope( functional_scope )
+ end
+
+ def has_feature?(feature)
+ features && features.include?(feature.to_s)
+ end
+
end
diff --git a/app/models/public_version.rb b/app/models/public_version.rb
new file mode 100644
index 000000000..4dbf6ce27
--- /dev/null
+++ b/app/models/public_version.rb
@@ -0,0 +1,4 @@
+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 851a33653..509e0412f 100644
--- a/app/models/referential.rb
+++ b/app/models/referential.rb
@@ -13,17 +13,17 @@ class Referential < ActiveRecord::Base
validates_uniqueness_of :slug
- validates_format_of :slug, :with => %r{\A[a-z][0-9a-z_]+\Z}
- validates_format_of :prefix, :with => %r{\A[0-9a-zA-Z_]+\Z}
- validates_format_of :upper_corner, :with => %r{\A-?[0-9]+\.?[0-9]*\,-?[0-9]+\.?[0-9]*\Z}
- validates_format_of :lower_corner, :with => %r{\A-?[0-9]+\.?[0-9]*\,-?[0-9]+\.?[0-9]*\Z}
+ validates_format_of :slug, with: %r{\A[a-z][0-9a-z_]+\Z}
+ validates_format_of :prefix, with: %r{\A[0-9a-zA-Z_]+\Z}
+ validates_format_of :upper_corner, with: %r{\A-?[0-9]+\.?[0-9]*\,-?[0-9]+\.?[0-9]*\Z}
+ validates_format_of :lower_corner, with: %r{\A-?[0-9]+\.?[0-9]*\,-?[0-9]+\.?[0-9]*\Z}
validate :slug_excluded_values
attr_accessor :upper_corner
attr_accessor :lower_corner
has_one :user
- has_many :api_keys, :class_name => 'Api::V1::ApiKey', :dependent => :destroy
+ has_many :api_keys, class_name: 'Api::V1::ApiKey', dependent: :destroy
belongs_to :organisation
validates_presence_of :organisation
@@ -61,6 +61,60 @@ class Referential < ActiveRecord::Base
scope :include_metadatas_lines, ->(line_ids) { where('referential_metadata.line_ids && ARRAY[?]::bigint[]', line_ids) }
scope :order_by_validity_period, ->(dir) { joins(:metadatas).order("unnest(periodes) #{dir}") }
scope :order_by_lines, ->(dir) { joins(:metadatas).group("referentials.id").order("sum(array_length(referential_metadata.line_ids,1)) #{dir}") }
+ scope :not_in_referential_suite, -> { where referential_suite_id: nil }
+ scope :blocked, -> { where('ready = ? AND created_at < ?', false, 4.hours.ago) }
+
+ def save_with_table_lock_timeout(options = {})
+ save_without_table_lock_timeout(options)
+ rescue ActiveRecord::StatementInvalid => e
+ if e.message.include?('PG::LockNotAvailable')
+ raise TableLockTimeoutError.new(e)
+ else
+ raise
+ end
+ end
+
+ 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
+
+ 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
+ end
+ end
+ end
+
+ def self.register_model_with_checksum klass
+ @_models_with_checksum ||= []
+ @_models_with_checksum << klass
+ end
+
+ def self.models_with_checksum
+ @_models_with_checksum || []
+ end
def lines
if metadatas.blank?
@@ -79,7 +133,7 @@ class Referential < ActiveRecord::Base
errors.add(:slug,I18n.t("referentials.errors.public_excluded"))
end
if slug == self.class.connection_config[:username]
- errors.add(:slug,I18n.t("referentials.errors.user_excluded", :user => slug))
+ errors.add(:slug,I18n.t("referentials.errors.user_excluded", user: slug))
end
end
end
@@ -92,8 +146,12 @@ class Referential < ActiveRecord::Base
self.class.human_attribute_name(*args)
end
- def stop_areas
- Chouette::StopArea.all
+ def full_name
+ if in_referential_suite?
+ name
+ else
+ "#{self.class.model_name.human.capitalize} #{name}"
+ end
end
def access_points
@@ -128,6 +186,26 @@ class Referential < ActiveRecord::Base
Chouette::RoutingConstraintZone.all
end
+ def purchase_windows
+ Chouette::PurchaseWindow.all
+ end
+
+ def routes
+ Chouette::Route.all
+ end
+
+ def journey_patterns
+ Chouette::JourneyPattern.all
+ end
+
+ def stop_points
+ Chouette::StopPoint.all
+ end
+
+ def compliance_check_sets
+ ComplianceCheckSet.all
+ end
+
before_validation :define_default_attributes
def define_default_attributes
@@ -135,14 +213,26 @@ class Referential < ActiveRecord::Base
self.objectid_format ||= workbench.objectid_format if workbench
end
- def switch
+ def switch(&block)
raise "Referential not created" if new_record?
- Apartment::Tenant.switch!(slug)
- self
+
+ unless block_given?
+ Rails.logger.debug "Referential switch to #{slug}"
+ Apartment::Tenant.switch! slug
+ self
+ else
+ result = nil
+ Apartment::Tenant.switch slug do
+ Rails.logger.debug "Referential switch to #{slug}"
+ result = yield
+ end
+ Rails.logger.debug "Referential back"
+ result
+ end
end
def self.new_from(from, functional_scope)
- Referential.new(
+ Referential.new(
name: I18n.t("activerecord.copy", name: from.name),
slug: "#{from.slug}_clone",
prefix: from.prefix,
@@ -192,15 +282,29 @@ class Referential < ActiveRecord::Base
projection_type || ""
end
- before_validation :assign_line_and_stop_area_referential, :on => :create, if: :workbench
- before_validation :assign_slug, :on => :create
- before_validation :assign_prefix, :on => :create
+ before_validation :assign_line_and_stop_area_referential, on: :create, if: :workbench
+ before_validation :assign_slug, on: :create
+ before_validation :assign_prefix, on: :create
+
+ # Lock the `referentials` table to prevent duplicate referentials from being
+ # created simultaneously in separate transactions. This must be the last hook
+ # to minimise the duration of the lock.
+ before_save :lock_table, on: [:create, :update]
+
before_create :create_schema
after_create :clone_schema, if: :created_from
before_destroy :destroy_schema
before_destroy :destroy_jobs
+ def referential_read_only?
+ in_referential_suite? || archived?
+ end
+
+ def in_referential_suite?
+ referential_suite_id.present?
+ end
+
def in_workbench?
workbench_id.present?
end
@@ -264,7 +368,7 @@ class Referential < ActiveRecord::Base
query = "select distinct(public.referential_metadata.referential_id) FROM public.referential_metadata, unnest(line_ids) line, LATERAL unnest(periodes) period
WHERE public.referential_metadata.referential_id
- IN (SELECT public.referentials.id FROM public.referentials WHERE referentials.workbench_id = #{workbench_id} and referentials.archived_at is null #{not_myself})
+ IN (SELECT public.referentials.id FROM public.referentials WHERE referentials.workbench_id = #{workbench_id} and referentials.archived_at is null and referentials.referential_suite_id is null #{not_myself})
AND line in (#{line_ids.join(',')}) and (#{periods_query});"
self.class.connection.select_values(query).map(&:to_i)
@@ -274,22 +378,45 @@ class Referential < ActiveRecord::Base
overlapped_referential_ids.present?
end
- validate :detect_overlapped_referentials
+ validate :detect_overlapped_referentials, unless: :in_referential_suite?
def detect_overlapped_referentials
self.class.where(id: overlapped_referential_ids).each do |referential|
+ Rails.logger.info "Referential #{referential.id} #{referential.metadatas.inspect} overlaps #{metadatas.inspect}"
errors.add :metadatas, I18n.t("referentials.errors.overlapped_referential", :referential => referential.name)
end
end
+
+ attr_accessor :inline_clone
def clone_schema
- ReferentialCloning.create(source_referential: created_from, target_referential: self)
+ cloning = ReferentialCloning.new source_referential: created_from, target_referential: self
+
+ if inline_clone
+ cloning.clone!
+ else
+ cloning.save!
+ end
end
def create_schema
unless created_from
- Apartment::Tenant.create slug
- Rails.logger.error( "Schema migrations count for Referential #{slug} " + Referential.connection.select_value("select count(*) from #{slug}.schema_migrations;").to_s )
+ report = Benchmark.measure do
+ Apartment::Tenant.create slug
+ end
+
+ check_migration_count(report)
+ end
+ end
+
+ def check_migration_count(report)
+ Rails.logger.info("Schema create benchmark: '#{slug}'\t#{report}")
+ Rails.logger.info("Schema migrations count for Referential #{slug}: #{migration_count || '-'}")
+ end
+
+ def migration_count
+ if self.class.connection.table_exists?("#{slug}.schema_migrations")
+ self.class.connection.select_value("select count(*) from #{slug}.schema_migrations;")
end
end
@@ -373,4 +500,25 @@ class Referential < ActiveRecord::Base
not metadatas_overlap?
end
+ def merged?
+ merged_at.present?
+ end
+
+ def self.not_merged
+ where merged_at: nil
+ end
+
+ def self.mergeable
+ ready.not_merged.not_in_referential_suite
+ end
+
+ private
+
+ def lock_table
+ # No explicit unlock is needed as it will be released at the end of the
+ # transaction.
+ ActiveRecord::Base.connection.execute(
+ 'LOCK public.referentials IN ACCESS EXCLUSIVE MODE'
+ )
+ end
end
diff --git a/app/models/referential_cloning.rb b/app/models/referential_cloning.rb
index 5bf283814..d4b74bd52 100644
--- a/app/models/referential_cloning.rb
+++ b/app/models/referential_cloning.rb
@@ -2,14 +2,33 @@ class ReferentialCloning < ActiveRecord::Base
include AASM
belongs_to :source_referential, class_name: 'Referential'
belongs_to :target_referential, class_name: 'Referential'
- after_commit :perform_clone, :on => :create
+ after_commit :clone, on: :create
- private
- def perform_clone
+ def clone
ReferentialCloningWorker.perform_async(id)
- # ReferentialCloningWorker.new.perform(id)
end
+ def clone_with_status!
+ run!
+ clone!
+ successful!
+ rescue Exception => e
+ Rails.logger.error "Clone failed : #{e}"
+ Rails.logger.error e.backtrace.join('\n')
+ failed!
+ end
+
+ def clone!
+ report = Benchmark.measure do
+ AF83::SchemaCloner
+ .new(source_referential.slug, target_referential.slug)
+ .clone_schema
+ end
+ target_referential.check_migration_count(report)
+ end
+
+ private
+
aasm column: :status do
state :new, :initial => true
state :pending
diff --git a/app/models/referential_suite.rb b/app/models/referential_suite.rb
index 93c2c3f36..4f825628c 100644
--- a/app/models/referential_suite.rb
+++ b/app/models/referential_suite.rb
@@ -1,7 +1,7 @@
class ReferentialSuite < ActiveRecord::Base
belongs_to :new, class_name: 'Referential'
validate def validate_consistent_new
- return true if new_id.nil?
+ return true if new_id.nil? || new.nil?
return true if new.referential_suite_id == id
errors.add(:inconsistent_new,
I18n.t('referential_suites.errors.inconsistent_new', name: new.name))
@@ -9,11 +9,11 @@ class ReferentialSuite < ActiveRecord::Base
belongs_to :current, class_name: 'Referential'
validate def validate_consistent_current
- return true if current_id.nil?
+ return true if current_id.nil? || current.nil?
return true if current.referential_suite_id == id
errors.add(:inconsistent_current,
I18n.t('referential_suites.errors.inconsistent_current', name: current.name))
end
- has_many :referentials
+ has_many :referentials, -> { order "created_at desc" }
end
diff --git a/app/models/route_control/opposite_route.rb b/app/models/route_control/opposite_route.rb
index d5616ca6f..e0e9572ce 100644
--- a/app/models/route_control/opposite_route.rb
+++ b/app/models/route_control/opposite_route.rb
@@ -4,6 +4,6 @@ module RouteControl
def self.default_code; "3-Route-2" end
- def self.prerequisite; I18n.t("compliance_controls.#{self.name.underscore}.prerequisite") end
+ def prerequisite; I18n.t("compliance_controls.#{self.class.name.underscore}.prerequisite") end
end
end
diff --git a/app/models/route_control/opposite_route_terminus.rb b/app/models/route_control/opposite_route_terminus.rb
index 24c557734..e70d2c702 100644
--- a/app/models/route_control/opposite_route_terminus.rb
+++ b/app/models/route_control/opposite_route_terminus.rb
@@ -3,6 +3,6 @@ module RouteControl
def self.default_code; "3-Route-5" end
- def self.prerequisite; I18n.t("compliance_controls.#{self.name.underscore}.prerequisite") end
+ def prerequisite; I18n.t("compliance_controls.#{self.class.name.underscore}.prerequisite") end
end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 37d35209a..1342f60ed 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -36,7 +36,7 @@ class User < ActiveRecord::Base
self.name = extra[:full_name]
self.email = extra[:email]
self.organisation = Organisation.sync_update extra[:organisation_code], extra[:organisation_name], extra[:functional_scope]
- self.permissions = Stif::PermissionTranslator.translate(extra[:permissions])
+ self.permissions = Stif::PermissionTranslator.translate(extra[:permissions], self.organisation)
end
def self.portail_api_request
diff --git a/app/models/vehicle_journey_control/delta.rb b/app/models/vehicle_journey_control/delta.rb
index 1f3a4d492..737b7d78c 100644
--- a/app/models/vehicle_journey_control/delta.rb
+++ b/app/models/vehicle_journey_control/delta.rb
@@ -1,9 +1,10 @@
module VehicleJourneyControl
class Delta < ComplianceControl
- hstore_accessor :control_attributes, maximum: :integer
+ store_accessor :control_attributes, :maximum
- validates :maximum, numericality: true, allow_nil: true
+ validates_numericality_of :maximum, allow_nil: true, greater_than_or_equal_to: 0
+ validates_presence_of :maximum
def self.default_code; "3-VehicleJourney-3" end
end
diff --git a/app/models/vehicle_journey_control/speed.rb b/app/models/vehicle_journey_control/speed.rb
index be9f838e4..e5e331b50 100644
--- a/app/models/vehicle_journey_control/speed.rb
+++ b/app/models/vehicle_journey_control/speed.rb
@@ -1,9 +1,9 @@
module VehicleJourneyControl
class Speed < ComplianceControl
- hstore_accessor :control_attributes, minimum: :integer, maximum: :integer
+ store_accessor :control_attributes, :minimum, :maximum
- validates :minimum, numericality: true, allow_nil: true
- validates :maximum, numericality: true, allow_nil: true
+ 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_control/waiting_time.rb b/app/models/vehicle_journey_control/waiting_time.rb
index 68fccb5c1..89a18a5d9 100644
--- a/app/models/vehicle_journey_control/waiting_time.rb
+++ b/app/models/vehicle_journey_control/waiting_time.rb
@@ -1,8 +1,9 @@
module VehicleJourneyControl
class WaitingTime < ComplianceControl
- hstore_accessor :control_attributes, maximum: :integer
+ store_accessor :control_attributes, :maximum
- validates :maximum, numericality: true, allow_nil: true
+ validates_numericality_of :maximum, allow_nil: true, greater_than_or_equal_to: 0
+ validates_presence_of :maximum
def self.default_code; "3-VehicleJourney-1" end
end
diff --git a/app/models/workbench.rb b/app/models/workbench.rb
index e36589210..b80fa64ac 100644
--- a/app/models/workbench.rb
+++ b/app/models/workbench.rb
@@ -4,6 +4,7 @@ class Workbench < ActiveRecord::Base
belongs_to :line_referential
belongs_to :stop_area_referential
belongs_to :output, class_name: 'ReferentialSuite'
+ belongs_to :workgroup
has_many :lines, -> (workbench) { Stif::MyWorkbenchScopes.new(workbench).line_scope(self) }, through: :line_referential
has_many :networks, through: :line_referential
@@ -14,6 +15,7 @@ class Workbench < ActiveRecord::Base
has_many :workbench_imports
has_many :compliance_check_sets
has_many :compliance_control_sets
+ has_many :merges
validates :name, presence: true
validates :organisation, presence: true
@@ -29,7 +31,12 @@ class Workbench < ActiveRecord::Base
if line_ids.empty?
Referential.none
else
- Referential.joins(:metadatas).where(['referential_metadata.line_ids && ARRAY[?]::bigint[]', line_ids]).ready
+ workgroup
+ .referentials
+ .joins(:metadatas)
+ .where(['referential_metadata.line_ids && ARRAY[?]::bigint[]', line_ids])
+ .ready
+ .not_in_referential_suite
end
end
diff --git a/app/models/workgroup.rb b/app/models/workgroup.rb
new file mode 100644
index 000000000..3af20ae23
--- /dev/null
+++ b/app/models/workgroup.rb
@@ -0,0 +1,20 @@
+class Workgroup < ActiveRecord::Base
+ belongs_to :line_referential
+ belongs_to :stop_area_referential
+
+ has_many :workbenches
+ has_many :calendars
+ has_many :organisations, through: :workbenches
+ has_many :referentials, through: :workbenches
+
+ validates_uniqueness_of :name
+
+ validates_presence_of :line_referential_id
+ validates_presence_of :stop_area_referential_id
+
+ has_many :custom_fields
+
+ def custom_fields_definitions
+ Hash[*custom_fields.map{|cf| [cf.code, cf]}.flatten]
+ end
+end