diff --git a/app/logical/bulk_update_request_processor.rb b/app/logical/bulk_update_request_processor.rb index 741e8f38b..7e3a8e7b2 100644 --- a/app/logical/bulk_update_request_processor.rb +++ b/app/logical/bulk_update_request_processor.rb @@ -50,6 +50,10 @@ class BulkUpdateRequestProcessor [:change_category, Tag.normalize_name($1), $2.downcase] when /\Anuke (\S+)\z/i [:nuke, $1] + when /\Adeprecate (\S+)\z/i + [:deprecate, $1] + when /\Aundeprecate (\S+)\z/i + [:undeprecate, $1] else [:invalid_line, line] end @@ -125,6 +129,18 @@ class BulkUpdateRequestProcessor when :nuke # okay + when :deprecate + tag = Tag.find_by_name(args[0]) + if tag.is_deprecated? + errors.add(:base, "Can't deprecate #{args[0]} (tag is already deprecated)") + end + + when :undeprecate + tag = Tag.find_by_name(args[0]) + if !tag.is_deprecated? + errors.add(:base, "Can't undeprecate #{args[0]} (tag is not deprecated)") + end + when :invalid_line errors.add(:base, "Invalid line: #{args[0]}") @@ -182,6 +198,16 @@ class BulkUpdateRequestProcessor tag = Tag.find_or_create_by_name(args[0]) tag.update!(category: Tag.categories.value_for(args[1])) + when :deprecate + tag = Tag.find_or_create_by_name(args[0]) + tag.update!(is_deprecated: true) + TagImplication.active.where(consequent_name: tag.name).each { |ti| ti.reject!(User.system) } + TagImplication.active.where(antecedent_name: tag.name).each { |ti| ti.reject!(User.system) } + + when :undeprecate + tag = Tag.find_or_create_by_name(args[0]) + tag.update!(is_deprecated: false) + else # should never happen raise Error, "Unknown command: #{command}" @@ -204,7 +230,7 @@ class BulkUpdateRequestProcessor [args[0], args[1]] when :mass_update PostQuery.new(args[0]).tag_names + PostQuery.new(args[1]).tag_names - when :nuke + when :nuke, :deprecate, :undeprecate PostQuery.new(args[0]).tag_names when :change_category args[0] @@ -240,6 +266,8 @@ class BulkUpdateRequestProcessor else "nuke {{#{args[0]}}}" end + when :deprecate, :undeprecate + "#{command.to_s} [[#{args[0]}]]" when :change_category "category [[#{args[0]}]] -> #{args[1]}" else diff --git a/app/models/mod_action.rb b/app/models/mod_action.rb index a305e665f..4978e0c0e 100644 --- a/app/models/mod_action.rb +++ b/app/models/mod_action.rb @@ -60,6 +60,8 @@ class ModAction < ApplicationRecord tag_implication_create: 140, tag_implication_update: 141, # XXX unused tag_implication_delete: 142, + tag_deprecate: 240, + tag_undeprecate: 242, ip_ban_create: 160, ip_ban_delete: 162, ip_ban_undelete: 163, diff --git a/app/models/post.rb b/app/models/post.rb index e629a9e61..9f981b194 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -411,6 +411,7 @@ class Post < ApplicationRecord normalized_tags = Tag.convert_cosplay_tags(normalized_tags) normalized_tags += Tag.create_for_list(Tag.automatic_tags_for(normalized_tags)) normalized_tags += TagImplication.tags_implied_by(normalized_tags).map(&:name) + normalized_tags -= added_deprecated_tags normalized_tags = normalized_tags.compact.uniq.sort normalized_tags = Tag.create_for_list(normalized_tags) self.tag_string = normalized_tags.join(" ") @@ -428,6 +429,16 @@ class Post < ApplicationRecord tag_names - invalid_tags.map(&:name) end + def added_deprecated_tags + added_deprecated_tags = added_tags.select(&:is_deprecated) + if added_deprecated_tags.present? + added_deprecated_tags_list = added_deprecated_tags.map { |t| "[[#{t.name}]]" }.to_sentence + warnings.add(:base, "The following tags are deprecated and could not be added: #{added_deprecated_tags_list}") + end + + added_deprecated_tags.pluck(:name) + end + def remove_negated_tags(tags) @negated_tags, tags = tags.partition {|x| x =~ /\A-/i} @negated_tags = @negated_tags.map {|x| x[1..-1]} diff --git a/app/models/tag.rb b/app/models/tag.rb index 402953306..3114ffbaa 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -16,6 +16,7 @@ class Tag < ApplicationRecord validates :category, inclusion: { in: TagCategory.category_ids } before_create :create_character_tag_for_cosplay_tag, if: :is_cosplay_tag? + before_save :create_mod_action after_save :update_category_cache, if: :saved_change_to_category? after_save :update_category_post_counts, if: :saved_change_to_category? @@ -217,6 +218,17 @@ class Tag < ApplicationRecord end end + concerning :DeprecationMethods do + def create_mod_action + return if CurrentUser.user == User.system + if is_deprecated_was == true and is_deprecated == false + ModAction.log("marked the tag [[#{name}]] as not deprecated", :tag_undeprecate) + elsif is_deprecated_was == false and is_deprecated == true + ModAction.log("marked the tag [[#{name}]] as deprecated", :tag_deprecate) + end + end + end + module SearchMethods def autocorrect_matches(name) fuzzy_name_matches(name).order_similarity(name) @@ -265,7 +277,7 @@ class Tag < ApplicationRecord end def search(params) - q = search_attributes(params, :id, :created_at, :updated_at, :category, :post_count, :name, :wiki_page, :artist, :antecedent_alias, :consequent_aliases, :antecedent_implications, :consequent_implications, :dtext_links) + q = search_attributes(params, :id, :created_at, :updated_at, :is_deprecated, :category, :post_count, :name, :wiki_page, :artist, :antecedent_alias, :consequent_aliases, :antecedent_implications, :consequent_implications, :dtext_links) if params[:fuzzy_name_matches].present? q = q.fuzzy_name_matches(params[:fuzzy_name_matches]) diff --git a/app/policies/tag_policy.rb b/app/policies/tag_policy.rb index 563279321..e299ac8f9 100644 --- a/app/policies/tag_policy.rb +++ b/app/policies/tag_policy.rb @@ -7,7 +7,14 @@ class TagPolicy < ApplicationPolicy (user.is_member? && record.post_count < 50) end + def can_change_deprecated_status? + user.is_admin? || (record.post_count == 0 && !record.is_deprecated?) + end + def permitted_attributes - [(:category if can_change_category?)].compact + [ + (:category if can_change_category?), + (:is_deprecated if can_change_deprecated_status?), + ].compact end end diff --git a/app/views/posts/index.html.erb b/app/views/posts/index.html.erb index e3224bac0..3653efbe8 100644 --- a/app/views/posts/index.html.erb +++ b/app/views/posts/index.html.erb @@ -121,6 +121,10 @@ <%= format_text(wiki_page.body) %> <%= render "tag_relationships/alias_and_implication_list", tag: wiki_page.tag %> + <% if wiki_page.tag&.is_deprecated? %> +

