From d14700c51d692335f001a93c2f6b13b135783206 Mon Sep 17 00:00:00 2001 From: IN COMMON Collective Date: Thu, 8 Apr 2021 16:34:15 +0200 Subject: [FIX] Use form model to create/edit resources (fixes #4, fixes #5, refs #3) Since we must associate other models (e.g., classifications) to a Resource, we use a composite model to save all changes inside a database transaction. This approach makes it simpler to handle resources and their associations. Work remains to fix the geolocation and reverse geolocation to ensure these are in sync. --- app/models/classification.rb | 2 + app/models/resource.rb | 11 ++-- app/models/resource_form.rb | 138 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 146 insertions(+), 5 deletions(-) create mode 100644 app/models/resource_form.rb (limited to 'app/models') diff --git a/app/models/classification.rb b/app/models/classification.rb index 0754054..e2b180f 100644 --- a/app/models/classification.rb +++ b/app/models/classification.rb @@ -5,4 +5,6 @@ class Classification < ApplicationRecord belongs_to :resource belongs_to :section + + self.primary_key = :resource_id end diff --git a/app/models/resource.rb b/app/models/resource.rb index c603978..b803afb 100644 --- a/app/models/resource.rb +++ b/app/models/resource.rb @@ -21,7 +21,7 @@ class Resource < ApplicationRecord length: { in: 3..64 } validates :email, - with: { format: URI::MailTo::EMAIL_REGEXP }, + format: { with: URI::MailTo::EMAIL_REGEXP }, allow_blank: true validates :source, @@ -31,7 +31,8 @@ class Resource < ApplicationRecord 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 } + phony_plausible: { ignore_record_country_code: true, ignore_record_country_number: true }, + allow_blank: true # Depends on validate_url Gem validates :website, @@ -47,19 +48,19 @@ class Resource < ApplicationRecord format('%3.7f', lon: feature['geometry']['coordinates'][0]).to_f end def longitude=(value) - feature['geometry']['coordinates'][0] = format('%3.7f', lon: value).to_f + feature['geometry']['coordinates'][0] = format('%3.7f', lon: value.to_f).to_f end # You can use, e.g.: res.latitude = 0.123 def latitude format('%2.7f', lat: feature['geometry']['coordinates'][1]).to_f end def latitude=(value) - feature['geometry']['coordinates'][1] = format('%2.7f', lat: value).to_f + feature['geometry']['coordinates'][1] = format('%2.7f', lat: value.to_f).to_f end # Properties - [:name, :summary, :description, :email, :source, :address, :postal_code, :city, :phone_number, :website].each do |prop| + [:name, :summary, :description, :email, :source, :address, :postal_code, :city, :phone_number, :website, :entry_number].each do |prop| # Define a reader define_method prop do properties[prop.to_s] diff --git a/app/models/resource_form.rb b/app/models/resource_form.rb new file mode 100644 index 0000000..14275a4 --- /dev/null +++ b/app/models/resource_form.rb @@ -0,0 +1,138 @@ +class ResourceForm + include ActiveModel::Model + + attr_accessor :agent, :resource + attr_accessor :agent_id, :uuid, :name, :summary, :description, :email, + :website, :phone_number, :address, :postal_code, :city, + :entry_number, :latitude, :longitude, :section_ids, :source + + with_options presence: true do + validates :name + validates :email, allow_blank: true +# validates :source + validates :phone_number, allow_blank: true + validates :website, allow_blank: true + end + + validate :validate_models + + def initialize(agent, attributes = nil) + @agent = agent + @attributes = attributes || default_attributes + + raise ArgumentError "Attributes must be permitted" unless @attributes.permitted? + + # We use an empty hidden field to ensure this is always set, even if no + # section is selected, so let's remove the empty value + @section_ids = @attributes.extract!(:section_ids)[:section_ids].filter_map { |x| x.presence } rescue [] + # We may have an existing and correct source, but anyway we must have one + @resource = @agent.resources.build({ source: @agent.name }.merge(@attributes)) + end + + def new_record? + resource&.new_record? || true + end + + # New forms can provide an existing classification, + # and updates can prefer passed arguments to ensure correct changes + def section_ids + resource.present? && @section_ids.empty? ? resource.section_ids : @section_ids + end + + # Ensure field values stick around + # OMG this is ugly + # TODO: use delegator or metaprog + def name + @name ||= resource&.name + end + def summary + @summary ||= resource&.summary + end + def description + @description ||= resource&.description + end + def email + @email ||= resource&.email + end + def website + @website ||= resource&.website + end + def phone_number + @phone_number ||= resource&.phone_number + end + def address + @address ||= resource&.address + end + def postal_code + @postal_code ||= resource&.postal_code + end + def city + @city ||= resource&.city + end + def longitude + @longitude ||= resource&.longitude + end + def latitude + @latitude ||= resource&.latitude + end + + + def save + Rails.logger.info "--- Calling SAVE ---" + return false if invalid? + + ActiveRecord::Base.transaction do + resource.save! && \ + @section_ids.each do |sec_id| + Rails.logger.info("section " + sec_id) + resource.categories << resource.classifications.find_or_create_by!(section_id: sec_id) + end + + true + rescue ActiveRecord::StatementInvalid => e + errors.add(:base, e.message) + false + end + + rescue Exception => e + Rails.logger.info "Unhandled Exception #{e.class}: #{e.message}" + false + end + + private + + def default_attributes + ActionController::Parameters.new( + { + agent_id: @agent.id, + uuid: '', + name: '', + summary: '', + description: '', + email: '', + website: '', + phone_number: '', + address: '', + postal_code: '', + city: '', + entry_number: nil, + latitude: 0.0, + longitude: 0.0, + source: @agent.name, + section_ids: [] + }).permit! + end + + # Validate underlying models + def validate_models + Rails.logger.info('--- validate_models ---') + resource.valid? || promote_errors(resource.errors) + end + + # Promote an error from the Model to the Form + def promote_errors(model_errors) + model_errors.each do |attribute, error| + errors.add(attribute, error.full_message) + end + end +end -- cgit v1.2.3