From f48ce2f4c934fde3862cdad593eececc7a567d61 Mon Sep 17 00:00:00 2001 From: hellekin Date: Fri, 9 Oct 2020 10:24:06 +0200 Subject: Add Classifications and Resource validations - Turn `has_and_belongs_to_many` into `has_many :through`: now, resources and sections are related through Classifications. - Refactor usage of jsonb column to use ActiveRecord validations - Attention! store_accessor: NOTE: If you are using structured database data types (eg. PostgreSQL hstore/json, or MySQL 5.7+ json) there is no need for the serialization provided by .store. Simply use .store_accessor instead to generate the accessor methods. Be aware that these columns use a string keyed hash and do not allow access using a symbol. NOTE: The default validations with the exception of uniqueness will work. For example, if you want to check for uniqueness with hstore you will need to use a custom validation to handle it. https://api.rubyonrails.org/classes/ActiveRecord/Store.html --- Gemfile | 9 ++- Gemfile.lock | 10 ++++ app/models/agent.rb | 11 ++++ app/models/classification.rb | 4 ++ app/models/resource.rb | 68 ++++++++++++++++------ .../schemas/resource_feature_properties.json | 12 ++++ app/models/section.rb | 5 +- app/serializers/hash_serializer.rb | 9 +++ ...201009025353_add_default_to_resource_feature.rb | 30 ++++++++++ ...rename_resources_sections_to_classifications.rb | 5 ++ db/schema.rb | 15 ++--- 11 files changed, 147 insertions(+), 31 deletions(-) create mode 100644 app/models/classification.rb create mode 100644 app/models/schemas/resource_feature_properties.json create mode 100644 app/serializers/hash_serializer.rb create mode 100644 db/migrate/20201009025353_add_default_to_resource_feature.rb create mode 100644 db/migrate/20201009061548_rename_resources_sections_to_classifications.rb diff --git a/Gemfile b/Gemfile index e11ac05..1897dd4 100644 --- a/Gemfile +++ b/Gemfile @@ -32,11 +32,14 @@ gem 'bitfields' gem 'discourse_api' # User pagination gem 'kaminari' +# Use of Leaflet maps +gem 'leaflet-rails' +# Validate phone numbers +gem 'phony_rails' # Enforce stable UUIDs for models gem 'uuid_parameter', '~> 0.2.5' - -#Enable use of leaflet -gem 'leaflet-rails' +# Validate URLs in models +gem 'validate_url' # Reduces boot times through caching; required in config/boot.rb diff --git a/Gemfile.lock b/Gemfile.lock index 96d66e2..aa16165 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -120,11 +120,16 @@ GEM nokogiri (1.10.10) mini_portile2 (~> 2.4.0) pg (1.2.3) + phony (2.18.15) + phony_rails (0.14.13) + activesupport (>= 3.0) + phony (> 2.15) pry (0.13.1) coderay (~> 1.1) method_source (~> 1.0) pry-rails (0.3.9) pry (>= 0.10.4) + public_suffix (4.0.6) puma (4.3.6) nio4r (~> 2.0) puma-plugin-systemd (0.1.5) @@ -196,6 +201,9 @@ GEM thread_safe (~> 0.1) uuid_parameter (0.2.6) rails (>= 5.2.1) + validate_url (1.0.13) + activemodel (>= 3.0.0) + public_suffix web-console (4.0.4) actionview (>= 6.0.0) activemodel (>= 6.0.0) @@ -224,6 +232,7 @@ DEPENDENCIES leaflet-rails listen (~> 3.2) pg (>= 0.18, < 2.0) + phony_rails pry pry-rails puma (~> 4.1) @@ -235,6 +244,7 @@ DEPENDENCIES turbolinks (~> 5) tzinfo-data uuid_parameter (~> 0.2.5) + validate_url web-console (>= 3.3.0) webpacker (~> 4.0) diff --git a/app/models/agent.rb b/app/models/agent.rb index 07e0b8e..6264ba3 100644 --- a/app/models/agent.rb +++ b/app/models/agent.rb @@ -2,4 +2,15 @@ class Agent < ApplicationRecord has_many :agencies has_many :members, through: :agencies, source: :user has_many :resources + has_many :taxonomies + has_many :categories, through: :taxonomies + has_many :sections, through: :categories + + def to_param + uuid + end + + def to_s + name + end end diff --git a/app/models/classification.rb b/app/models/classification.rb new file mode 100644 index 0000000..c384aa7 --- /dev/null +++ b/app/models/classification.rb @@ -0,0 +1,4 @@ +class Classification < ApplicationRecord + belongs_to :resource + belongs_to :section +end diff --git a/app/models/resource.rb b/app/models/resource.rb index cd43bf9..906933d 100644 --- a/app/models/resource.rb +++ b/app/models/resource.rb @@ -1,26 +1,56 @@ +require_dependency 'phony_rails' + class Resource < ApplicationRecord # Universally Unique Identifier :uuid include UUIDParameter belongs_to :agent - has_and_belongs_to_many :sections - - # Figure out the requested property name - def method_missing(name, *args, &block) - Rails.logger.info("method_missing: #{name} // #{feature['properties'][name.to_s]}") - if feature['properties'].key?(name.to_s) - feature['properties'][name.to_s] - else - case name.to_s - when 'lon', 'longitude' - feature['geometry']['coordinates'].first - when 'lat', 'latitude' - feature['geometry']['coordinates'].last - when 'geo_type' - feature['geometry']['type'] - else - super - end - end + has_many :classifications + has_many :sections, through: :classifications + + serialize :feature, HashSerializer + store_accessor :feature, :name, :summary, :description, :email, :source, :address, :postal_code, :city, :phone_number, :website + + validates_associated :agent + + validates :name, + presence: true, + length: { in: 3..64 } + + validates :email, + with: { format: URI::MailTo::EMAIL_REGEXP }, + allow_blank: true + + validates :source, + inclusion: { in: Agent.pluck(:name) } + + # TODO: Address,Postal Code,City validation + + phony_normalize :phone_number, default_country_code: 'BE', normalize_when_valid: true + validates :phone_number, + phony_plausible: { ignore_record_country_code: true, ignore_record_country_number: true } + + # Depends on validate_url Gem + validates :website, + url: { allow_blank: true } + + # Accessors for feature['geometry'] + def geo_type + self.feature['geometry']['type'] + end + + # You can use, e.g.: res.longitude = 0.123 + def longitude + feature['geometry']['coordinates'][0] + end + def longitude=(value) + feature['geometry']['coordinates'][0] = value + end + # You can use, e.g.: res.latitude = 0.123 + def latitude + feature['geometry']['coordinates'][1] + end + def latitude=(value) + feature['geometry']['coordinates'][1] = value end end diff --git a/app/models/schemas/resource_feature_properties.json b/app/models/schemas/resource_feature_properties.json new file mode 100644 index 0000000..84cd6ae --- /dev/null +++ b/app/models/schemas/resource_feature_properties.json @@ -0,0 +1,12 @@ +// JSON Schema for Resource#feature +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://api.incommon.cc/schema/resource_feature.json", + "title": + "type": "object", + "required": [], + "geometry": { + }, + "properties": { + } +} diff --git a/app/models/section.rb b/app/models/section.rb index 6cfeb38..7d26882 100644 --- a/app/models/section.rb +++ b/app/models/section.rb @@ -1,11 +1,12 @@ class Section < ApplicationRecord belongs_to :category has_one :taxonomy, through: :category - has_and_belongs_to_many :resources + has_many :classifications + has_many :resources, through: :classifications acts_as_list column: :rank, scope: :category validates :name, uniqueness: { scope: :category_id }, - length: 3..64 + length: { in: 3..64 } end diff --git a/app/serializers/hash_serializer.rb b/app/serializers/hash_serializer.rb new file mode 100644 index 0000000..5db639f --- /dev/null +++ b/app/serializers/hash_serializer.rb @@ -0,0 +1,9 @@ +class HashSerializer + def self.dump(hash) + hash.to_json + end + + def self.load(hash) + (hash || {}).with_indifferent_access + end +end diff --git a/db/migrate/20201009025353_add_default_to_resource_feature.rb b/db/migrate/20201009025353_add_default_to_resource_feature.rb new file mode 100644 index 0000000..06cad6c --- /dev/null +++ b/db/migrate/20201009025353_add_default_to_resource_feature.rb @@ -0,0 +1,30 @@ +class AddDefaultToResourceFeature < ActiveRecord::Migration[6.0] + def up + change_column_default :resources, :feature, + { + "geometry": { + "type": "Point", + "coordinates": [0,0] + }, + "properties": { + "name":"", + "description":"", + "address":"", + "postal_code":"", + "city":"", + "email":"", + "phone_number":"", + "website":"", + "categories":[], + "source":"incommon", + "entry_number":nil, + "srid":4326 + } + } + add_index :resources, :feature, using: :gin + end + def down + change_column_default :resources, :feature, nil + remove_index :resources, :feature + end +end diff --git a/db/migrate/20201009061548_rename_resources_sections_to_classifications.rb b/db/migrate/20201009061548_rename_resources_sections_to_classifications.rb new file mode 100644 index 0000000..4f7972b --- /dev/null +++ b/db/migrate/20201009061548_rename_resources_sections_to_classifications.rb @@ -0,0 +1,5 @@ +class RenameResourcesSectionsToClassifications < ActiveRecord::Migration[6.0] + def change + rename_table :resources_sections, :classifications + end +end diff --git a/db/schema.rb b/db/schema.rb index 16ba65c..0660ef9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2020_10_08_190558) do +ActiveRecord::Schema.define(version: 2020_10_09_061548) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -50,21 +50,22 @@ ActiveRecord::Schema.define(version: 2020_10_08_190558) do t.index ["taxonomy_id"], name: "index_categories_on_taxonomy_id" end + create_table "classifications", id: false, force: :cascade do |t| + t.bigint "resource_id", null: false + t.bigint "section_id", null: false + end + create_table "resources", force: :cascade do |t| t.uuid "uuid" - t.jsonb "feature" + t.jsonb "feature", default: {"geometry"=>{"type"=>"Point", "coordinates"=>[0, 0]}, "properties"=>{"city"=>"", "name"=>"", "srid"=>4326, "email"=>"", "source"=>"incommon", "address"=>"", "website"=>"", "categories"=>[], "description"=>"", "postal_code"=>"", "entry_number"=>nil, "phone_number"=>""}} t.bigint "agent_id", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.index ["agent_id"], name: "index_resources_on_agent_id" + t.index ["feature"], name: "index_resources_on_feature", using: :gin t.index ["uuid"], name: "index_resources_on_uuid", unique: true end - create_table "resources_sections", id: false, force: :cascade do |t| - t.bigint "resource_id", null: false - t.bigint "section_id", null: false - end - create_table "sections", force: :cascade do |t| t.string "name", limit: 64 t.string "summary", limit: 136 -- cgit v1.2.3