This tag is <%= link_to "deprecated", wiki_page_path("help:deprecation_notice") %> and can't be added to new posts.

+ <% end %> +

<%= link_to_wiki "View wiki", wiki_page.title, id: "view-wiki-link" %>

@@ -154,6 +158,10 @@

There is no wiki page yet for the tag <%= link_to_wiki @post_set.tag.pretty_name %>. <%= link_to "Create new wiki page", new_wiki_page_path(wiki_page: { title: @post_set.tag.name }), rel: "nofollow" %>.

<%= render "tag_relationships/alias_and_implication_list", tag: @post_set.tag %> + + <% if @post_set.tag&.is_deprecated? %> +

This tag is <%= link_to "deprecated", wiki_page_path("help:deprecation_notice") %> and can't be added to new posts.

+ <% end %> <% end %> diff --git a/app/views/tags/_search.html.erb b/app/views/tags/_search.html.erb index 8c57dc61e..32729a8bd 100644 --- a/app/views/tags/_search.html.erb +++ b/app/views/tags/_search.html.erb @@ -2,6 +2,7 @@ <%= f.input :name_or_alias_matches, label: "Name", input_html: { value: params[:search][:name_or_alias_matches], data: { autocomplete: "tag" } } %> <%= f.input :category, label: "Category", collection: TagCategory.canonical_mapping.to_a, include_blank: true,selected: params[:search][:category] %> <%= f.input :order, collection: [%w[Newest date], %w[Count count], %w[Name name]], include_blank: false, selected: params[:search][:order] %> + <%= f.input :is_deprecated, label: "Is deprecated?", collection: %w[yes no], include_blank: true, selected: params[:search][:is_deprecated] %> <%= f.input :hide_empty, label: "Hide empty?", collection: %w[yes no], selected: params[:search][:hide_empty] %> <%= f.input :has_wiki_page, label: "Has wiki?", collection: %w[yes no], include_blank: true, selected: params[:search][:has_wiki_page] %> <%= f.input :has_artist, label: "Has artist?", collection: %w[yes no], include_blank: true, selected: params[:search][:has_artist] %> diff --git a/app/views/tags/edit.html.erb b/app/views/tags/edit.html.erb index 6cb39264c..0929dbd45 100644 --- a/app/views/tags/edit.html.erb +++ b/app/views/tags/edit.html.erb @@ -5,10 +5,20 @@ <%= edit_form_for(@tag) do |f| %> <% if policy(@tag).can_change_category? %> <%= f.input :category, collection: TagCategory.canonical_mapping.to_a, include_blank: false %> - <%= f.button :submit, "Submit" %> <% else %>

