From 82f5bb9cdda01b3c9fd47d5f41b91a504893b977 Mon Sep 17 00:00:00 2001 From: Spencer Date: Fri, 13 Dec 2019 10:02:45 -0800 Subject: [PATCH] Mod tooling: add tags via /mod refactor (#5017) * mod view form changes * add error validation for removal tag * add error validation to tag_list count * re-render tag_adjustment form errors * refactor admin delete adjusted tags form * add prompt to tag adjustment form select * front end validations to add/remove tag_adjustment * allow tag_mod to undo own tag adjustment * update/add specs * update send spec * update tag adjustment spec --- app/controllers/moderations_controller.rb | 6 +- app/controllers/tag_adjustments_controller.rb | 15 +- app/models/tag_adjustment.rb | 6 + .../tag_adjustment_creation_service.rb | 5 +- app/views/moderations/mod.html.erb | 141 +++++++++--------- spec/factories/tag_adjustments.rb | 2 +- spec/models/tag_adjustment_spec.rb | 104 ++++++++----- .../tag_adjustment_notification/send_spec.rb | 10 +- .../tag_adjustment_update_service_spec.rb | 2 +- 9 files changed, 167 insertions(+), 124 deletions(-) diff --git a/app/controllers/moderations_controller.rb b/app/controllers/moderations_controller.rb index a40e1584f..d8594de94 100644 --- a/app/controllers/moderations_controller.rb +++ b/app/controllers/moderations_controller.rb @@ -17,12 +17,12 @@ class ModerationsController < ApplicationController def article authorize(User, :moderation_routes?) + @tag_adjustment = TagAdjustment.new @moderatable = Article.find_by(slug: params[:slug]) + @tag_moderator_tags = Tag.with_role(:tag_moderator, current_user) @adjustments = TagAdjustment.where(article_id: @moderatable.id) - @removed_adjustments = @adjustments.filter { |a| a.adjustment_type == "removal" } - @added_adjustments = @adjustments.filter { |a| a.adjustment_type == "addition" } @already_adjusted_tags = @adjustments.map(&:tag_name).join(", ") - @allowed_to_add = @moderatable.class.name == "Article" && (current_user.has_role?(:super_admin) || current_user.has_role?(:tag_moderator, :any)) + @allowed_to_adjust = @moderatable.class.name == "Article" && (current_user.has_role?(:super_admin) || @tag_moderator_tags.any?) render template: "moderations/mod" end diff --git a/app/controllers/tag_adjustments_controller.rb b/app/controllers/tag_adjustments_controller.rb index 8ed51359d..e3a7d16bd 100644 --- a/app/controllers/tag_adjustments_controller.rb +++ b/app/controllers/tag_adjustments_controller.rb @@ -10,15 +10,20 @@ class TagAdjustmentsController < ApplicationController reason_for_adjustment: params[:tag_adjustment][:reason_for_adjustment], ) tag_adjustment = service.tag_adjustment + article = service.article if tag_adjustment.save service.update_tags_and_notify + redirect_to "#{URI.parse(article.path).path}/mod" else - errors = tag_adjustment.errors.full_messages.join(", ") - flash[:error_removal] = errors if tag_adjustment.adjustment_type == "removal" - flash[:error_addition] = errors if tag_adjustment.adjustment_type == "addition" + authorize(User, :moderation_routes?) + @tag_adjustment = tag_adjustment + @moderatable = article + @tag_moderator_tags = Tag.with_role(:tag_moderator, current_user) + @adjustments = TagAdjustment.where(article_id: article.id) + @already_adjusted_tags = @adjustments.map(&:tag_name).join(", ") + @allowed_to_adjust = @moderatable.class.name == "Article" && (current_user.any_admin? || @tag_moderator_tags.any?) + render template: "moderations/mod" end - @article = Article.find(params[:tag_adjustment][:article_id]) - redirect_to "#{URI.parse(@article.path).path}/mod" end def destroy diff --git a/app/models/tag_adjustment.rb b/app/models/tag_adjustment.rb index bb99fea21..1218ac425 100644 --- a/app/models/tag_adjustment.rb +++ b/app/models/tag_adjustment.rb @@ -8,6 +8,7 @@ class TagAdjustment < ApplicationRecord validates :status, inclusion: { in: %w[committed pending committed_and_resolvable resolved] }, presence: true has_many :notifications, as: :notifiable, inverse_of: :notifiable, dependent: :delete_all validate :user_permissions + validate :article_tag_list belongs_to :user belongs_to :tag @@ -26,4 +27,9 @@ class TagAdjustment < ApplicationRecord user.has_role?(:admin) || user.has_role?(:super_admin) end + + def article_tag_list + errors.add(:tag_id, "selected for removal is not a current live tag.") if adjustment_type == "removal" && article.tag_list.exclude?(tag_name) + errors.add(:base, "4 tags max per article.") if adjustment_type == "addition" && article.tag_list.count > 3 + end end diff --git a/app/services/tag_adjustment_creation_service.rb b/app/services/tag_adjustment_creation_service.rb index 148ddc5d6..3d35b12d2 100644 --- a/app/services/tag_adjustment_creation_service.rb +++ b/app/services/tag_adjustment_creation_service.rb @@ -8,6 +8,10 @@ class TagAdjustmentCreationService @tag_adjustment ||= TagAdjustment.new(creation_args) end + def article + @article ||= Article.find(creation_args[:article_id]) + end + def update_tags_and_notify update_article Notification.send_tag_adjustment_notification(tag_adjustment) @@ -16,7 +20,6 @@ class TagAdjustmentCreationService private def update_article - article = Article.find(creation_args[:article_id]) article.update!(tag_list: article.tag_list.remove(@tag_adjustment.tag_name)) if @tag_adjustment.adjustment_type == "removal" article.update!(tag_list: article.tag_list.add(@tag_adjustment.tag_name)) if @tag_adjustment.adjustment_type == "addition" end diff --git a/app/views/moderations/mod.html.erb b/app/views/moderations/mod.html.erb index 1a630b3f0..ad41d5f8d 100644 --- a/app/views/moderations/mod.html.erb +++ b/app/views/moderations/mod.html.erb @@ -92,9 +92,34 @@ background: #ff0000; /* $red */ } - .adjustment-error { - text-align:center; - color:red; + .tag-mod-form input[type=radio] { + position: absolute; + } + + .tag-mod-form input[type=radio] + label { + display: inline-block; + cursor: pointer; + font-weight: bold; + padding-left: 25px; + padding-right: 25px; + font-size: 20px; + margin: auto; + margin-bottom: 15px; + } + + .tag-mod-form input[type=radio]:checked + label{ + color: white; + background: #0045ff; + } + + .tag-mod-form select { + display: block; + font-size: 20px; + padding: 12px; + font-weight: bold; + margin: auto; + border: solid 2px; + margin-top: 5px; } @@ -125,90 +150,64 @@

The DEV team will be notified about vomits and take appropriate action, but leaving a level-headed comment addressing the behavior is welcome if you feel up for it.

<% end %> - <% is_mod = (@moderatable.tag_list.select { |t| current_user.has_role?(:tag_moderator, Tag.find_by(name: t)) }).any? if @moderatable.class.name == "Article" %> - <% if @moderatable.class.name == "Article" && (current_user.has_role?(:super_admin) || is_mod) %> - Community moderation guide + <% if @allowed_to_adjust %>
- <%= form_for(TagAdjustment.new) do |f| %> -

Remove Inappropriate Tags

+

Tag Adjustment

+ <%= form_for(@tag_adjustment) do |f| %> +

Add or Remove Tags

<% unless current_user.has_role?(:super_admin) %> <% end %> Current live tags: <%= @moderatable.tag_list %>
Already adjusted tags: <%= @already_adjusted_tags %>

- <% if flash[:error_removal].present? %> -
-

Error: <%= flash[:error_removal] %>


+ <% if @tag_adjustment.errors.any? %> +
+

<%= pluralize(@tag_adjustment.errors.count, "error") %> prohibited this block from being saved:

+
    + <% @tag_adjustment.errors.full_messages.each do |message| %> +
  • <%= message %>
  • + <% end %> +
+



<% end %> <%= f.hidden_field :article_id, value: @moderatable.id %> - <%= f.hidden_field :adjustment_type, value: "removal" %> - <%= f.text_field :tag_name, placeholder: "Tag Name", required: true %> - <%= f.text_area :reason_for_adjustment, placeholder: "Reason for Removal (Be super kind) - Only the reason is needed, the notification will take care of the rest.", required: true %> - <%= f.submit "Remove Tag" %> - <% end %> - <% if @removed_adjustments.any? %> -

- Removed tags: - <% @removed_adjustments.each do |adjustment| %> - <% if current_user.any_admin? %> - <%= form_for(adjustment, url: "/tag_adjustments/#{adjustment.id}", html: { method: :delete, onsubmit: "return confirm('Are you sure you want to undo the removal of the #{adjustment.tag_name} tag?')" }) do |f| %> - <%= adjustment.tag_name %> - <%= f.submit "X", id: "undo" %> - <% end %> - <% end %> + <% if current_user.any_admin? || @tag_moderator_tags.any? { |tag| @moderatable.tag_list.include?(tag.name) } %> + <%= f.radio_button :adjustment_type, "removal", required: true %> + <%= f.label :adjustment_type, "Remove", value: "removal" %> <% end %> - <% end %> -
- <% end %> - - <% if @allowed_to_add %> -
-

Add Appropriate Tags

- <% if @moderatable.tag_list.count < 4 %> - <%= form_for(TagAdjustment.new) do |f| %> - <% unless current_user.has_role?(:super_admin) %> -
    -
  • You can only add tags that you moderate.
  • -
  • Only add tags that are appropriate.
  • -
  • Always be friendly in your tag adding reason.
  • -
  • Only add tags which haven't been adjusted already.
  • -
- <% end %> - Current live tags: <%= @moderatable.tag_list %>
- Already adjusted tags: <%= @already_adjusted_tags %> -

- <% if flash[:error_addition].present? %> -
-

Error: <%= flash[:error_addition] %>


-
- <% end %> - <%= f.hidden_field :article_id, value: @moderatable.id %> - <%= f.hidden_field :adjustment_type, value: "addition" %> - <%= f.text_field :tag_name, placeholder: "Tag Name - only add one at a time", required: true %> - <%= f.text_area :reason_for_adjustment, placeholder: "Reason for addition (Be super kind) - Only the reason is needed, the notification will take care of the rest.", required: true %> - <%= f.submit "Add Tag" %> + <% if @moderatable.tag_list.count < 4 %> + <%= f.radio_button :adjustment_type, "addition", required: true %> + <%= f.label :adjustment_type, "Add", value: "addition" %> <% end %> - <% else %> - Unable to add new tags. 4 tags max. +
+ <% if current_user.has_role?(:super_admin) %> + <%= f.text_field :tag_name, placeholder: "Tag Name", required: true %> + <% else %> + <%= f.select :tag_name, @tag_moderator_tags, { prompt: "Select Tag" }, required: true %> +
+ <% end %> + <%= f.text_area :reason_for_adjustment, placeholder: "Reason for adjustment (Be super kind) - Only the reason is needed, the notification will take care of the rest.", required: true %> + <%= f.submit "Submit Tag Adjustment" %> <% end %> - <% if @added_adjustments.any? %> + <% if @adjustments.any? %>

- Added tags: - <% @added_adjustments.each do |adjustment| %> - <% if current_user.any_admin? %> - <%= form_for(adjustment, url: "/tag_adjustments/#{adjustment.id}", html: { method: :delete, onsubmit: "return confirm('Are you sure you want to undo the addition of the #{adjustment.tag_name} tag?')" }) do |f| %> - <%= adjustment.tag_name %> - <%= f.submit "X", id: "undo" %> - <% end %> + Adjusted Tags:

+ <% @adjustments.each do |adjustment| %> + <% if current_user.any_admin? || adjustment.user_id == current_user.id %> + <%= form_for(adjustment, url: "/tag_adjustments/#{adjustment.id}", html: { method: :delete, onsubmit: "return confirm('Are you sure you want to undo the #{adjustment.adjustment_type} of the #{adjustment.tag_name} tag?')" }) do |f| %> + <%= adjustment.adjustment_type == "removal" ? "Removed" : "Added" %>: <%= adjustment.tag_name %> + <%= f.submit "X", id: "undo" %> <% end %> + <% end %> +
<% end %> <% end %>
diff --git a/spec/factories/tag_adjustments.rb b/spec/factories/tag_adjustments.rb index cf6f56b26..af3bc1e67 100644 --- a/spec/factories/tag_adjustments.rb +++ b/spec/factories/tag_adjustments.rb @@ -4,7 +4,7 @@ FactoryBot.define do article_id { 1 } tag_id { 1 } tag_name { "NOTHING" } - adjustment_type { "removal" } + adjustment_type { "addition" } reason_for_adjustment { "reason #{rand(10_000)}" } status { "committed" } end diff --git a/spec/models/tag_adjustment_spec.rb b/spec/models/tag_adjustment_spec.rb index 296534921..67d14878d 100644 --- a/spec/models/tag_adjustment_spec.rb +++ b/spec/models/tag_adjustment_spec.rb @@ -1,57 +1,87 @@ require "rails_helper" RSpec.describe TagAdjustment, type: :model do - let_it_be(:article) { create(:article) } - let_it_be(:admin_user) { create(:user, :admin) } - let_it_be(:regular_user) { create(:user) } + before do + mod_user.add_role(:tag_moderator, tag) + admin_user.add_role(:admin) + end + + let(:article) { create(:article, tags: nil) } + let(:tag) { create(:tag) } + let(:admin_user) { create(:user) } + let(:mod_user) { create(:user) } + let(:regular_user) { create(:user) } it { is_expected.to validate_presence_of(:user_id) } it { is_expected.to validate_presence_of(:article_id) } it { is_expected.to validate_presence_of(:tag_id) } it { is_expected.to validate_presence_of(:tag_name) } it { is_expected.to validate_presence_of(:adjustment_type) } - it { is_expected.to validate_inclusion_of(:adjustment_type).in_array(%w[removal addition]) } it { is_expected.to validate_presence_of(:status) } - - it do - # rubocop:disable RSpec/NamedSubject - expect(subject).to validate_inclusion_of(:status).in_array( - %w[committed pending committed_and_resolvable resolved], - ) - # rubocop:enable RSpec/NamedSubject - end - it { is_expected.to have_many(:notifications).dependent(:delete_all) } - describe "validations" do - let(:tag) { create(:tag) } - let(:mod_user) { create(:user) } + describe "privileges" do + it "allows tag mods to create for their tags" do + tag_adjustment = build(:tag_adjustment, user_id: mod_user.id, article_id: article.id, tag_id: tag.id) + expect(tag_adjustment).to be_valid + end - describe "privileges" do - before do - mod_user.add_role(:tag_moderator, tag) - end + it "does not allow tag mods to create for other tags" do + another_tag = create(:tag) + tag_adjustment = build(:tag_adjustment, user_id: mod_user.id, article_id: article.id, tag_id: another_tag.id) + expect(tag_adjustment).to be_invalid + end - it "allows tag mods to create for their tags" do - tag_adjustment = build(:tag_adjustment, user: mod_user, article: article, tag: tag) - expect(tag_adjustment).to be_valid - end + it "allows admins to create for any tags" do + tag_adjustment = build(:tag_adjustment, user_id: admin_user.id, article_id: article.id, tag_id: tag.id) + expect(tag_adjustment).to be_valid + end - it "does not allow tag mods to create for other tags" do - another_tag = create(:tag) - tag_adjustment = build(:tag_adjustment, user: mod_user, article: article, tag: another_tag) - expect(tag_adjustment).to be_invalid - end + it "does not allow normal users to create for any tags" do + tag_adjustment = build(:tag_adjustment, user_id: regular_user.id, article_id: article.id, tag_id: tag.id) + expect(tag_adjustment).to be_invalid + end + end - it "allows admins to create for any tags" do - tag_adjustment = build(:tag_adjustment, user: admin_user, article: article, tag: tag) - expect(tag_adjustment).to be_valid - end + describe "allowed attribute states" do + it "allows addition adjustment_types" do + tag_adjustment = build(:tag_adjustment, user_id: mod_user.id, article_id: article.id, tag_id: tag.id) + expect(tag_adjustment).to be_valid + end - it "does not allow normal users to create for any tags" do - tag_adjustment = build(:tag_adjustment, user: regular_user, article: article, tag: tag) - expect(tag_adjustment).to be_invalid - end + it "allows removal adjustment_types" do + article = create(:article, tags: tag.name) + tag_adjustment = build(:tag_adjustment, user_id: mod_user.id, article_id: article.id, tag_id: tag.id, tag_name: tag.name, adjustment_type: "removal") + expect(tag_adjustment).to be_valid + end + + it "disallows improper adjustment_types" do + tag_adjustment = build(:tag_adjustment, user_id: mod_user.id, article_id: article.id, tag_id: tag.id, adjustment_type: "slushie") + expect(tag_adjustment).to be_invalid + end + + it "allows proper status" do + tag_adjustment = build(:tag_adjustment, user_id: mod_user.id, article_id: article.id, tag_id: tag.id, status: "committed") + expect(tag_adjustment).to be_valid + end + + it "disallows improper status" do + tag_adjustment = build(:tag_adjustment, user_id: mod_user.id, article_id: article.id, tag_id: tag.id, status: "slushiemonkey") + expect(tag_adjustment).to be_invalid + end + end + + describe "validates article tag_list" do + it "does not allow addition on articles with 4 tags" do + article_tags_maxed = create(:article) + tag_adjustment = build(:tag_adjustment, user_id: admin_user.id, article_id: article_tags_maxed.id, tag_id: tag.id, tag_name: tag.name) + expect(tag_adjustment).to be_invalid + end + + it "does not create if removed tag not on tag_list" do + article = create(:article, tags: tag.name) + tag_adjustment = build(:tag_adjustment, user_id: admin_user.id, article_id: article.id, adjustment_type: "removal") + expect(tag_adjustment).to be_invalid end end end diff --git a/spec/services/notifications/tag_adjustment_notification/send_spec.rb b/spec/services/notifications/tag_adjustment_notification/send_spec.rb index d57c0a701..75da1b8d6 100644 --- a/spec/services/notifications/tag_adjustment_notification/send_spec.rb +++ b/spec/services/notifications/tag_adjustment_notification/send_spec.rb @@ -6,7 +6,7 @@ RSpec.describe Notifications::TagAdjustmentNotification::Send, type: :service do let(:article) { create(:article, title: "My title", user: user2, body_markdown: "---\ntitle: Hellohnnnn#{rand(1000)}\npublished: true\ntags: heyheyhey,#{tag.name}\n---\n\nHello") } let(:tag) { create(:tag) } let(:mod_user) { create(:user) } - let(:tag_adjustment) { create(:tag_adjustment, user_id: mod_user.id, article_id: article.id, tag_id: tag.id, adjustment_type: "removal") } + let(:tag_adjustment) { create(:tag_adjustment, user_id: mod_user.id, article_id: article.id, tag_id: tag.id, adjustment_type: "addition") } let(:notification) { described_class.call(tag_adjustment) } before do @@ -33,14 +33,14 @@ RSpec.describe Notifications::TagAdjustmentNotification::Send, type: :service do it "tests JSON data" do json = notification.json_data expect(json["article"]["title"]).to start_with("Hello") - expect(json["adjustment_type"]). to eq "removal" + expect(json["adjustment_type"]). to eq "addition" end - it "tests JSON data for addition" do - tag_adjustment.adjustment_type = "addition" + it "tests JSON data for removal" do + tag_adjustment.adjustment_type = "removal" json = notification.json_data expect(json["article"]["title"]).to start_with("Hello") - expect(json["adjustment_type"]). to eq "addition" + expect(json["adjustment_type"]). to eq "removal" end specify "notification to be inserted on DB" do diff --git a/spec/services/tag_adjustment_update_service_spec.rb b/spec/services/tag_adjustment_update_service_spec.rb index 05576b429..21a4d3bfc 100644 --- a/spec/services/tag_adjustment_update_service_spec.rb +++ b/spec/services/tag_adjustment_update_service_spec.rb @@ -2,7 +2,7 @@ require "rails_helper" RSpec.describe TagAdjustmentUpdateService, type: :service do let(:user) { create(:user) } - let(:article) { create(:article) } + let(:article) { create(:article, tags: tag.name) } let(:tag) { create(:tag) } def create_tag_adjustment