aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIN COMMON Collective <collective@incommon.cc>2021-04-08 16:34:15 +0200
committerIN COMMON Collective <collective@incommon.cc>2021-04-08 16:34:15 +0200
commitd14700c51d692335f001a93c2f6b13b135783206 (patch)
tree1204bc1ae744098eba6604a961765187984a90d8
parentc738e96b2b99bfd92b70d4cec26d6874a7f609e4 (diff)
downloadincommon-map-d14700c51d692335f001a93c2f6b13b135783206.tar.gz
[FIX] Use form model to create/edit resources (fixes #4, fixes #5, refs #3)v0.1.10
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.
-rw-r--r--app/controllers/resources_controller.rb31
-rw-r--r--app/helpers/resources_helper.rb5
-rw-r--r--app/models/classification.rb2
-rw-r--r--app/models/resource.rb11
-rw-r--r--app/models/resource_form.rb138
-rw-r--r--app/views/resources/_edit.html.erb4
-rw-r--r--app/views/resources/_form.html.erb37
-rw-r--r--app/views/resources/new.html.erb4
-rw-r--r--config/routes.rb1
9 files changed, 194 insertions, 39 deletions
diff --git a/app/controllers/resources_controller.rb b/app/controllers/resources_controller.rb
index 2fa392f..4a99f2a 100644
--- a/app/controllers/resources_controller.rb
+++ b/app/controllers/resources_controller.rb
@@ -3,7 +3,8 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
class ResourcesController < ApplicationController
- before_action :set_resource, only: [:new, :show, :edit, :update, :delete, :destroy]
+ before_action :set_resource, only: [:show, :delete, :destroy]
+# before_action :set_resource_form, only: [:new, :create, :edit, :update]
# GET /resources
def index
@@ -12,6 +13,7 @@ class ResourcesController < ApplicationController
# GET /resources/new
def new
+ @resource = ResourceForm.new(current_agent)
end
# POST /resources
@@ -19,18 +21,12 @@ class ResourcesController < ApplicationController
# TODO Background job to list similar items
# TODO If there's a match, return to user with new record or list of mergeable ones
- classification = resource_params.delete(:classification) || { section_ids: [] }
-
- Rails.logger.info resource_params
-
- @resource = current_agent.resources.build(resource_params)
+ @resource = ResourceForm.new(current_agent, resource_form_params)
respond_to do |format|
- Rails.logger.info "format: #{format} - Res: #{@resource.inspect}"
if @resource.save
- classification[:section_ids].each { |id| @resource.classifications.find_or_create_by(section_id: id) }
- format.html { redirect_to @resource, notice: 'Merci de votre contribution !' }
- format.json { render :show, status: :created, location: @resource }
+ format.html { redirect_to @resource.resource, notice: 'Merci de votre contribution !' }
+ format.json { render :show, status: :created, location: @resource.resource }
else
format.html { render :new }
format.json { render json: @resource.errors, status: :unprocessable_entity }
@@ -62,8 +58,8 @@ class ResourcesController < ApplicationController
respond_to do |format|
if @resource.update(resource_params)
- format.html { redirect_to @resource, notice: 'Merci de votre contribution !' }
- format.json { render :show, status: :ok, location: @resource }
+ format.html { redirect_to @resource.resource, notice: 'Merci de votre contribution !' }
+ format.json { render :show, status: :ok, location: @resource.resource }
else
format.html { render :edit }
format.json { render json: @resource.errors, status: :unprocessable_entity }
@@ -97,9 +93,9 @@ class ResourcesController < ApplicationController
private
- def resource_params
+ def resource_form_params
params
- .require(:resource)
+ .require(:resource_form)
.permit(:agent_id,
:uuid,
:name,
@@ -111,11 +107,12 @@ class ResourcesController < ApplicationController
:address,
:postal_code,
:city,
- :entry_number,
- :categories,
+ :entry_number, # Unused on new resources
+ :categories, #
:latitude,
:longitude,
- classification: [ :section_ids ])
+ :source,
+ section_ids: [])
end
def set_resource
diff --git a/app/helpers/resources_helper.rb b/app/helpers/resources_helper.rb
index a949311..792a78a 100644
--- a/app/helpers/resources_helper.rb
+++ b/app/helpers/resources_helper.rb
@@ -4,13 +4,14 @@
module ResourcesHelper
# Return a SELECT tag to choose a section in a given taxonomy
- def section_select(taxonomy, selected = nil)
+ def section_select(taxonomy, selected = nil, options = {})
grouped_options = []
+ tag_name = options[:name] || 'classification[section_ids]'
taxonomy.categories.each do |c|
grouped_options << [c.name, c.sections.map { |s| [s.name, s.id] }]
end
- select_tag('classification[section_ids]', grouped_options_for_select(grouped_options), selected: selected, multiple: true)
+ select_tag(tag_name, grouped_options_for_select(grouped_options, selected), multiple: true)
end
end
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('%<lon>3.7f', lon: feature['geometry']['coordinates'][0]).to_f
end
def longitude=(value)
- feature['geometry']['coordinates'][0] = format('%<lon>3.7f', lon: value).to_f
+ feature['geometry']['coordinates'][0] = format('%<lon>3.7f', lon: value.to_f).to_f
end
# You can use, e.g.: res.latitude = 0.123
def latitude
format('%<lat>2.7f', lat: feature['geometry']['coordinates'][1]).to_f
end
def latitude=(value)
- feature['geometry']['coordinates'][1] = format('%<lat>2.7f', lat: value).to_f
+ feature['geometry']['coordinates'][1] = format('%<lat>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
diff --git a/app/views/resources/_edit.html.erb b/app/views/resources/_edit.html.erb
index 55c59f6..f22aee3 100644
--- a/app/views/resources/_edit.html.erb
+++ b/app/views/resources/_edit.html.erb
@@ -1,6 +1,6 @@
-<h3>Edit <%= @resource.presence.feature['properties']['name'] || 'new resource' %></h3>
+<h3>Edit <%= @resource.presence.name || 'new resource' %></h3>
<h2>Context: <%= current_agent %></h2>
-<%= form_with model: [current_agent,@resource], url: controller.action_name == 'new' ? agent_resources_path(agent: current_agent) : resource_path(@resource) do |f| %>
+<%= form_with model: @resource, url: @resource.presence.new_record? ? agent_resources_path(current_agent) : resource_path(@resource.resource) do |f| %>
<%= render partial: 'form', locals: { resource: resource, f: f } %>
<% end %>
diff --git a/app/views/resources/_form.html.erb b/app/views/resources/_form.html.erb
index 89745bd..648834d 100644
--- a/app/views/resources/_form.html.erb
+++ b/app/views/resources/_form.html.erb
@@ -1,12 +1,24 @@
-<fieldset>
+<% if resource.errors.any? %>
+ <div id="error_explanation">
+ <h2><%= pluralize(resource.errors.count, "error") %> prohibited this resource from being saved:</h2>
+
+ <ul>
+ <% resource.errors.each do |error| %>
+ <li><%= error.full_message %></li>
+ <% end %>
+ </ul>
+ </div>
+<% end %>
+
+ <fieldset>
<%= tag.legend "Propriétés de la ressource" %>
- <dl>
+ <dl class="field">
<dt><%= f.label :name %></dt>
- <dd><%= f.text_field :name, maxlength: 64, placeholder: 'La ferme des animaux', value: resource.name %></dd>
+ <dd><%= f.text_field :name, maxlength: 64, placeholder: 'La ferme des animaux' %></dd>
<dt><%= f.label :summary %></dt>
- <dd><%= f.text_field :summary, maxlength: 136, placeholder: 'Une ferme locale', value: resource.name %></dd>
+ <dd><%= f.text_field :summary, maxlength: 136, placeholder: 'Une ferme locale' %></dd>
<dt><%= f.label :description %></dt>
<dd><%= f.text_area :description, cols: 72, rows: 10, placeholder: '## Un choix pertinent
@@ -29,33 +41,34 @@ La description _peut_ comporter du [Markdown].
<fieldset>
<%= tag.legend "Classification" %>
<%# TODO Add a taxonomy selector %>
- <%= section_select(current_agent.taxonomies&.first || Taxonomy.first) %>
+ <%= section_select(current_agent.taxonomies&.first || Taxonomy.first, resource.section_ids, name: 'resource_form[section_ids]') %>
</fieldset>
<fieldset>
<%= tag.legend "Coordonées géographiques" %>
<%# TODO Add a graphical geo selector %>
- <dl>
+ <dl class="field">
<dt><%= f.label :address %></dt>
- <dd><%= f.text_field :address, value: resource.address, placeholder: 'Rue aux herbes, 123' %></dd>
+ <dd><%= f.text_field :address, placeholder: 'Rue aux herbes, 123' %></dd>
<dt><%= f.label :postal_code %></dt>
- <dd><%= f.text_field :postal_code, value: resource.postal_code, placeholder: '1000' %></dd>
+ <dd><%= f.text_field :postal_code, placeholder: '1000' %></dd>
<dt><%= f.label :city %></dt>
- <dd><%= f.text_field :city, value: resource.city, placeholder: 'Bruxelles' %></dd>
+ <dd><%= f.text_field :city, placeholder: 'Bruxelles' %></dd>
</dl>
- <dl>
+ <dl class="field">
<dt><%= f.label :longitude %></dt>
- <dd><%= f.text_field :longitude, value: resource.longitude, placeholder: '0.56789012' %></dd>
+ <dd><%= f.text_field :longitude, placeholder: '0.56789012', pattern: '[\d]+.[\d]+' %></dd>
<dt><%= f.label :latitude %></dt>
- <dd><%= f.text_field :latitude, value: resource.latitude, placeholder: '50.12345678' %></dd>
+ <dd><%= f.text_field :latitude, placeholder: '50.12345678', pattern: '[\d]+.[\d]+' %></dd>
</dl>
</fieldset>
<%= f.hidden_field :agent_id, value: current_agent.id %>
+<%= f.hidden_field :source, value: current_agent.name %>
<p><%= f.submit 'Save' %></p>
diff --git a/app/views/resources/new.html.erb b/app/views/resources/new.html.erb
index 80d8206..e8347b4 100644
--- a/app/views/resources/new.html.erb
+++ b/app/views/resources/new.html.erb
@@ -1,2 +1,4 @@
-<%= render partial: 'edit', locals: { resource: @resource } %>
+<%= render 'edit', resource: @resource %>
+
+<#%= render partial: 'edit', locals: { resource: @resource } %>
diff --git a/config/routes.rb b/config/routes.rb
index 54f383e..7fd064a 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -2,6 +2,7 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
Rails.application.routes.draw do
+
get 'sections/show'
# For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html