require Rails.root.join("lib/ISO3166/country") # so the class definition has access to extensions class Geolocation class ArrayType < ActiveRecord::Type::Value # Since an array can be modified directly, this module flags any field using this type as internally # mutable (and provides different implementations of methods used internally by ActiveRecord to mark # fields as changed/unchanged) include ActiveModel::Type::Helpers::Mutable def cast(codes) return [] if codes.blank? # Allows setting comma-separated string codes = codes.split(/[\s,]+/) unless codes.is_a?(Array) codes.map { |code| Geolocation.from_iso3166(code) } end def serialize(geo_locations) PG::TextEncoder::Array.new.encode(cast(geo_locations).map(&:to_ltree)) end def deserialize(geo_ltrees) PG::TextDecoder::Array.new.decode(geo_ltrees).map { |geo_ltree| Geolocation.from_ltree(geo_ltree) } end end include ActiveModel::Validations FEATURE_FLAG = :billboard_location_targeting ISO3166_SEPARATOR = "-".freeze LTREE_SEPARATOR = ".".freeze DEFAULT_ENABLED_COUNTRIES = { ISO3166::Country.code_from_name("United States") => :with_regions, ISO3166::Country.code_from_name("Canada") => :with_regions }.freeze attr_reader :country_code, :region_code def self.from_iso3166(iso_3166) parse(iso_3166, separator: ISO3166_SEPARATOR) end def self.from_ltree(ltree) parse(ltree, separator: LTREE_SEPARATOR) end def self.parse(code, separator:) return if code.blank? return code if code.is_a?(Geolocation) country, region = code.split(separator) new(country, region) end def initialize(country_code, region_code = nil) @country_code = country_code @region_code = region_code end validates :country_code, inclusion: { in: ->(_) { Settings::General.billboard_enabled_countries.keys } } validates :region_code, inclusion: { in: ->(geolocation) { ISO3166::Country.region_codes_if_exists(geolocation.country_code) } }, allow_nil: true validate :valid_region_for_targeting, on: :targeting def ==(other) country_code == other.country_code && region_code == other.region_code end def to_iso3166 [country_code, region_code].compact.join("-") end def to_ltree [country_code, region_code].compact.join(".") end def to_sql_query_clause(column_name = :target_geolocations) return unless valid? lquery = country_code # Match region if specified lquery += ".#{region_code}{,}" if region_code "'#{lquery}' ~ #{column_name}" end def valid_region_for_targeting return if region_code.nil? || Settings::General.billboard_enabled_countries[country_code] == :with_regions errors.add(:region_code, "was provided on a country with region targeting disabled") end def errors_as_sentence errors.full_messages.to_sentence end end