From 1740e0ba66fc1a2e3e63fbad897691c98a76fbdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javi=20Mart=C3=ADn?= Date: Tue, 17 Nov 2020 21:32:09 +0100 Subject: [PATCH 1/3] Add SDG::Relation model --- app/models/concerns/sdg/relatable.rb | 7 ++++ app/models/concerns/sdg/related.rb | 7 ++++ app/models/sdg/goal.rb | 2 ++ app/models/sdg/relation.rb | 6 ++++ app/models/sdg/target.rb | 1 + .../20201117200945_create_sdg_relations.rb | 12 +++++++ db/schema.rb | 14 +++++++- spec/models/sdg/relation_spec.rb | 33 +++++++++++++++++++ 8 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 app/models/concerns/sdg/relatable.rb create mode 100644 app/models/concerns/sdg/related.rb create mode 100644 app/models/sdg/relation.rb create mode 100644 db/migrate/20201117200945_create_sdg_relations.rb create mode 100644 spec/models/sdg/relation_spec.rb diff --git a/app/models/concerns/sdg/relatable.rb b/app/models/concerns/sdg/relatable.rb new file mode 100644 index 000000000..e886616fc --- /dev/null +++ b/app/models/concerns/sdg/relatable.rb @@ -0,0 +1,7 @@ +module SDG::Relatable + extend ActiveSupport::Concern + + included do + has_many :sdg_relations, as: :relatable, dependent: :destroy, class_name: "SDG::Relation" + end +end diff --git a/app/models/concerns/sdg/related.rb b/app/models/concerns/sdg/related.rb new file mode 100644 index 000000000..ac96ff325 --- /dev/null +++ b/app/models/concerns/sdg/related.rb @@ -0,0 +1,7 @@ +module SDG::Related + extend ActiveSupport::Concern + + included do + has_many :relations, as: :related_sdg, dependent: :destroy + end +end diff --git a/app/models/sdg/goal.rb b/app/models/sdg/goal.rb index 940f7a7d1..bd26438c8 100644 --- a/app/models/sdg/goal.rb +++ b/app/models/sdg/goal.rb @@ -1,4 +1,6 @@ class SDG::Goal < ApplicationRecord + include SDG::Related + validates :code, presence: true, uniqueness: true, inclusion: { in: 1..17 } has_many :targets, dependent: :destroy diff --git a/app/models/sdg/relation.rb b/app/models/sdg/relation.rb new file mode 100644 index 000000000..0148bea46 --- /dev/null +++ b/app/models/sdg/relation.rb @@ -0,0 +1,6 @@ +class SDG::Relation < ApplicationRecord + validates :related_sdg_id, uniqueness: { scope: [:related_sdg_type, :relatable_id, :relatable_type] } + + belongs_to :relatable, polymorphic: true, optional: false + belongs_to :related_sdg, polymorphic: true, optional: false +end diff --git a/app/models/sdg/target.rb b/app/models/sdg/target.rb index 72568490d..ad165e280 100644 --- a/app/models/sdg/target.rb +++ b/app/models/sdg/target.rb @@ -1,5 +1,6 @@ class SDG::Target < ApplicationRecord include Comparable + include SDG::Related validates :code, presence: true, uniqueness: true validates :goal, presence: true diff --git a/db/migrate/20201117200945_create_sdg_relations.rb b/db/migrate/20201117200945_create_sdg_relations.rb new file mode 100644 index 000000000..55415c78c --- /dev/null +++ b/db/migrate/20201117200945_create_sdg_relations.rb @@ -0,0 +1,12 @@ +class CreateSDGRelations < ActiveRecord::Migration[5.2] + def change + create_table :sdg_relations do |t| + t.references :related_sdg, polymorphic: true + t.references :relatable, polymorphic: true + + t.index [:related_sdg_id, :related_sdg_type, :relatable_id, :relatable_type], name: "sdg_relations_unique", unique: true + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 5cb3ed956..5b1944278 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_11_12_155047) do +ActiveRecord::Schema.define(version: 2020_11_17_200945) do # These are extensions that must be enabled in order to support this database enable_extension "pg_trgm" @@ -1304,6 +1304,18 @@ ActiveRecord::Schema.define(version: 2020_11_12_155047) do t.index ["code"], name: "index_sdg_goals_on_code", unique: true end + create_table "sdg_relations", force: :cascade do |t| + t.string "related_sdg_type" + t.bigint "related_sdg_id" + t.string "relatable_type" + t.bigint "relatable_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["relatable_type", "relatable_id"], name: "index_sdg_relations_on_relatable_type_and_relatable_id" + t.index ["related_sdg_id", "related_sdg_type", "relatable_id", "relatable_type"], name: "sdg_relations_unique", unique: true + t.index ["related_sdg_type", "related_sdg_id"], name: "index_sdg_relations_on_related_sdg_type_and_related_sdg_id" + end + create_table "sdg_targets", force: :cascade do |t| t.bigint "goal_id" t.string "code", null: false diff --git a/spec/models/sdg/relation_spec.rb b/spec/models/sdg/relation_spec.rb new file mode 100644 index 000000000..d2da87fdd --- /dev/null +++ b/spec/models/sdg/relation_spec.rb @@ -0,0 +1,33 @@ +require "rails_helper" + +describe SDG::Relation do + describe "Validations" do + it "is valid with a related SDG and a relatable model" do + relation = SDG::Relation.new(related_sdg: SDG::Goal[1], relatable: create(:proposal)) + + expect(relation).to be_valid + end + + it "is not valid without a related SDG" do + relation = SDG::Relation.new(relatable: create(:proposal)) + + expect(relation).not_to be_valid + end + + it "is not valid without a relatable model" do + relation = SDG::Relation.new(related_sdg: SDG::Goal[1]) + + expect(relation).not_to be_valid + end + + it "is not valid when a relation already exists" do + proposal = create(:proposal) + goal = SDG::Goal[1] + + SDG::Relation.create!(related_sdg: goal, relatable: proposal) + relation = SDG::Relation.new(related_sdg: goal, relatable: proposal) + + expect(relation).not_to be_valid + end + end +end From 42699275a1095ed369c193d8655e04334b61cd05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javi=20Mart=C3=ADn?= Date: Wed, 18 Nov 2020 19:35:17 +0100 Subject: [PATCH 2/3] Add relations between relatable models and SDGs Note we cannot directly get all related SDGs through SQL because they're spread through different tables. --- app/models/budget/investment.rb | 1 + app/models/concerns/sdg/relatable.rb | 11 ++++++ app/models/debate.rb | 1 + app/models/legislation/process.rb | 1 + app/models/poll.rb | 1 + app/models/proposal.rb | 1 + spec/models/sdg/relatable_spec.rb | 55 ++++++++++++++++++++++++++++ 7 files changed, 71 insertions(+) create mode 100644 spec/models/sdg/relatable_spec.rb diff --git a/app/models/budget/investment.rb b/app/models/budget/investment.rb index 8258bcc6b..092153eed 100644 --- a/app/models/budget/investment.rb +++ b/app/models/budget/investment.rb @@ -13,6 +13,7 @@ class Budget include Imageable include Mappable include Documentable + include SDG::Relatable acts_as_taggable_on :valuation_tags acts_as_votable diff --git a/app/models/concerns/sdg/relatable.rb b/app/models/concerns/sdg/relatable.rb index e886616fc..e85e1407c 100644 --- a/app/models/concerns/sdg/relatable.rb +++ b/app/models/concerns/sdg/relatable.rb @@ -3,5 +3,16 @@ module SDG::Relatable included do has_many :sdg_relations, as: :relatable, dependent: :destroy, class_name: "SDG::Relation" + + %w[SDG::Goal SDG::Target].each do |sdg_type| + has_many sdg_type.constantize.table_name.to_sym, + through: :sdg_relations, + source: :related_sdg, + source_type: sdg_type + end + end + + def related_sdgs + sdg_relations.map(&:related_sdg) end end diff --git a/app/models/debate.rb b/app/models/debate.rb index 22ed568b1..47a78f360 100644 --- a/app/models/debate.rb +++ b/app/models/debate.rb @@ -14,6 +14,7 @@ class Debate < ApplicationRecord include Relationable include Notifiable include Randomizable + include SDG::Relatable acts_as_votable acts_as_paranoid column: :hidden_at diff --git a/app/models/legislation/process.rb b/app/models/legislation/process.rb index 63f819a5d..f1a2e12c1 100644 --- a/app/models/legislation/process.rb +++ b/app/models/legislation/process.rb @@ -4,6 +4,7 @@ class Legislation::Process < ApplicationRecord include Milestoneable include Imageable include Documentable + include SDG::Relatable acts_as_paranoid column: :hidden_at acts_as_taggable_on :customs diff --git a/app/models/poll.rb b/app/models/poll.rb index edef37f06..0289a0fc2 100644 --- a/app/models/poll.rb +++ b/app/models/poll.rb @@ -8,6 +8,7 @@ class Poll < ApplicationRecord include Sluggable include StatsVersionable include Reportable + include SDG::Relatable translates :name, touch: true translates :summary, touch: true diff --git a/app/models/proposal.rb b/app/models/proposal.rb index d35da3b75..9fe469915 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -19,6 +19,7 @@ class Proposal < ApplicationRecord include Relationable include Milestoneable include Randomizable + include SDG::Relatable acts_as_votable acts_as_paranoid column: :hidden_at diff --git a/spec/models/sdg/relatable_spec.rb b/spec/models/sdg/relatable_spec.rb new file mode 100644 index 000000000..4b87971be --- /dev/null +++ b/spec/models/sdg/relatable_spec.rb @@ -0,0 +1,55 @@ +require "rails_helper" + +describe SDG::Relatable do + let(:goal) { SDG::Goal[1] } + let(:target) { SDG::Target["1.2"] } + let(:another_goal) { SDG::Goal[2] } + let(:another_target) { SDG::Target["2.3"] } + + let(:relatable) { create(:proposal) } + + describe "#sdg_goals" do + it "can assign goals to a model" do + relatable.sdg_goals = [goal, another_goal] + + expect(SDG::Relation.count).to be 2 + expect(SDG::Relation.first.relatable).to eq relatable + expect(SDG::Relation.last.relatable).to eq relatable + expect(SDG::Relation.first.related_sdg).to eq goal + expect(SDG::Relation.last.related_sdg).to eq another_goal + end + + it "can obtain the list of goals" do + relatable.sdg_goals = [goal, another_goal] + + expect(relatable.reload.sdg_goals).to match_array [goal, another_goal] + end + end + + describe "#sdg_targets" do + it "can assign targets to a model" do + relatable.sdg_targets = [target, another_target] + + expect(SDG::Relation.count).to be 2 + expect(SDG::Relation.first.relatable).to eq relatable + expect(SDG::Relation.last.relatable).to eq relatable + expect(SDG::Relation.first.related_sdg).to eq target + expect(SDG::Relation.last.related_sdg).to eq another_target + end + + it "can obtain the list of targets" do + relatable.sdg_targets = [target, another_target] + + expect(relatable.reload.sdg_targets).to match_array [target, another_target] + end + end + + describe "#related_sdgs" do + it "returns all related goals and targets" do + relatable.sdg_goals = [goal, another_goal] + relatable.sdg_targets = [target, another_target] + + expect(relatable.reload.related_sdgs).to match_array [goal, another_goal, target, another_target] + end + end +end From c0edd1b227820e063066bd73b621464f5f295638 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javi=20Mart=C3=ADn?= Date: Fri, 20 Nov 2020 12:14:17 +0100 Subject: [PATCH 3/3] Allow SDGs to get all their related contents Note we cannot directly get all related contents through SQL because related contents are spread through different tables. --- app/models/concerns/sdg/related.rb | 19 ++++++++++++++++ spec/models/sdg/related_spec.rb | 36 ++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 spec/models/sdg/related_spec.rb diff --git a/app/models/concerns/sdg/related.rb b/app/models/concerns/sdg/related.rb index ac96ff325..1820b3ae3 100644 --- a/app/models/concerns/sdg/related.rb +++ b/app/models/concerns/sdg/related.rb @@ -1,7 +1,26 @@ module SDG::Related extend ActiveSupport::Concern + RELATABLE_TYPES = %w[ + Budget::Investment + Debate + Legislation::Process + Poll + Proposal + ].freeze + included do has_many :relations, as: :related_sdg, dependent: :destroy + + RELATABLE_TYPES.each do |relatable_type| + has_many relatable_type.constantize.table_name.to_sym, + through: :relations, + source: :relatable, + source_type: relatable_type + end + end + + def relatables + relations.map(&:relatable) end end diff --git a/spec/models/sdg/related_spec.rb b/spec/models/sdg/related_spec.rb new file mode 100644 index 000000000..fb19c3732 --- /dev/null +++ b/spec/models/sdg/related_spec.rb @@ -0,0 +1,36 @@ +require "rails_helper" + +describe SDG::Related do + let(:proposal) { create(:proposal) } + let(:another_proposal) { create(:proposal) } + let(:related_sdg) { SDG::Goal[1] } + + describe "#proposals" do + it "can assign proposals to a model" do + related_sdg.proposals = [proposal, another_proposal] + + expect(SDG::Relation.count).to be 2 + expect(SDG::Relation.first.related_sdg).to eq related_sdg + expect(SDG::Relation.last.related_sdg).to eq related_sdg + expect(SDG::Relation.first.relatable).to eq proposal + expect(SDG::Relation.last.relatable).to eq another_proposal + end + + it "can obtain the list of proposals" do + related_sdg.proposals = [proposal, another_proposal] + + expect(related_sdg.reload.proposals).to match_array [proposal, another_proposal] + end + end + + describe "#relatables" do + let(:investment) { create(:budget_investment) } + + it "returns all related content" do + related_sdg.proposals = [proposal, another_proposal] + related_sdg.budget_investments = [investment] + + expect(related_sdg.reload.relatables).to match_array [proposal, another_proposal, investment] + end + end +end