From 143f06f2c600bc9aa94c4d64ca84cd9a06b75b9e Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Mon, 29 Apr 2024 15:50:55 +0530 Subject: [PATCH] FEATURE: Allow watched words to be created as a group (#26632) At the moment, there is no way to create a group of related watched words together. If a user needed a set of words to be created together, they'll have to create them individually one at a time. This change attempts to allow related watched words to be created as a group. The idea here is to have a list of words be tied together via a common `WatchedWordGroup` record. Given a list of words, a `WatchedWordGroup` record is created and assigned to each `WatchedWord` record. The existing WatchedWord creation behaviour remains largely unchanged. Co-authored-by: Selase Krakani Co-authored-by: Martin Brennan --- .../addon/components/watched-word-form.hbs | 17 ++-- .../addon/components/watched-word-form.js | 82 +++++++++---------- .../admin/addon/models/watched-word.js | 2 +- .../acceptance/admin-watched-words-test.js | 44 +++++++--- .../tests/helpers/create-pretender.js | 22 ++++- .../addon/components/watched-words.js | 20 +++++ .../stylesheets/common/admin/staff_logs.scss | 1 + .../admin/watched_words_controller.rb | 32 ++++++-- app/models/user_history.rb | 6 ++ app/models/watched_word.rb | 29 +++++-- app/models/watched_word_group.rb | 51 ++++++++++++ app/serializers/watched_word_serializer.rb | 2 +- config/locales/client.en.yml | 7 +- ...230515103515_create_watched_word_groups.rb | 11 +++ ..._watched_word_group_id_to_watched_words.rb | 8 ++ .../watched_word_group_fabricator.rb | 3 + spec/models/watched_word_group_spec.rb | 56 +++++++++++++ .../admin/watched_words_controller_spec.rb | 22 ++++- 18 files changed, 327 insertions(+), 88 deletions(-) create mode 100644 app/assets/javascripts/select-kit/addon/components/watched-words.js create mode 100644 app/models/watched_word_group.rb create mode 100644 db/migrate/20230515103515_create_watched_word_groups.rb create mode 100644 db/migrate/20230515131111_add_watched_word_group_id_to_watched_words.rb create mode 100644 spec/fabricators/watched_word_group_fabricator.rb create mode 100644 spec/models/watched_word_group_spec.rb diff --git a/app/assets/javascripts/admin/addon/components/watched-word-form.hbs b/app/assets/javascripts/admin/addon/components/watched-word-form.hbs index 72412dac6b0..0335ed7b643 100644 --- a/app/assets/javascripts/admin/addon/components/watched-word-form.hbs +++ b/app/assets/javascripts/admin/addon/components/watched-word-form.hbs @@ -1,14 +1,13 @@
-
diff --git a/app/assets/javascripts/admin/addon/components/watched-word-form.js b/app/assets/javascripts/admin/addon/components/watched-word-form.js index f2b0f819c81..ff19643f61a 100644 --- a/app/assets/javascripts/admin/addon/components/watched-word-form.js +++ b/app/assets/javascripts/admin/addon/components/watched-word-form.js @@ -1,11 +1,11 @@ import Component from "@ember/component"; import { action } from "@ember/object"; -import { equal, not } from "@ember/object/computed"; -import { schedule } from "@ember/runloop"; +import { empty, equal } from "@ember/object/computed"; import { service } from "@ember/service"; import { isEmpty } from "@ember/utils"; import { classNames, tagName } from "@ember-decorators/component"; import { observes } from "@ember-decorators/object"; +import { popupAjaxError } from "discourse/lib/ajax-error"; import discourseComputed from "discourse-common/utils/decorators"; import I18n from "discourse-i18n"; import WatchedWord from "admin/models/watched-word"; @@ -18,10 +18,11 @@ export default class WatchedWordForm extends Component { formSubmitted = false; actionKey = null; showMessage = false; - selectedTags = null; isCaseSensitive = false; + selectedTags = []; + words = []; - @not("word") submitDisabled; + @empty("words") submitDisabled; @equal("actionKey", "replace") canReplace; @@ -29,11 +30,6 @@ export default class WatchedWordForm extends Component { @equal("actionKey", "link") canLink; - didInsertElement() { - super.didInsertElement(...arguments); - this.set("selectedTags", []); - } - @discourseComputed("siteSettings.watched_words_regular_expressions") placeholderKey(watchedWordsRegularExpressions) { if (watchedWordsRegularExpressions) { @@ -43,29 +39,38 @@ export default class WatchedWordForm extends Component { } } - @observes("word") + @observes("words.[]") removeMessage() { - if (this.showMessage && !isEmpty(this.word)) { + if (this.showMessage && !isEmpty(this.words)) { this.set("showMessage", false); } } - @discourseComputed("word") - isUniqueWord(word) { - const words = this.filteredContent || []; - const filtered = words.filter( - (content) => content.action === this.actionKey - ); - return filtered.every((content) => { - if (content.case_sensitive === true) { - return content.word !== word; - } - return content.word.toLowerCase() !== word.toLowerCase(); + @observes("actionKey") + actionChanged() { + this.setProperties({ + showMessage: false, }); } - focusInput() { - schedule("afterRender", () => this.element.querySelector("input").focus()); + @discourseComputed("words.[]") + isUniqueWord(words) { + const existingWords = this.filteredContent || []; + const filtered = existingWords.filter( + (content) => content.action === this.actionKey + ); + + const duplicate = filtered.find((content) => { + if (content.case_sensitive === true) { + return words.includes(content.word); + } else { + return words + .map((w) => w.toLowerCase()) + .includes(content.word.toLowerCase()); + } + }); + + return !duplicate; } @action @@ -90,7 +95,7 @@ export default class WatchedWordForm extends Component { this.set("formSubmitted", true); const watchedWord = WatchedWord.create({ - word: this.word, + words: this.words, replacement: this.canReplace || this.canTag || this.canLink ? this.replacement @@ -103,30 +108,23 @@ export default class WatchedWordForm extends Component { .save() .then((result) => { this.setProperties({ - word: "", + words: [], replacement: "", - formSubmitted: false, selectedTags: [], showMessage: true, message: I18n.t("admin.watched_words.form.success"), isCaseSensitive: false, }); - this.action(WatchedWord.create(result)); - this.focusInput(); + if (result.words) { + result.words.forEach((word) => { + this.action(WatchedWord.create(word)); + }); + } else { + this.action(result); + } }) - .catch((e) => { - this.set("formSubmitted", false); - const message = e.jqXHR.responseJSON?.errors - ? I18n.t("generic_error_with_reason", { - error: e.jqXHR.responseJSON.errors.join(". "), - }) - : I18n.t("generic_error"); - this.dialog.alert({ - message, - didConfirm: () => this.focusInput(), - didCancel: () => this.focusInput(), - }); - }); + .catch(popupAjaxError) + .finally(this.set("formSubmitted", false)); } } } diff --git a/app/assets/javascripts/admin/addon/models/watched-word.js b/app/assets/javascripts/admin/addon/models/watched-word.js index e27fe142e48..d59f3e6a650 100644 --- a/app/assets/javascripts/admin/addon/models/watched-word.js +++ b/app/assets/javascripts/admin/addon/models/watched-word.js @@ -34,7 +34,7 @@ export default class WatchedWord extends EmberObject { { type: this.id ? "PUT" : "POST", data: { - word: this.word, + words: this.words, replacement: this.replacement, action_key: this.action, case_sensitive: this.isCaseSensitive, diff --git a/app/assets/javascripts/discourse/tests/acceptance/admin-watched-words-test.js b/app/assets/javascripts/discourse/tests/acceptance/admin-watched-words-test.js index 154c96468fc..9b9b2f510d8 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/admin-watched-words-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/admin-watched-words-test.js @@ -1,4 +1,4 @@ -import { click, fillIn, visit } from "@ember/test-helpers"; +import { click, fillIn, triggerKeyEvent, visit } from "@ember/test-helpers"; import { test } from "qunit"; import { acceptance, @@ -57,20 +57,34 @@ acceptance("Admin - Watched Words", function (needs) { test("add words", async function (assert) { await visit("/admin/customize/watched_words/action/block"); + const submitButton = query(".watched-word-form button"); await click(".show-words-checkbox"); - await fillIn(".watched-word-form input", "poutine"); - await click(".watched-word-form button"); + await click(".select-kit-header.multi-select-header"); - let found = []; - [...queryAll(".watched-words-list .watched-word")].forEach((elem) => { - if (elem.innerText.trim() === "poutine") { - found.push(true); + await fillIn(".select-kit-filter input", "poutine"); + await triggerKeyEvent(".select-kit-filter input", "keydown", "Enter"); + + await fillIn(".select-kit-filter input", "cheese"); + await triggerKeyEvent(".select-kit-filter input", "keydown", "Enter"); + + assert.equal( + query(".select-kit-header-wrapper .formatted-selection").innerText, + "poutine, cheese", + "has the correct words in the input field" + ); + + await click(submitButton); + + const words = [...queryAll(".watched-words-list .watched-word")].map( + (elem) => { + return elem.innerText.trim(); } - }); + ); - assert.strictEqual(found.length, 1); - assert.strictEqual(count(".watched-words-list .case-sensitive"), 0); + assert.ok(words.includes("poutine"), "has word 'poutine'"); + assert.ok(words.includes("cheese"), "has word 'cheese'"); + assert.equal(count(".watched-words-list .case-sensitive"), 0); }); test("add case-sensitive words", async function (assert) { @@ -82,7 +96,11 @@ acceptance("Admin - Watched Words", function (needs) { "Add button is disabled by default" ); await click(".show-words-checkbox"); - await fillIn(".watched-word-form input", "Discourse"); + + await click(".select-kit-header.multi-select-header"); + await fillIn(".select-kit-filter input", "Discourse"); + await triggerKeyEvent(".select-kit-filter input", "keydown", "Enter"); + await click(".case-sensitivity-checkbox"); assert.strictEqual( submitButton.disabled, @@ -95,7 +113,9 @@ acceptance("Admin - Watched Words", function (needs) { .dom(".watched-words-list .watched-word") .hasText(`Discourse ${I18n.t("admin.watched_words.case_sensitive")}`); - await fillIn(".watched-word-form input", "discourse"); + await click(".select-kit-header.multi-select-header"); + await fillIn(".select-kit-filter input", "discourse"); + await triggerKeyEvent(".select-kit-filter input", "keydown", "Enter"); await click(".case-sensitivity-checkbox"); await click(submitButton); diff --git a/app/assets/javascripts/discourse/tests/helpers/create-pretender.js b/app/assets/javascripts/discourse/tests/helpers/create-pretender.js index b6203453fd9..d651d3978c4 100644 --- a/app/assets/javascripts/discourse/tests/helpers/create-pretender.js +++ b/app/assets/javascripts/discourse/tests/helpers/create-pretender.js @@ -1,3 +1,4 @@ +import EmberObject from "@ember/object"; import Pretender from "pretender"; import User from "discourse/models/user"; import getURL from "discourse-common/lib/get-url"; @@ -972,9 +973,24 @@ export function applyDefaultHandlers(pretender) { pretender.delete("/admin/customize/watched_words/:id.json", success); pretender.post("/admin/customize/watched_words.json", (request) => { - const result = parsePostData(request.requestBody); - result.id = new Date().getTime(); - result.case_sensitive = result.case_sensitive === "true"; + const requestData = parsePostData(request.requestBody); + + const result = cloneJSON( + fixturesByUrl["/admin/customize/watched_words.json"] + ); + result.words = []; + + const words = requestData.words || requestData["words[]"]; + words.forEach((word, index) => { + result.words[index] = EmberObject.create({ + id: new Date().getTime(), + word, + action: requestData.action_key, + replacement: requestData.replacement, + case_sensitive: requestData.case_sensitive === "true", + }); + }); + return response(200, result); }); diff --git a/app/assets/javascripts/select-kit/addon/components/watched-words.js b/app/assets/javascripts/select-kit/addon/components/watched-words.js new file mode 100644 index 00000000000..0ae5e11d78b --- /dev/null +++ b/app/assets/javascripts/select-kit/addon/components/watched-words.js @@ -0,0 +1,20 @@ +import { computed } from "@ember/object"; +import { makeArray } from "discourse-common/lib/helpers"; +import MultiSelectComponent from "select-kit/components/multi-select"; + +export default MultiSelectComponent.extend({ + pluginApiIdentifiers: ["watched-words"], + classNames: ["watched-word-input-field"], + + selectKitOptions: { + autoInsertNoneItem: false, + fullWidthOnMobile: true, + allowAny: true, + none: "admin.watched_words.form.words_or_phrases", + }, + + @computed("value.[]") + get content() { + return makeArray(this.value).map((x) => this.defaultItem(x, x)); + }, +}); diff --git a/app/assets/stylesheets/common/admin/staff_logs.scss b/app/assets/stylesheets/common/admin/staff_logs.scss index 6fb75262cf2..600619b9560 100644 --- a/app/assets/stylesheets/common/admin/staff_logs.scss +++ b/app/assets/stylesheets/common/admin/staff_logs.scss @@ -347,6 +347,7 @@ table.screened-ip-addresses { } .select-kit.multi-select.watched-word-input-field { width: 300px; + margin-bottom: 9px; } + .btn-primary { diff --git a/app/controllers/admin/watched_words_controller.rb b/app/controllers/admin/watched_words_controller.rb index 16d81987334..8800f150fc7 100644 --- a/app/controllers/admin/watched_words_controller.rb +++ b/app/controllers/admin/watched_words_controller.rb @@ -13,19 +13,36 @@ class Admin::WatchedWordsController < Admin::StaffController end def create - watched_word = WatchedWord.create_or_update_word(watched_words_params) - if watched_word.valid? - StaffActionLogger.new(current_user).log_watched_words_creation(watched_word) - render json: watched_word, root: false + opts = watched_words_params + action = opts[:action] || self.class.actions[opts[:action_key].to_sym] + words = opts.delete(:words) + + watched_word_group = WatchedWordGroup.new(action: action) + watched_word_group.create_or_update_members(words, opts) + + if watched_word_group.valid? + StaffActionLogger.new(current_user).log_watched_words_creation(watched_word_group) + render_json_dump WatchedWordListSerializer.new( + watched_word_group.watched_words, + scope: guardian, + root: false, + ) else - render_json_error(watched_word) + render_json_error(watched_word_group) end end def destroy watched_word = WatchedWord.find_by(id: params[:id]) raise Discourse::InvalidParameters.new(:id) unless watched_word - watched_word.destroy! + + watched_word_group = watched_word.watched_word_group + if watched_word_group&.watched_words&.count == 1 + watched_word_group.destroy! + else + watched_word.destroy! + end + StaffActionLogger.new(current_user).log_watched_words_deletion(watched_word) render json: success_json end @@ -100,6 +117,7 @@ class Admin::WatchedWordsController < Admin::StaffController private def watched_words_params - params.permit(:id, :word, :replacement, :action_key, :case_sensitive) + @watched_words_params ||= + params.permit(:id, :replacement, :action, :action_key, :case_sensitive, words: []) end end diff --git a/app/models/user_history.rb b/app/models/user_history.rb index 5b6c6f006cc..78146253b12 100644 --- a/app/models/user_history.rb +++ b/app/models/user_history.rb @@ -141,6 +141,9 @@ class UserHistory < ActiveRecord::Base update_public_sidebar_section: 102, destroy_public_sidebar_section: 103, reset_bounce_score: 104, + create_watched_word_group: 105, + update_watched_word_group: 106, + delete_watched_word_group: 107, ) end @@ -246,6 +249,9 @@ class UserHistory < ActiveRecord::Base deleted_tag chat_channel_status_change chat_auto_remove_membership + create_watched_word_group + update_watched_word_group + delete_watched_word_group ] end diff --git a/app/models/watched_word.rb b/app/models/watched_word.rb index 0ceccd5272d..a1f6b882b04 100644 --- a/app/models/watched_word.rb +++ b/app/models/watched_word.rb @@ -48,6 +48,16 @@ class WatchedWord < ActiveRecord::Base ) end + belongs_to :watched_word_group + + scope :for, + ->(word:) do + where( + "(word ILIKE :word AND case_sensitive = 'f') OR (word LIKE :word AND case_sensitive = 't')", + word: word, + ) + end + def self.create_or_update_word(params) word = normalize_word(params[:word]) word = self.for(word: word).first_or_initialize(word: word) @@ -55,6 +65,7 @@ class WatchedWord < ActiveRecord::Base word.action_key = params[:action_key] if params[:action_key] word.action = params[:action] if params[:action] word.case_sensitive = params[:case_sensitive] if !params[:case_sensitive].nil? + word.watched_word_group_id = params[:watched_word_group_id] word.save word end @@ -102,15 +113,17 @@ end # # Table name: watched_words # -# id :integer not null, primary key -# word :string not null -# action :integer not null -# created_at :datetime not null -# updated_at :datetime not null -# replacement :string -# case_sensitive :boolean default(FALSE), not null +# id :integer not null, primary key +# word :string not null +# action :integer not null +# created_at :datetime not null +# updated_at :datetime not null +# replacement :string +# case_sensitive :boolean default(FALSE), not null +# watched_word_group_id :bigint # # Indexes # -# index_watched_words_on_action_and_word (action,word) UNIQUE +# index_watched_words_on_action_and_word (action,word) UNIQUE +# index_watched_words_on_watched_word_group_id (watched_word_group_id) # diff --git a/app/models/watched_word_group.rb b/app/models/watched_word_group.rb new file mode 100644 index 00000000000..6bee2d22b1d --- /dev/null +++ b/app/models/watched_word_group.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +class WatchedWordGroup < ActiveRecord::Base + validates :action, presence: true + + has_many :watched_words, dependent: :destroy + + def self.actions + WatchedWord.actions + end + + def create_or_update_members(words, params) + WatchedWordGroup.transaction do + self.action_key = params[:action_key] if params[:action_key] + self.action = params[:action] if params[:action] + self.save! if self.changed? + + words.each do |word| + watched_word = + WatchedWord.create_or_update_word( + params.merge(word: word, watched_word_group_id: self.id), + ) + + unless watched_word.valid? + self.errors.merge!(watched_word.errors) + + raise ActiveRecord::Rollback + end + end + end + end + + def action_key=(arg) + self.action = WatchedWordGroup.actions[arg.to_sym] + end + + def action_log_details + action_key = WatchedWord.actions.key(self.action) + "#{action_key} → #{watched_words.pluck(:word).join(", ")}" + end +end + +# == Schema Information +# +# Table name: watched_word_groups +# +# id :bigint not null, primary key +# action :integer not null +# created_at :datetime not null +# updated_at :datetime not null +# diff --git a/app/serializers/watched_word_serializer.rb b/app/serializers/watched_word_serializer.rb index 0ba4f7cc5da..8a1658e285f 100644 --- a/app/serializers/watched_word_serializer.rb +++ b/app/serializers/watched_word_serializer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class WatchedWordSerializer < ApplicationSerializer - attributes :id, :word, :regexp, :replacement, :action, :case_sensitive + attributes :id, :word, :regexp, :replacement, :action, :case_sensitive, :watched_word_group_id def regexp WordWatcher.word_to_regexp(word, engine: :js) diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 729aff4c311..ec558b23254 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -6122,9 +6122,9 @@ en: silence: "Silence new accounts if their very first post contains any of these words. The post will be automatically hidden until staff approves it." link: "Replace words in posts with links." form: - label: "Has word or phrase" - placeholder: "Enter word or phrase (* is a wildcard)" - placeholder_regexp: "regular expression" + label: "Has words or phrases" + placeholder: "words or phrases (* is a wildcard)" + placeholder_regexp: "regular expressions" replace_label: "Replacement" replace_placeholder: "example" tag_label: "Tag" @@ -6137,6 +6137,7 @@ en: upload_successful: "Upload successful. Words have been added." case_sensitivity_label: "Is case-sensitive" case_sensitivity_description: "Only words with matching character casing" + words_or_phrases: "words or phrases" test: button_label: "Test" modal_title: "%{action}: Test Watched Words" diff --git a/db/migrate/20230515103515_create_watched_word_groups.rb b/db/migrate/20230515103515_create_watched_word_groups.rb new file mode 100644 index 00000000000..31542d3282c --- /dev/null +++ b/db/migrate/20230515103515_create_watched_word_groups.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class CreateWatchedWordGroups < ActiveRecord::Migration[7.0] + def change + create_table :watched_word_groups do |t| + t.integer :action, null: false + + t.timestamps + end + end +end diff --git a/db/migrate/20230515131111_add_watched_word_group_id_to_watched_words.rb b/db/migrate/20230515131111_add_watched_word_group_id_to_watched_words.rb new file mode 100644 index 00000000000..b627ba05d7f --- /dev/null +++ b/db/migrate/20230515131111_add_watched_word_group_id_to_watched_words.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class AddWatchedWordGroupIdToWatchedWords < ActiveRecord::Migration[7.0] + def change + add_column :watched_words, :watched_word_group_id, :bigint + add_index :watched_words, :watched_word_group_id + end +end diff --git a/spec/fabricators/watched_word_group_fabricator.rb b/spec/fabricators/watched_word_group_fabricator.rb new file mode 100644 index 00000000000..94a3c2cff8b --- /dev/null +++ b/spec/fabricators/watched_word_group_fabricator.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +Fabricator(:watched_word_group) { action WatchedWord.actions[:block] } diff --git a/spec/models/watched_word_group_spec.rb b/spec/models/watched_word_group_spec.rb new file mode 100644 index 00000000000..98d684617c9 --- /dev/null +++ b/spec/models/watched_word_group_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +RSpec.describe WatchedWordGroup do + fab!(:watched_word_group) + fab!(:watched_word_1) { Fabricate(:watched_word, watched_word_group_id: watched_word_group.id) } + fab!(:watched_word_2) { Fabricate(:watched_word, watched_word_group_id: watched_word_group.id) } + + describe "#create_or_update_members" do + it "updates watched word action" do + words = [watched_word_1.word, watched_word_2.word, "damn", "4sale"] + old_action = watched_word_group.action + watched_words_before_update = watched_word_group.watched_words + + expect(old_action).to eq(WatchedWord.actions[:block]) + expect(watched_words_before_update.map(&:action).uniq).to contain_exactly(old_action) + + watched_word_group.create_or_update_members(words, action_key: :censor) + + expect(watched_word_group.reload.errors).to be_empty + + watched_words = watched_word_group.watched_words + + expect(watched_words.size).to eq(4) + expect(watched_words.map(&:word)).to contain_exactly(*words) + expect(watched_words.map(&:action).uniq).to contain_exactly(WatchedWord.actions[:censor]) + expect(watched_word_group.action).to eq(WatchedWord.actions[:censor]) + end + + it "leaves membership intact if update fails" do + words = [watched_word_1.word, watched_word_2.word, "a" * 120] + old_action = watched_word_group.action + watched_words_before_update = watched_word_group.watched_words + + expect(watched_words_before_update.size).to eq(2) + expect(watched_words_before_update.map(&:word)).to contain_exactly( + watched_word_1.word, + watched_word_2.word, + ) + expect(watched_words_before_update.map(&:action).uniq).to contain_exactly(old_action) + + watched_word_group.create_or_update_members( + words, + action_key: WatchedWord.actions[watched_word_group.action], + ) + + expect(watched_word_group.reload.errors).not_to be_empty + + watched_words = watched_word_group.watched_words + + expect(watched_word_group.action).to eq(old_action) + expect(watched_words.size).to eq(2) + expect(watched_words.map(&:word)).to contain_exactly(watched_word_1.word, watched_word_2.word) + expect(watched_words.map(&:action).uniq).to contain_exactly(old_action) + end + end +end diff --git a/spec/requests/admin/watched_words_controller_spec.rb b/spec/requests/admin/watched_words_controller_spec.rb index 62e93596a8a..41dc5d67d8a 100644 --- a/spec/requests/admin/watched_words_controller_spec.rb +++ b/spec/requests/admin/watched_words_controller_spec.rb @@ -86,6 +86,17 @@ RSpec.describe Admin::WatchedWordsController do expect(WatchedWord.find_by(id: watched_word.id)).to eq(nil) expect(UserHistory.where(action: UserHistory.actions[:watched_word_destroy]).count).to eq(1) end + + it "should delete watched word group if it's the last word" do + watched_word_group = Fabricate(:watched_word_group) + watched_word.update!(watched_word_group: watched_word_group) + + delete "/admin/customize/watched_words/#{watched_word.id}.json" + + expect(response.status).to eq(200) + expect(WatchedWordGroup.exists?(id: watched_word_group.id)).to be_falsey + expect(WatchedWord.exists?(id: watched_word.id)).to be_falsey + end end end @@ -104,17 +115,24 @@ RSpec.describe Admin::WatchedWordsController do before { sign_in(admin) } it "creates a word with default case sensitivity" do - post "/admin/customize/watched_words.json", params: { action_key: "flag", word: "Deals" } + expect { + post "/admin/customize/watched_words.json", + params: { + action_key: "flag", + words: %w[Deals Offer], + } + }.to change { WatchedWord.count }.by(2) expect(response.status).to eq(200) expect(WatchedWord.take.word).to eq("Deals") + expect(WatchedWord.last.word).to eq("Offer") end it "creates a word with the given case sensitivity" do post "/admin/customize/watched_words.json", params: { action_key: "flag", - word: "PNG", + words: ["PNG"], case_sensitive: true, }