Create a <%= link_to "bulk update request", new_bulk_update_request_path(bulk_update_request: { script: "category #{@tag.name} -> general" }) %> to change this tag's category.

<% end %> + + <% if policy(@tag).can_change_deprecated_status? %> + <%= f.input :is_deprecated, :collection => [["No", "false"], ["Yes", "true"]], :include_blank => false %> + <% else %> + <% if @tag.is_deprecated? %> +

Create a <%= link_to "bulk update request", new_bulk_update_request_path(bulk_update_request: { script: "deprecate #{@tag.name}" }) %> to mark this tag as not deprecated.

+ <% else %> +

Create a <%= link_to "bulk update request", new_bulk_update_request_path(bulk_update_request: { script: "undeprecate #{@tag.name}" }) %> to mark this tag as deprecated.

+ <% end %> + <% end %> + <%= f.button :submit, "Submit" %> <% end %> diff --git a/app/views/tags/show.html.erb b/app/views/tags/show.html.erb index cc65cfb23..ab7908481 100644 --- a/app/views/tags/show.html.erb +++ b/app/views/tags/show.html.erb @@ -8,6 +8,9 @@ diff --git a/app/views/wiki_pages/new.html.erb b/app/views/wiki_pages/new.html.erb index f5077534b..09ce961c3 100644 --- a/app/views/wiki_pages/new.html.erb +++ b/app/views/wiki_pages/new.html.erb @@ -18,6 +18,11 @@ <% end %> <%= render "tag_relationships/alias_and_implication_list", tag: @wiki_page.tag %> + + <% if @wiki_page.tag&.is_deprecated? %> +

This tag is <%= link_to "deprecated", wiki_page_path("help:deprecation_notice") %> and can't be added to new posts.

+ <% end %> + <%= render "wiki_pages/posts", wiki_page: @wiki_page %> <% end %> diff --git a/app/views/wiki_pages/show.html.erb b/app/views/wiki_pages/show.html.erb index e2cdcf11d..d3d01eaa2 100644 --- a/app/views/wiki_pages/show.html.erb +++ b/app/views/wiki_pages/show.html.erb @@ -37,6 +37,10 @@ <% end %> <%= render "tag_relationships/alias_and_implication_list", tag: @wiki_page.tag %> + + <% if @wiki_page.tag&.is_deprecated? %> +

This tag is <%= link_to "deprecated", wiki_page_path("help:deprecation_notice") %> and can't be added to new posts.

