From d413ff86d67da489e4acef63015c3ad896310379 Mon Sep 17 00:00:00 2001 From: Zee <50284+zspencer@users.noreply.github.com> Date: Fri, 14 Jul 2023 18:07:35 -0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=B9=20`Journal`:=20`Entries`=20store?= =?UTF-8?q?=20their=20`Keywords`=20(#1663)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * `Journal`: `Entries` may be queried by `Keywords` - https://github.com/zinc-collective/convene/issues/1662 - https://github.com/zinc-collective/convene/issues/1566 This gets us a little bit closer to being able to browse `Entry` by `Keyword`s, since each `Entry` will know it's keywords * `Journal`: `Keyword.search` provides case-insensitive tag search --- app/furniture/journal/entry.rb | 4 ++-- app/furniture/journal/keyword.rb | 12 +++++++++--- ...230713230334_journal_add_keywords_to_entries.rb | 5 +++++ db/schema.rb | 3 ++- spec/factories/furniture/journal.rb | 4 ++++ spec/furniture/journal/entry_spec.rb | 3 ++- spec/furniture/journal/keyword_spec.rb | 14 ++++++++++++++ 7 files changed, 38 insertions(+), 7 deletions(-) create mode 100644 db/migrate/20230713230334_journal_add_keywords_to_entries.rb diff --git a/app/furniture/journal/entry.rb b/app/furniture/journal/entry.rb index 2670b2731..958f6ab1e 100644 --- a/app/furniture/journal/entry.rb +++ b/app/furniture/journal/entry.rb @@ -30,7 +30,7 @@ class Entry < ApplicationRecord belongs_to :journal, inverse_of: :entries has_one :room, through: :journal has_one :space, through: :journal - after_save :extract_keywords, if: :saved_change_to_body? + before_save :extract_keywords, if: :will_save_change_to_body? def published? published_at.present? @@ -52,7 +52,7 @@ def self.renderer end def extract_keywords - journal.keywords.extract_and_create_from!(body) + self.keywords = journal.keywords.extract_and_create_from!(body).pluck(:canonical_keyword) end def to_param diff --git a/app/furniture/journal/keyword.rb b/app/furniture/journal/keyword.rb index bff92119e..e89058681 100644 --- a/app/furniture/journal/keyword.rb +++ b/app/furniture/journal/keyword.rb @@ -6,10 +6,16 @@ class Keyword < ApplicationRecord validates :canonical_keyword, presence: true, uniqueness: {case_sensitive: false, scope: :journal_id} belongs_to :journal, inverse_of: :keywords + scope(:search, lambda do |*keywords| + where("lower(aliases::text)::text[] && ARRAY[?]::text[]", keywords.map(&:downcase)) + .or(where("lower(canonical_keyword) IN (?)", keywords.map(&:downcase))) + end) + def self.extract_and_create_from!(body) - body.scan(/#(\w+)/)&.flatten&.each do |keyword| - next if where(":aliases = ANY (aliases)", aliases: keyword) - .or(where(canonical_keyword: keyword)).exists? + body.scan(/#(\w+)/)&.flatten&.map do |keyword| + existing_keyword = search(keyword).first + + next existing_keyword if existing_keyword.present? find_or_create_by!(canonical_keyword: keyword) end diff --git a/db/migrate/20230713230334_journal_add_keywords_to_entries.rb b/db/migrate/20230713230334_journal_add_keywords_to_entries.rb new file mode 100644 index 000000000..aad25704e --- /dev/null +++ b/db/migrate/20230713230334_journal_add_keywords_to_entries.rb @@ -0,0 +1,5 @@ +class JournalAddKeywordsToEntries < ActiveRecord::Migration[7.0] + def change + add_column :journal_entries, :keywords, :string, array: true + end +end diff --git a/db/schema.rb b/db/schema.rb index a52718609..5c55faff5 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[7.0].define(version: 2023_07_06_003709) do +ActiveRecord::Schema[7.0].define(version: 2023_07_13_230334) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" @@ -114,6 +114,7 @@ t.datetime "published_at" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.string "keywords", array: true t.index ["journal_id"], name: "index_journal_entries_on_journal_id" end diff --git a/spec/factories/furniture/journal.rb b/spec/factories/furniture/journal.rb index 13c418411..ac9b11217 100644 --- a/spec/factories/furniture/journal.rb +++ b/spec/factories/furniture/journal.rb @@ -9,4 +9,8 @@ body { 5.times.map { headline }.join("\n") } journal end + + factory :journal_keyword, class: "Journal::Keyword" do + journal + end end diff --git a/spec/furniture/journal/entry_spec.rb b/spec/furniture/journal/entry_spec.rb index f8ee56790..14c2ca16d 100644 --- a/spec/furniture/journal/entry_spec.rb +++ b/spec/furniture/journal/entry_spec.rb @@ -25,7 +25,7 @@ let(:journal) { entry.journal } context "when the body is changing" do - it "idempotently creates `Keywords` in the `Journal`" do + it "idempotently creates `Keywords` in the `Journal` and `Entry`" do bad_apple = entry.journal.keywords.create!(canonical_keyword: "BadApple", aliases: ["BadApples"]) good_times = entry.journal.keywords.find_by!(canonical_keyword: "GoodTimes") expect do @@ -35,6 +35,7 @@ expect(journal.keywords.where(canonical_keyword: "GoodTimes")).to exist expect(journal.keywords.where(canonical_keyword: "HardCider")).to exist expect(journal.keywords.where(canonical_keyword: "BadApples")).not_to exist + expect(entry.reload.keywords).to eq(["GoodTimes", "HardCider", "BadApple"]) end end end diff --git a/spec/furniture/journal/keyword_spec.rb b/spec/furniture/journal/keyword_spec.rb index dba78f742..4629f944e 100644 --- a/spec/furniture/journal/keyword_spec.rb +++ b/spec/furniture/journal/keyword_spec.rb @@ -6,4 +6,18 @@ it { is_expected.to validate_presence_of(:canonical_keyword) } it { is_expected.to validate_uniqueness_of(:canonical_keyword).case_insensitive.scoped_to(:journal_id) } it { is_expected.to belong_to(:journal).inverse_of(:keywords) } + + describe ".search" do + it "returns the `Keywords` that match either canonicaly or via aliases" do + dog = create(:journal_keyword, canonical_keyword: "Dog", aliases: ["doggo"]) + cat = create(:journal_keyword, canonical_keyword: "Cat", aliases: ["meower"]) + + expect(described_class.search("Doggo")).to include(dog) + expect(described_class.search("Dog")).to include(dog) + expect(described_class.search("Cat")).to include(cat) + expect(described_class.search("Meower")).to include(cat) + expect(described_class.search("Meower", "Dog")).to include(cat) + expect(described_class.search("Meower", "Dog")).to include(dog) + end + end end