From 2ad66409e214421bb8f7f22337b816f1fba7789d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sen=C3=A9n=20Rodero=20Rodr=C3=ADguez?= Date: Mon, 23 Nov 2020 17:00:17 +0100 Subject: [PATCH] Add SDG LocalTarget model --- app/models/concerns/sdg/relatable.rb | 2 +- app/models/sdg/local_target.rb | 37 +++++++++ app/models/sdg/target.rb | 1 + config/locales/en/activerecord.yml | 4 + config/locales/es/activerecord.yml | 4 + ...20201123124006_create_sdg_local_targets.rb | 22 ++++++ db/schema.rb | 22 +++++- spec/factories/sdg.rb | 8 ++ spec/models/sdg/local_target_spec.rb | 77 +++++++++++++++++++ spec/models/sdg/relatable_spec.rb | 24 +++++- 10 files changed, 198 insertions(+), 3 deletions(-) create mode 100644 app/models/sdg/local_target.rb create mode 100644 db/migrate/20201123124006_create_sdg_local_targets.rb create mode 100644 spec/models/sdg/local_target_spec.rb diff --git a/app/models/concerns/sdg/relatable.rb b/app/models/concerns/sdg/relatable.rb index e85e1407c..9da20b0d5 100644 --- a/app/models/concerns/sdg/relatable.rb +++ b/app/models/concerns/sdg/relatable.rb @@ -4,7 +4,7 @@ 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| + %w[SDG::Goal SDG::Target SDG::LocalTarget].each do |sdg_type| has_many sdg_type.constantize.table_name.to_sym, through: :sdg_relations, source: :related_sdg, diff --git a/app/models/sdg/local_target.rb b/app/models/sdg/local_target.rb new file mode 100644 index 000000000..8443e2a40 --- /dev/null +++ b/app/models/sdg/local_target.rb @@ -0,0 +1,37 @@ +class SDG::LocalTarget < ApplicationRecord + include Comparable + include SDG::Related + + delegate :goal, to: :target + + translates :title, touch: true + translates :description, touch: true + include Globalizable + + validates_translation :title, presence: true + validates_translation :description, presence: true + + validates :code, presence: true, uniqueness: true, + format: ->(local_target) { /\A#{local_target.target&.code}\.\d+/ } + validates :target, presence: true + + belongs_to :target + + def <=>(local_target) + return unless local_target.class == self.class + + [target, numeric_subcode] <=> [local_target.target, local_target.numeric_subcode] + end + + protected + + def numeric_subcode + subcode.to_i + end + + private + + def subcode + code.split(".").last + end +end diff --git a/app/models/sdg/target.rb b/app/models/sdg/target.rb index ad165e280..6ff87d155 100644 --- a/app/models/sdg/target.rb +++ b/app/models/sdg/target.rb @@ -6,6 +6,7 @@ class SDG::Target < ApplicationRecord validates :goal, presence: true belongs_to :goal + has_many :local_targets, dependent: :destroy def title I18n.t("sdg.goals.goal_#{goal.code}.targets.target_#{code_key}.title") diff --git a/config/locales/en/activerecord.yml b/config/locales/en/activerecord.yml index 5721fc5e5..ca6200560 100644 --- a/config/locales/en/activerecord.yml +++ b/config/locales/en/activerecord.yml @@ -571,6 +571,10 @@ en: attributes: locale: already_translated: Already translated resource + sdg/local_target: + attributes: + code: + invalid: "must start with the same code as its target followed by a dot and end with a number" messages: translations_too_short: Is mandatory to provide one translation at least record_invalid: "Validation failed: %{errors}" diff --git a/config/locales/es/activerecord.yml b/config/locales/es/activerecord.yml index 4abe798c7..0b82e9120 100644 --- a/config/locales/es/activerecord.yml +++ b/config/locales/es/activerecord.yml @@ -573,6 +573,10 @@ es: attributes: locale: already_translated: Recurso ya traducido + sdg/local_target: + attributes: + code: + invalid: "debe empezar con el código de su meta seguido de un punto y terminar con un número" messages: translations_too_short: El obligatorio proporcionar una traducción como mínimo record_invalid: "Error de validación: %{errors}" diff --git a/db/migrate/20201123124006_create_sdg_local_targets.rb b/db/migrate/20201123124006_create_sdg_local_targets.rb new file mode 100644 index 000000000..264a9cee9 --- /dev/null +++ b/db/migrate/20201123124006_create_sdg_local_targets.rb @@ -0,0 +1,22 @@ +class CreateSDGLocalTargets < ActiveRecord::Migration[5.2] + def change + create_table :sdg_local_targets do |t| + t.references :target + t.string :code + t.timestamps + + t.index :code, unique: true + end + + create_table :sdg_local_target_translations do |t| + t.bigint :sdg_local_target_id, null: false + t.string :locale, null: false + t.string :title + t.text :description + t.timestamps null: false + + t.index :locale + t.index :sdg_local_target_id + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 5b1944278..601a3cdd3 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_17_200945) do +ActiveRecord::Schema.define(version: 2020_11_23_124006) do # These are extensions that must be enabled in order to support this database enable_extension "pg_trgm" @@ -1304,6 +1304,26 @@ ActiveRecord::Schema.define(version: 2020_11_17_200945) do t.index ["code"], name: "index_sdg_goals_on_code", unique: true end + create_table "sdg_local_target_translations", force: :cascade do |t| + t.bigint "sdg_local_target_id", null: false + t.string "locale", null: false + t.string "title" + t.text "description" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["locale"], name: "index_sdg_local_target_translations_on_locale" + t.index ["sdg_local_target_id"], name: "index_sdg_local_target_translations_on_sdg_local_target_id" + end + + create_table "sdg_local_targets", force: :cascade do |t| + t.bigint "target_id" + t.string "code" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["code"], name: "index_sdg_local_targets_on_code", unique: true + t.index ["target_id"], name: "index_sdg_local_targets_on_target_id" + end + create_table "sdg_relations", force: :cascade do |t| t.string "related_sdg_type" t.bigint "related_sdg_id" diff --git a/spec/factories/sdg.rb b/spec/factories/sdg.rb index 5e46622e8..9495a4114 100644 --- a/spec/factories/sdg.rb +++ b/spec/factories/sdg.rb @@ -6,4 +6,12 @@ FactoryBot.define do factory :sdg_target, class: "SDG::Target" do sequence(:code, 1) { |n| "#{n}.#{n}" } end + + factory :sdg_local_target, class: "SDG::LocalTarget" do + code { "1.1.1" } + sequence(:title) { |n| "Local Target #{n} title" } + sequence(:description) { |n| "Help for Local Target #{n}" } + + target { SDG::Target[code.rpartition(".").first] } + end end diff --git a/spec/models/sdg/local_target_spec.rb b/spec/models/sdg/local_target_spec.rb new file mode 100644 index 000000000..24c51c683 --- /dev/null +++ b/spec/models/sdg/local_target_spec.rb @@ -0,0 +1,77 @@ +require "rails_helper" + +describe SDG::LocalTarget do + describe "Concerns" do + it_behaves_like "globalizable", :sdg_local_target + end + + it "is valid" do + expect(build(:sdg_local_target)).to be_valid + end + + it "is not valid without a title" do + expect(build(:sdg_local_target, title: nil)).not_to be_valid + end + + it "is not valid without a description" do + expect(build(:sdg_local_target, description: nil)).not_to be_valid + end + + it "is not valid without a code" do + expect(build(:sdg_local_target, code: nil, target: SDG::Target[1.1])).not_to be_valid + end + + it "is not valid when code does not include associated target code" do + local_target = build(:sdg_local_target, code: "1.6.1", target: SDG::Target[1.1]) + + expect(local_target).not_to be_valid + expect(local_target.errors.full_messages).to include "Code must start with the same code as its target followed by a dot and end with a number" + end + + it "is not valid when local target code part is not a number" do + local_target = build(:sdg_local_target, code: "1.1.A", target: SDG::Target[1.1]) + + expect(local_target).not_to be_valid + expect(local_target.errors.full_messages).to include "Code must start with the same code as its target followed by a dot and end with a number" + end + + it "is not valid if code is not unique" do + create(:sdg_local_target, code: "1.1.1") + local_target = build(:sdg_local_target, code: "1.1.1") + + expect(local_target).not_to be_valid + expect(local_target.errors.full_messages).to include "Code has already been taken" + end + + it "is not valid without a target" do + expect(build(:sdg_local_target, target: nil)).not_to be_valid + end + + describe "#goal" do + it "returns the target goal" do + local_target = create(:sdg_local_target, code: "1.1.1") + + expect(local_target.goal).to eq(SDG::Goal[1]) + end + end + + describe "#<=>" do + let(:local_target,) { create(:sdg_local_target, code: "10.B.10") } + + it "compares using the target first" do + lesser_local_target = create(:sdg_local_target, code: "10.A.1") + greater_local_target = create(:sdg_local_target, code: "11.1.1") + + expect(local_target).to be > lesser_local_target + expect(local_target).to be < greater_local_target + end + + it "compares using the local target code when the target is the same" do + lesser_local_target = create(:sdg_local_target, code: "10.B.9") + greater_local_target = create(:sdg_local_target, code: "10.B.11") + + expect(local_target).to be > lesser_local_target + expect(local_target).to be < greater_local_target + end + end +end diff --git a/spec/models/sdg/relatable_spec.rb b/spec/models/sdg/relatable_spec.rb index 4b87971be..e6fd1b8d0 100644 --- a/spec/models/sdg/relatable_spec.rb +++ b/spec/models/sdg/relatable_spec.rb @@ -3,8 +3,10 @@ require "rails_helper" describe SDG::Relatable do let(:goal) { SDG::Goal[1] } let(:target) { SDG::Target["1.2"] } + let(:local_target) { create(:sdg_local_target, code: "1.2.1") } let(:another_goal) { SDG::Goal[2] } let(:another_target) { SDG::Target["2.3"] } + let(:another_local_target) { create(:sdg_local_target, code: "2.3.1") } let(:relatable) { create(:proposal) } @@ -44,12 +46,32 @@ describe SDG::Relatable do end end + describe "#sdg_local_targets" do + it "can assign local targets to a model" do + relatable.sdg_local_targets = [local_target, another_local_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 local_target + expect(SDG::Relation.last.related_sdg).to eq another_local_target + end + + it "can obtain the list of local targets" do + relatable.sdg_local_targets = [local_target, another_local_target] + + expect(relatable.reload.sdg_local_targets).to match_array [local_target, another_local_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] + relatable.sdg_local_targets = [local_target, another_local_target] - expect(relatable.reload.related_sdgs).to match_array [goal, another_goal, target, another_target] + related_sdgs = [goal, another_goal, target, another_target, local_target, another_local_target] + expect(relatable.reload.related_sdgs).to match_array related_sdgs end end end