Admin comments page with flag and quality reaction details. (#19647)
* Created separate comments page * Optimised code for single comment and multiple comments * Indifual comments poage added * Minor header change * Created show method * Full implementation with eager loading error * Temporarily suppress Bullet from Admin::CommentsController * Minor fixes * Minor fixes * Added table * Added french translations * Added reaction stats / summary * Tests added fore empty comments * Added seed data and few more tests * Updated test * Added model tests for comment and article * Nit test fix * Applied changes as per PJ's review --------- Co-authored-by: Joshua Wehner <joshua@forem.com>
This commit is contained in:
parent
7ecb72413d
commit
b397292a33
11 changed files with 309 additions and 10 deletions
|
|
@ -1,26 +1,32 @@
|
|||
module Admin
|
||||
class CommentsController < Admin::ApplicationController
|
||||
around_action :skip_bullet, if: -> { defined?(Bullet) }
|
||||
|
||||
layout "admin"
|
||||
|
||||
def index
|
||||
@comments = if params[:state]&.start_with?("toplast-")
|
||||
Comment
|
||||
.includes(:user)
|
||||
.includes(:commentable)
|
||||
.includes(:user, :commentable, :reactions)
|
||||
.order(public_reactions_count: :desc)
|
||||
.where("created_at > ?", params[:state].split("-").last.to_i.days.ago)
|
||||
.page(params[:page] || 1).per(50)
|
||||
else
|
||||
Comment
|
||||
.includes(:user)
|
||||
.includes(:commentable)
|
||||
.includes(:user, :commentable, :reactions)
|
||||
.order(created_at: :desc)
|
||||
.page(params[:page] || 1).per(50)
|
||||
end
|
||||
@countable_vomits = {}
|
||||
@comments.each do |comment|
|
||||
@countable_vomits[comment.id] = calculate_flags_for_single_comment(comment)
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
@comment = Comment.includes(:user, :commentable).find(params[:id])
|
||||
@comment = Comment.includes(:user, :commentable, :reactions).find(params[:id])
|
||||
@countable_vomits = {}
|
||||
@countable_vomits[@comment.id] = calculate_flags_for_single_comment(@comment)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
@ -28,5 +34,19 @@ module Admin
|
|||
def authorize_admin
|
||||
authorize Comment, :access?, policy_class: InternalPolicy
|
||||
end
|
||||
|
||||
def calculate_flags_for_single_comment(comment)
|
||||
comment.reactions.privileged_category.count do |reaction|
|
||||
reaction.category == "vomit" && reaction.status != "invalid"
|
||||
end
|
||||
end
|
||||
|
||||
def skip_bullet
|
||||
previous_value = Bullet.enable?
|
||||
Bullet.enable = false
|
||||
yield
|
||||
ensure
|
||||
Bullet.enable = previous_value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -194,6 +194,10 @@ class Comment < ApplicationRecord
|
|||
ancestry && Comment.exists?(id: ancestry)
|
||||
end
|
||||
|
||||
def privileged_reaction_counts
|
||||
@privileged_reaction_counts ||= reactions.privileged_category.group(:category).count
|
||||
end
|
||||
|
||||
private_class_method :build_sort_query
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -222,7 +222,7 @@
|
|||
<% if params[:tab] == "quality_reactions" %>
|
||||
<% if quality_article_reactions.present? %>
|
||||
<% quality_article_reactions.each do |quality_reaction| %>
|
||||
<%= render "admin/articles/quality_action_item", quality_reaction: quality_reaction %>
|
||||
<%= render "admin/shared/quality_action_item", quality_reaction: quality_reaction %>
|
||||
<hr id="js__reaction__div__hr__<%= quality_reaction.id %>" class="w-100 hr-no-margins">
|
||||
<% end %>
|
||||
<% else %>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,27 @@
|
|||
<a href="<%= comment.commentable.path %>" class="c-link c-link--branded"><%= comment.commentable.title %></a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="crayons-card crayons-card--secondary px-3 py-1 flex gap-2 items-center" title="<%= t("views.moderations.actions.thumb_up") %>">
|
||||
<%= crayons_icon_tag("twemoji/thumb-up", native: true, width: 16, height: 16) %>
|
||||
<span class="fs-s fw-medium lh-base"><%= comment.privileged_reaction_counts["thumbsup"] || "0" %></span>
|
||||
</span>
|
||||
|
||||
<span class="crayons-card crayons-card--secondary px-3 py-1 flex gap-2 items-center" title="<%= t("views.moderations.actions.thumb_down") %>">
|
||||
<%= crayons_icon_tag("twemoji/thumb-down", native: true, width: 16, height: 16) %>
|
||||
<span class="fs-s fw-medium lh-base"><%= comment.privileged_reaction_counts["thumbsdown"] || "0" %></span>
|
||||
</span>
|
||||
|
||||
<span class="crayons-card crayons-card--secondary px-3 py-1 flex gap-2 items-center" title="<%= t("views.moderations.actions.vomit") %>">
|
||||
<%= crayons_icon_tag("twemoji/suspicious", native: true, width: 16, height: 16) %>
|
||||
<span class="fs-s fw-medium lh-base"><%= (@countable_vomits&.dig(comment.id) || 0) %></span>
|
||||
</span>
|
||||
|
||||
<span class="crayons-card crayons-card--secondary px-3 py-1 ml-3 flex gap-2 items-center" title="<%= t("views.moderations.actions.score") %>">
|
||||
<%= crayons_icon_tag("analytics", native: true, width: 16, height: 16) %>
|
||||
<span class="fs-s fw-medium lh-base"><%= comment.score %></span>
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
<% end %>
|
||||
<div class="text-styles text-styles--tertiary">
|
||||
|
|
@ -35,4 +56,47 @@
|
|||
<% end %>
|
||||
</footer>
|
||||
</article>
|
||||
|
||||
<% if defined?(is_individual_comment) && is_individual_comment %>
|
||||
<% privileged_comment_reactions = comment.reactions.privileged_category %>
|
||||
<% vomit_comment_reactions = privileged_comment_reactions.select { |reaction| reaction.category == "vomit" }.reverse %>
|
||||
<% quality_comment_reactions = (privileged_comment_reactions - vomit_comment_reactions).reverse %>
|
||||
<article class="js-individual-article crayons-card py-6 flex flex-col mt-4">
|
||||
<h2 class="crayons-subtitle-2 mx-6"><%= t("views.admin.comments.priviliged_actions.title") %></h2>
|
||||
<p class="crayons-subtitle-3 fw-normal color-secondary mt-1 mx-6"><%= t("views.admin.comments.priviliged_actions.description") %></p>
|
||||
|
||||
<nav class="mt-4 pt-1 pb-2 px-3 member-data-heading" aria-label="Member details">
|
||||
<ul class="crayons-navigation crayons-navigation--horizontal">
|
||||
<li><%= link_to "Flags", admin_comment_path(tab: :flags), class: "crayons-navigation__item #{'crayons-navigation__item crayons-navigation__item--current' if params[:tab] == 'flags' || params[:tab].blank?}", aria: @current_tab == "flags" ? { current: "" } : {} %></li></li>
|
||||
<li><%= link_to "Quality reactions", admin_comment_path(tab: :quality_reactions), class: "crayons-navigation__item #{'crayons-navigation__item crayons-navigation__item--current' if params[:tab] == 'quality_reactions'}",
|
||||
aria: @current_tab == "quality_reactions" ? { current: "" } : {} %></li></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div id="reaction-content" class="flex flex-col gap-3 px-6 mt-6" style="overflow: auto; height: 406px;">
|
||||
<% if params[:tab].blank? || params[:tab] == "flags" %>
|
||||
<%= render "admin/shared/flag_reactions_table",
|
||||
vomit_reactions: vomit_comment_reactions,
|
||||
text_section: "comments",
|
||||
empty_text: t("views.admin.comments.priviliged_actions.no_flags") %>
|
||||
<% end %>
|
||||
|
||||
<% if params[:tab] == "quality_reactions" %>
|
||||
<% if quality_comment_reactions.present? %>
|
||||
<% quality_comment_reactions.each do |quality_reaction| %>
|
||||
<%= render "admin/shared/quality_action_item", quality_reaction: quality_reaction %>
|
||||
<hr id="js__reaction__div__hr__<%= quality_reaction.id %>" class="w-100 hr-no-margins">
|
||||
<% end %>
|
||||
<% else %>
|
||||
<div class="flex flex-col justify-center items-center gap-4 h-100">
|
||||
<div class="flex p-4 gap-2 radius-default" style="background: #EEF2FF;">
|
||||
<%= crayons_icon_tag("quality-reactions", native: true, width: 56, height: 56) %>
|
||||
</div>
|
||||
<p class="crayons-subtitle-3 fw-normal color-secondary"><%= t("views.admin.comments.priviliged_actions.no_quality_reactions") %></p>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<div>
|
||||
</article>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,14 @@ en:
|
|||
no_flags: Article has no flags.
|
||||
no_quality_reactions: Article has no quality reactions by trusted users.
|
||||
title: Moderator actions
|
||||
comments:
|
||||
flags:
|
||||
score: Score affected by a particular flag
|
||||
priviliged_actions:
|
||||
description: All the moderator actions affect the score. The overall score is a combination of moderator actions and public reactions.
|
||||
no_flags: Comment has no flags.
|
||||
no_quality_reactions: Comment has no quality reactions by trusted users.
|
||||
title: Moderator actions
|
||||
shared:
|
||||
flags:
|
||||
actions:
|
||||
|
|
|
|||
|
|
@ -10,6 +10,14 @@ fr:
|
|||
no_flags: Article has no flags.
|
||||
no_quality_reactions: Article has no quality reactions by trusted users.
|
||||
title: Moderator actions
|
||||
comments:
|
||||
flags:
|
||||
score: Score affecté par un drapeau particulier
|
||||
priviliged_actions:
|
||||
description: Toutes les actions du modérateur affectent le score. Le score global est une combinaison des actions des modérateurs et des réactions du public.
|
||||
no_flags: Le commentaire n'a pas de drapeaux.
|
||||
no_quality_reactions: Le commentaire n'a pas de réactions de qualité par des utilisateurs de confiance.
|
||||
title: Actions du modérateur
|
||||
shared:
|
||||
flags:
|
||||
actions:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,124 @@
|
|||
describe('View details link on comments list page in admin area', () => {
|
||||
beforeEach(() => {
|
||||
cy.testSetup();
|
||||
cy.fixture('users/adminUser.json').as('user');
|
||||
|
||||
cy.get('@user').then((user) => {
|
||||
cy.loginUser(user).then(() => {
|
||||
cy.visit(`/admin/content_manager/comments`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should contain view details link with correct href', () => {
|
||||
cy.findAllByRole('link', { name: 'View Details' }).each(($link) => {
|
||||
const href = $link.attr('href');
|
||||
const regex = /\/(\d+)/; // Regular expression to match any number in the URL
|
||||
expect(href).to.match(regex);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('New comment with empty flags and reactions', () => {
|
||||
let commentId;
|
||||
beforeEach(() => {
|
||||
cy.testSetup();
|
||||
cy.fixture('users/adminUser.json').as('user');
|
||||
|
||||
cy.get('@user').then((user) => {
|
||||
cy.loginAndVisit(user, '/').then(() => {
|
||||
cy.createArticle({
|
||||
title: 'Test Article',
|
||||
tags: ['beginner', 'ruby', 'go'],
|
||||
content: `This is a test article's contents.`,
|
||||
published: true,
|
||||
}).then((response) => {
|
||||
cy.createComment({
|
||||
content: 'This is a test comment.',
|
||||
commentableId: response.body.id,
|
||||
commentableType: 'Article',
|
||||
}).then((commentResponse) => {
|
||||
commentId = commentResponse.body.id;
|
||||
cy.visit(`/admin/content_manager/comments/${commentId}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should not contain view details link on individual comments page', () => {
|
||||
cy.findAllByRole('link', { name: 'View Details' }).should('not.exist');
|
||||
});
|
||||
|
||||
it('should update url on flag tab click', () => {
|
||||
cy.findByRole('link', { name: 'Flags' }).click();
|
||||
cy.url().should(
|
||||
'contains',
|
||||
`/admin/content_manager/comments/${commentId}?tab=flags`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should update url on quality reactions tab click', () => {
|
||||
cy.findByRole('link', { name: 'Quality reactions' }).click();
|
||||
cy.url().should(
|
||||
'contains',
|
||||
`/admin/content_manager/comments/${commentId}?tab=quality_reactions`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should not contain any flags and quality reactions', () => {
|
||||
cy.findByText('Comment has no flags.').should('exist');
|
||||
|
||||
cy.findByRole('link', { name: 'Quality reactions' }).click();
|
||||
cy.findByText('Comment has no quality reactions by trusted users.').should(
|
||||
'exist',
|
||||
);
|
||||
});
|
||||
|
||||
it('should display the correct values for privileged reactions and score', () => {
|
||||
// Thumbs-up count
|
||||
cy.get('.flex .crayons-card:nth-child(1) .fs-s').should('contain', '0');
|
||||
|
||||
// Thumbs-down count
|
||||
cy.get('.flex .crayons-card:nth-child(2) .fs-s').should('contain', '0');
|
||||
|
||||
// Vomit/flag count
|
||||
cy.get('.flex .crayons-card:nth-child(3) .fs-s').should('contain', '0');
|
||||
|
||||
//Score
|
||||
cy.get('.flex .crayons-card:nth-child(4) .fs-s').should('contain', '0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Comment with flags and reactions', () => {
|
||||
beforeEach(() => {
|
||||
cy.testSetup();
|
||||
cy.fixture('users/adminUser.json').as('user');
|
||||
|
||||
cy.get('@user').then((user) => {
|
||||
cy.loginUser(user).then(() => {
|
||||
cy.visit(`/admin/content_manager/comments`);
|
||||
|
||||
cy.contains('Contains various privileged reactions.')
|
||||
.parents('.crayons-card')
|
||||
.within(() => {
|
||||
cy.contains('View Details').click();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should display the correct values for privileged reactions and score', () => {
|
||||
// Thumbs-up count
|
||||
cy.get('.flex .crayons-card:nth-child(1) .fs-s').should('contain', '0');
|
||||
|
||||
// Thumbs-down count
|
||||
cy.get('.flex .crayons-card:nth-child(2) .fs-s').should('contain', '1');
|
||||
|
||||
// Vomit/flag count
|
||||
cy.get('.flex .crayons-card:nth-child(3) .fs-s').should('contain', '1');
|
||||
|
||||
//Score
|
||||
cy.get('.flex .crayons-card:nth-child(4) .fs-s').should('contain', '0');
|
||||
});
|
||||
});
|
||||
|
|
@ -1276,6 +1276,41 @@ RSpec.describe Article do
|
|||
end
|
||||
end
|
||||
|
||||
describe "#privileged_reaction_counts" do
|
||||
it "contains correct vomit count" do
|
||||
user = create(:user, :trusted)
|
||||
create(:reaction, reactable: article, category: "vomit", user: user)
|
||||
counts = article.privileged_reaction_counts
|
||||
expect(counts["vomit"]).to eq(1)
|
||||
expect(counts["thumbsup"]).to be_nil
|
||||
expect(counts["thumbsdown"]).to be_nil
|
||||
end
|
||||
|
||||
it "contains correct thumbsup count" do
|
||||
user = create(:user, :trusted)
|
||||
create(:reaction, reactable: article, category: "thumbsup", user: user)
|
||||
counts = article.privileged_reaction_counts
|
||||
expect(counts["vomit"]).to be_nil
|
||||
expect(counts["thumbsup"]).to eq(1)
|
||||
expect(counts["thumbsdown"]).to be_nil
|
||||
end
|
||||
|
||||
it "contains correct thumbsdown count" do
|
||||
user = create(:user, :trusted)
|
||||
create(:reaction, reactable: article, category: "thumbsdown", user: user)
|
||||
counts = article.privileged_reaction_counts
|
||||
expect(counts["vomit"]).to be_nil
|
||||
expect(counts["thumbsup"]).to be_nil
|
||||
expect(counts["thumbsdown"]).to eq(1)
|
||||
end
|
||||
|
||||
it "returns an empty hash if there are no privileged reactions" do
|
||||
counts = article.privileged_reaction_counts
|
||||
|
||||
expect(counts).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe "#followers" do
|
||||
it "returns an array of users who follow the article's author" do
|
||||
following_user = create(:user)
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ RSpec.describe Comment do
|
|||
it "not double wrap an already-linked mention" do
|
||||
comment.body_markdown = "Hello <a href='/#{user.username}'>@#{user.username}</a>, you are cool."
|
||||
comment.validate!
|
||||
expect(comment.processed_html.scan(/href/).count).to eq(1)
|
||||
expect(comment.processed_html.scan("href").count).to eq(1)
|
||||
end
|
||||
|
||||
it "does not wrap email mention with username" do
|
||||
|
|
@ -188,7 +188,6 @@ RSpec.describe Comment do
|
|||
expect(comment.processed_html.include?("Hello <a")).to be(true)
|
||||
end
|
||||
|
||||
# rubocop:disable RSpec/ExampleLength
|
||||
it "shortens long urls without removing formatting", :aggregate_failures do
|
||||
long_url = "https://longurl.com/#{'x' * 100}?#{'y' * 100}"
|
||||
comment.body_markdown = "Hello #{long_url}"
|
||||
|
|
@ -395,6 +394,7 @@ RSpec.describe Comment do
|
|||
expect(comments).to eq([new_comment.id, comment.id, other_comment.id, old_comment.id])
|
||||
end
|
||||
|
||||
# rubocop:disable RSpec/ExampleLength
|
||||
it "returns comments in the right order when order is top" do
|
||||
comment.update_column(:score, 5)
|
||||
highest_rated_comment = comment
|
||||
|
|
@ -409,6 +409,7 @@ RSpec.describe Comment do
|
|||
comments = comments.map { |key, _| key.id }
|
||||
expect(comments).to eq([highest_rated_comment.id, mid_high_rated_comment.id, mid_low_rated_comment.id, lowest_rated_comment.id]) # rubocop:disable Layout/LineLength
|
||||
end
|
||||
# rubocop:enable RSpec/ExampleLength
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -427,6 +428,7 @@ RSpec.describe Comment do
|
|||
end.to change(Comments::CalculateScoreWorker.jobs, :size).by(1)
|
||||
end
|
||||
|
||||
# rubocop:disable RSpec/ExampleLength
|
||||
it "enqueues a worker to send email" do
|
||||
comment.save!
|
||||
child_comment_user = create(:user)
|
||||
|
|
@ -436,6 +438,7 @@ RSpec.describe Comment do
|
|||
child_comment.save!
|
||||
end.to change(Comments::SendEmailNotificationWorker.jobs, :size).by(1)
|
||||
end
|
||||
# rubocop:enable RSpec/ExampleLength
|
||||
|
||||
it "enqueues a worker to bust comment cache" do
|
||||
expect do
|
||||
|
|
@ -491,6 +494,35 @@ RSpec.describe Comment do
|
|||
end
|
||||
end
|
||||
|
||||
describe "#privileged_reaction_counts" do
|
||||
it "contains correct vomit count" do
|
||||
user = create(:user, :trusted)
|
||||
create(:reaction, reactable: comment, category: "vomit", user: user)
|
||||
counts = comment.privileged_reaction_counts
|
||||
expect(counts["vomit"]).to eq(1)
|
||||
end
|
||||
|
||||
it "contains correct thumbsup count" do
|
||||
user = create(:user, :trusted)
|
||||
create(:reaction, reactable: comment, category: "thumbsup", user: user)
|
||||
counts = comment.privileged_reaction_counts
|
||||
expect(counts["thumbsup"]).to eq(1)
|
||||
end
|
||||
|
||||
it "contains correct thumbsdown count" do
|
||||
user = create(:user, :trusted)
|
||||
create(:reaction, reactable: comment, category: "thumbsdown", user: user)
|
||||
counts = comment.privileged_reaction_counts
|
||||
expect(counts["thumbsdown"]).to eq(1)
|
||||
end
|
||||
|
||||
it "returns an empty hash if there are no privileged reactions" do
|
||||
counts = comment.privileged_reaction_counts
|
||||
|
||||
expect(counts).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context "when callbacks are triggered before save" do
|
||||
context "when the post is present" do
|
||||
it "generates character count before saving" do
|
||||
|
|
@ -540,6 +572,7 @@ RSpec.describe Comment do
|
|||
expect(comment.notifications).to be_empty
|
||||
end
|
||||
|
||||
# rubocop:disable RSpec/ExampleLength
|
||||
it "updates the notifications of the descendants with [deleted]" do
|
||||
comment = create(:comment, commentable: article)
|
||||
child_comment = create(:comment, parent: comment, commentable: article, user: user)
|
||||
|
|
@ -550,6 +583,7 @@ RSpec.describe Comment do
|
|||
notification = child_comment.notifications.first
|
||||
expect(notification.json_data["comment"]["ancestors"][0]["title"]).to eq("[deleted]")
|
||||
end
|
||||
# rubocop:enable RSpec/ExampleLength
|
||||
end
|
||||
|
||||
context "when callbacks are triggered after destroy" do
|
||||
|
|
|
|||
|
|
@ -827,13 +827,15 @@ seeder.create_if_doesnt_exist(Article, "title", "Series test article") do
|
|||
)
|
||||
|
||||
comment_attributes = {
|
||||
body_markdown: Faker::Hipster.paragraph(sentence_count: 1),
|
||||
body_markdown: "Contains various privileged reactions.",
|
||||
user_id: questionable_user.id,
|
||||
commentable_id: article.id,
|
||||
commentable_type: "Article"
|
||||
}
|
||||
|
||||
Comment.create!(comment_attributes)
|
||||
comment = Comment.create!(comment_attributes)
|
||||
admin_user.reactions.create!(category: :vomit, reactable: comment, status: :confirmed)
|
||||
admin_user.reactions.create!(category: :thumbsdown, reactable: comment)
|
||||
end
|
||||
|
||||
##############################################################################
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue