# coding: utf-8 class Referential < ApplicationModel include DataFormatEnumerations include ObjectidFormatterSupport STATES = %i(pending active failed archived) validates_presence_of :name validates_presence_of :slug validates_presence_of :prefix # Fixme #3657 # validates_presence_of :time_zone # validates_presence_of :upper_corner # validates_presence_of :lower_corner 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} 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 :import_resources, class_name: 'Import::Resource', dependent: :destroy belongs_to :organisation validates_presence_of :organisation validate def validate_consistent_organisation return true if workbench_id.nil? ids = [workbench.organisation_id, organisation_id] return true if ids.first == ids.last errors.add(:inconsistent_organisation, I18n.t('referentials.errors.inconsistent_organisation', indirect_name: workbench.organisation.name, direct_name: organisation.name)) end, if: :organisation belongs_to :line_referential validates_presence_of :line_referential belongs_to :created_from, class_name: 'Referential' has_many :associated_lines, through: :line_referential, source: :lines has_many :companies, through: :line_referential has_many :group_of_lines, through: :line_referential has_many :networks, through: :line_referential has_many :metadatas, class_name: "ReferentialMetadata", inverse_of: :referential, dependent: :destroy accepts_nested_attributes_for :metadatas belongs_to :stop_area_referential validates_presence_of :stop_area_referential has_many :stop_areas, through: :stop_area_referential belongs_to :workbench delegate :workgroup, to: :workbench, allow_nil: true belongs_to :referential_suite scope :pending, -> { where(ready: false, failed_at: nil, archived_at: nil) } scope :active, -> { where(ready: true, failed_at: nil, archived_at: nil) } scope :failed, -> { where.not(failed_at: nil) } scope :archived, -> { where.not(archived_at: nil) } scope :ready, -> { where(ready: true) } scope :in_periode, ->(periode) { where(id: referential_ids_in_periode(periode)) } 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 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 def self.register_model_with_checksum klass @_models_with_checksum ||= [] @_models_with_checksum << klass end def self.models_with_checksum @_models_with_checksum || [] end OPERATIONS = [Import::Netex, Import::Gtfs] def last_operation operations = [] Referential::OPERATIONS.each do |klass| operations << klass.for_referential(self).limit(1).select("'#{klass.name}' as kind, id, created_at").order('created_at DESC').to_sql end sql = "SELECT * FROM ((#{operations.join(') UNION (')})) AS subquery ORDER BY subquery.created_at DESC" res = ActiveRecord::Base.connection.execute(sql).first if res res["kind"].constantize.find(res["id"]) end end def lines if metadatas.blank? workbench ? workbench.lines : associated_lines else metadatas_lines end end def slug_excluded_values if ! slug.nil? if slug.start_with? "pg_" errors.add(:slug,I18n.t("referentials.errors.pg_excluded")) end if slug == 'public' 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)) end end end def viewbox_left_top_right_bottom [ lower_corner.lng, upper_corner.lat, upper_corner.lng, lower_corner.lat ].join(',') end def human_attribute_name(*args) self.class.human_attribute_name(*args) end def full_name if in_referential_suite? name else "#{self.class.model_name.human.capitalize} #{name}" end end def access_points Chouette::AccessPoint.all end def access_links Chouette::AccessLink.all end def time_tables Chouette::TimeTable.all end def time_table_dates Chouette::TimeTableDate.all end def timebands Chouette::Timeband.all end def connection_links Chouette::ConnectionLink.all end def vehicle_journeys Chouette::VehicleJourney.all end def vehicle_journey_frequencies Chouette::VehicleJourneyFrequency.all end def vehicle_journey_at_stops Chouette::VehicleJourneyAtStop.all end def routing_constraint_zones 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 self.time_zone ||= Time.zone.name self.objectid_format ||= workbench.objectid_format if workbench end def switch(&block) raise "Referential not created" if new_record? 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, organisation) Referential.new( name: I18n.t("activerecord.copy", name: from.name), prefix: from.prefix, time_zone: from.time_zone, bounds: from.bounds, line_referential: from.line_referential, stop_area_referential: from.stop_area_referential, created_from: from, objectid_format: from.objectid_format, metadatas: from.metadatas.map { |m| ReferentialMetadata.new_from(m, organisation) }, ready: false ) end def self.available_srids [ [ "RGF 93 Lambert 93 (2154)", 2154 ], [ "RGF93 CC42 (zone 1) (3942)", 3942 ], [ "RGF93 CC43 (zone 2) (3943)", 3943 ], [ "RGF93 CC44 (zone 3) (3944)", 3944 ], [ "RGF93 CC45 (zone 4) (3945)", 3945 ], [ "RGF93 CC46 (zone 5) (3946)", 3946 ], [ "RGF93 CC47 (zone 6) (3947)", 3947 ], [ "RGF93 CC48 (zone 7) (3948)", 3948 ], [ "RGF93 CC49 (zone 8) (3949)", 3949 ], [ "RGF93 CC50 (zone 9) (3950)", 3950 ], [ "NTF Lambert Zone 1 Nord (27561)", 27561 ], [ "NTF Lambert Zone 2 Centre (27562)", 27562 ], [ "NTF Lambert Zone 3 Sud (27563)", 27563 ], [ "NTF Lambert Zone 4 Corse (27564)", 27564 ], [ "NTF Lambert 1 Carto (27571)", 27571 ], [ "NTF Lambert 2 Carto (27572)", 27572 ], [ "NTF Lambert 3 Carto (27573)", 27573 ], [ "NTF Lambert 4 Carto (27574)", 27574 ] , [ "Réunion RGR92 - UTM 40S (2975)", 2975 ], [ "Antilles Françaises RRAF1991 - UTM 20N - IGN (4559)", 4559 ], [ "Guyane RGFG95 - UTM 22N (2972)", 2972 ], [ "Guyane RGFG95 - UTM 21N (3312)", 3312 ] ] end def projection_type_label self.class.available_srids.each do |a| if a.last.to_s == projection_type return a.first.split('(').first.rstrip end end 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 # 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 after_create :active!, unless: :created_from before_destroy :destroy_schema before_destroy :destroy_jobs def referential_read_only? !ready? || in_referential_suite? || archived? end def in_referential_suite? referential_suite_id.present? end def in_workbench? workbench_id.present? end def init_metadatas(attributes = {}) if metadatas.blank? date_range = attributes.delete :default_date_range metadata = metadatas.build attributes metadata.periodes = [date_range] if date_range end end def metadatas_period query = "select min(lower), max(upper) from (select lower(unnest(periodes)) as lower, upper(unnest(periodes)) as upper from public.referential_metadata where public.referential_metadata.referential_id = #{id}) bounds;" row = self.class.connection.select_one(query) lower, upper = row["min"], row["max"] if lower and upper Range.new(Date.parse(lower), Date.parse(upper)-1) end end alias_method :validity_period, :metadatas_period def metadatas_lines if metadatas.present? associated_lines.where(id: metadatas.pluck(:line_ids).flatten) else Chouette::Line.none end end def self.referential_ids_in_periode(range) subquery = "SELECT DISTINCT(public.referential_metadata.referential_id) FROM public.referential_metadata, LATERAL unnest(periodes) period " subquery << "WHERE period && '#{range_to_string(range)}'" query = "SELECT * FROM public.referentials WHERE referentials.id IN (#{subquery})" self.connection.select_values(query).map(&:to_i) end # Copied from Rails 4.1 activerecord/lib/active_record/connection_adapters/postgresql/cast.rb # TODO: Relace with the appropriate Rais 4.2 / 5.x helper if one is found. def self.range_to_string(object) from = object.begin.respond_to?(:infinite?) && object.begin.infinite? ? '' : object.begin to = object.end.respond_to?(:infinite?) && object.end.infinite? ? '' : object.end "[#{from},#{to}#{object.exclude_end? ? ')' : ']'}" end def overlapped_referential_ids return [] unless metadatas.present? line_ids = metadatas.first.line_ids periodes = metadatas.first.periodes return [] unless line_ids.present? && periodes.present? not_myself = "and referential_id != #{id}" if persisted? periods_query = periodes.map do |periode| "period && '[#{periode.begin},#{periode.end})'" end.join(" OR ") 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 and referentials.referential_suite_id is null #{not_myself} AND referentials.failed_at IS NULL) AND line in (#{line_ids.join(',')}) and (#{periods_query});" self.class.connection.select_values(query).map(&:to_i) end def metadatas_overlap? overlapped_referential_ids.present? end 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 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 report = Benchmark.measure do Apartment::Tenant.create slug end check_migration_count(report) # raise "Wrong migration count: #{migration_count}" if migration_count < 300 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 raw_value = if self.class.connection.table_exists?("#{slug}.schema_migrations") self.class.connection.select_value("select count(*) from #{slug}.schema_migrations;") end raw_value.to_i end def assign_slug(time_reference = Time) self.slug ||= begin prefix = name.parameterize.gsub('-','_').gsub(/[^a-zA-Z_]/,'').gsub(/^_/,'')[0..12] prefix = "referential" if prefix.blank? "#{prefix}_#{time_reference.now.to_i}" end if name end def assign_prefix self.prefix = organisation.name.parameterize.gsub('-', '_') if organisation end def assign_line_and_stop_area_referential self.line_referential = workbench.line_referential self.stop_area_referential = workbench.stop_area_referential end def destroy_schema return unless ActiveRecord::Base.connection.schema_names.include?(slug) Apartment::Tenant.drop slug end def destroy_jobs #Ievkit.delete_jobs(slug) true end def upper_corner envelope.upper_corner end def upper_corner=(upper_corner) if String === upper_corner upper_corner = (upper_corner.blank? ? nil : GeoRuby::SimpleFeatures::Point::from_lat_lng(Geokit::LatLng.normalize(upper_corner), 4326)) end envelope.tap do |envelope| envelope.upper_corner = upper_corner self.bounds = envelope.to_polygon.as_ewkt end end def lower_corner envelope.lower_corner end def lower_corner=(lower_corner) if String === lower_corner lower_corner = (lower_corner.blank? ? nil : GeoRuby::SimpleFeatures::Point::from_lat_lng(Geokit::LatLng.normalize(lower_corner), 4326)) end envelope.tap do |envelope| envelope.lower_corner = lower_corner self.bounds = envelope.to_polygon.as_ewkt end end def default_bounds GeoRuby::SimpleFeatures::Envelope.from_coordinates( [ [-5.2, 42.25], [8.23, 51.1] ] ).to_polygon.as_ewkt end def envelope bounds = read_attribute(:bounds) GeoRuby::SimpleFeatures::Geometry.from_ewkt(bounds.present? ? bounds : default_bounds ).envelope end # Archive def archived? archived_at != nil end def archive! # self.archived = true touch :archived_at end def unarchive! return false unless can_unarchive? # self.archived = false update_column :archived_at, nil end def can_unarchive? 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 ### STATE def state return :failed if failed_at.present? return :archived if archived_at.present? ready? ? :active : :pending end def pending! update ready: false, failed_at: nil, archived_at: nil end def failed! update ready: false, failed_at: Time.now, archived_at: nil end def active! update ready: true, failed_at: nil, archived_at: nil end def archived! update failed_at: nil, archived_at: Time.now end STATES.each do |s| define_method "#{s}?" do state == s end end def pending_while vals = attributes.slice(*%w(ready archived_at failed_at)) pending! begin yield ensure update vals end 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