+ <% end %> <%= render "wiki_pages/posts", wiki_page: @wiki_page %> diff --git a/db/migrate/20220407203236_add_is_deprecated_to_tags.rb b/db/migrate/20220407203236_add_is_deprecated_to_tags.rb new file mode 100644 index 000000000..5ae53c4d5 --- /dev/null +++ b/db/migrate/20220407203236_add_is_deprecated_to_tags.rb @@ -0,0 +1,7 @@ +class AddIsDeprecatedToTags < ActiveRecord::Migration[7.0] + def change + ApplicationRecord.without_timeout do + add_column :tags, :is_deprecated, :boolean, null: false, default: false + end + end +end diff --git a/db/structure.sql b/db/structure.sql index 109fa33b5..181769d4b 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -5796,6 +5796,7 @@ INSERT INTO "schema_migrations" (version) VALUES ('20220211075129'), ('20220318082614'), ('20220403042706'), -('20220403220558'); +('20220403220558'), +('20220407203236'); diff --git a/test/functional/tags_controller_test.rb b/test/functional/tags_controller_test.rb index 266e0cd1b..58e2b5fea 100644 --- a/test/functional/tags_controller_test.rb +++ b/test/functional/tags_controller_test.rb @@ -119,6 +119,51 @@ class TagsControllerTest < ActionDispatch::IntegrationTest end end + context "for deprecation" do + setup do + @deprecated_tag = create(:tag, name: "bad_tag", category: Tag.categories.general, post_count: 0, is_deprecated: true) + @nondeprecated_tag = create(:tag, name: "log", category: Tag.categories.general, post_count: 0) + @normal_tag = create(:tag, name: "random", category: Tag.categories.general, post_count: 1000) + @normal_user = create(:user) + @admin = create(:admin_user) + end + + should "not remove deprecated status if the user is not an admin" do + put_auth tag_path(@deprecated_tag), @normal_user, params: {tag: { is_deprecated: false }} + + assert_response 403 + assert(true, @deprecated_tag.reload.is_deprecated?) + end + + should "remove the deprecated status if the user is admin" do + put_auth tag_path(@deprecated_tag), @admin, params: {tag: { is_deprecated: false }} + + assert_redirected_to @deprecated_tag + assert(false, @deprecated_tag.reload.is_deprecated?) + end + + should "allow marking a tag as deprecated if it's empty" do + put_auth tag_path(@nondeprecated_tag), @normal_user, params: {tag: { is_deprecated: true }} + + assert_redirected_to @nondeprecated_tag + assert(true, @nondeprecated_tag.reload.is_deprecated?) + end + + should "not allow marking a tag as deprecated if it's not empty" do + put_auth tag_path(@normal_tag), @normal_user, params: {tag: { is_deprecated: true }} + + assert_response 403 + assert(false, @normal_tag.reload.is_deprecated?) + end + + should "allow admins to mark tags as deprecated" do + put_auth tag_path(@normal_tag), @admin, params: {tag: { is_deprecated: true }} + + assert_redirected_to @normal_tag + assert(true, @normal_tag.reload.is_deprecated?) + end + end + should "not change category when the tag is too large to be changed by a builder" do @tag.update(category: Tag.categories.general, post_count: 1001) put_auth tag_path(@tag), @user, params: {:tag => {:category => Tag.categories.artist}} diff --git a/test/unit/bulk_update_request_test.rb b/test/unit/bulk_update_request_test.rb index c498fcbda..3ebe44776 100644 --- a/test/unit/bulk_update_request_test.rb +++ b/test/unit/bulk_update_request_test.rb @@ -446,6 +446,34 @@ class BulkUpdateRequestTest < ActiveSupport::TestCase end end + context "the deprecate command" do + should "deprecate the tag" do + @tag = create(:tag, name: "silver_hair") + @bur = create_bur!("deprecate silver_hair", @admin) + + assert_equal(true, @tag.reload.is_deprecated?) + end + + should "remove implications" do + @ti1 = create(:tag_implication, antecedent_name: "silver_hair", consequent_name: "old_woman") + @ti2 = create(:tag_implication, antecedent_name: "my_literal_dog", consequent_name: "silver_hair") + @bur = create_bur!("deprecate silver_hair", @admin) + + assert_equal("deleted", @ti1.reload.status) + assert_equal("deleted", @ti2.reload.status) + assert_equal("approved", @bur.reload.status) + end + end + + context "the undeprecate command" do + should "undeprecate the tag" do + @tag = create(:tag, name: "silver_hair", is_deprecated: true) + @bur = create_bur!("undeprecate silver_hair", @admin) + + assert_equal(false, @tag.reload.is_deprecated?) + end + end + context "that contains a mass update followed by an alias" do should "make the alias take effect after the mass update" do @p1 = create(:post, tag_string: "maid_dress") diff --git a/test/unit/post_test.rb b/test/unit/post_test.rb index b678a048f..b910ad631 100644 --- a/test/unit/post_test.rb +++ b/test/unit/post_test.rb @@ -479,6 +479,23 @@ class PostTest < ActiveSupport::TestCase end end + context "tagged with a deprecated tag" do + should "not remove the tag if the tag was already in the post" do + bad_tag = create(:tag, name: "bad_tag") + old_post = FactoryBot.create(:post, tag_string: "bad_tag") + bad_tag.update!(is_deprecated: true) + old_post.update!(tag_string: "asd bad_tag") + assert_equal("asd bad_tag", old_post.reload.tag_string) + + end + + should "not add the tag if it is being added" do + create(:tag, name: "a_bad_tag", is_deprecated: true) + @post.update!(tag_string: "asd a_bad_tag") + assert_equal("asd", @post.reload.tag_string) + end + end + context "tagged with a metatag" do context "for typing a tag" do setup do