FEATURE: Flag count in post menu

This change shows a notification number besides the flag icon in the
post menu if there is reviewable content associated with the post.
Additionally, if there is pending stuff to review, the icon has a red
background.

We have also removed the list of links below a post with the flag
status. A reviewer is meant to click the number beside the flag icon to
view the flags. As a consequence of losing those links, we've removed
the ability to undo or ignore flags below a post.
This commit is contained in:
Robin Ward 2019-05-03 14:26:37 -04:00
parent e6843afa9e
commit 31e100530f
24 changed files with 384 additions and 630 deletions

View File

@ -1,19 +1,5 @@
import { userPath } from "discourse/lib/url"; import { userPath } from "discourse/lib/url";
function actionDescription(action, acted, count) {
if (acted) {
if (count <= 1) {
return I18n.t(`post.actions.by_you.${action}`);
} else {
return I18n.t(`post.actions.by_you_and_others.${action}`, {
count: count - 1
});
}
} else {
return I18n.t(`post.actions.by_others.${action}`, { count });
}
}
const _additionalAttributes = []; const _additionalAttributes = [];
export function includeAttributes(...attributes) { export function includeAttributes(...attributes) {
@ -62,6 +48,10 @@ export function transformBasicPost(post) {
canRecover: post.can_recover, canRecover: post.can_recover,
canEdit: post.can_edit, canEdit: post.can_edit,
canFlag: !Ember.isEmpty(post.get("flagsAvailable")), canFlag: !Ember.isEmpty(post.get("flagsAvailable")),
canReviewTopic: false,
reviewableId: post.reviewable_id,
reviewableScoreCount: post.reviewable_score_count,
reviewableScorePendingCount: post.reviewable_score_pending_count,
version: post.version, version: post.version,
canRecoverTopic: false, canRecoverTopic: false,
canDeletedTopic: false, canDeletedTopic: false,
@ -121,6 +111,7 @@ export default function transformPost(
postAtts.canViewRawEmail = postAtts.canViewRawEmail =
currentUser && (currentUser.id === post.user_id || currentUser.staff); currentUser && (currentUser.id === post.user_id || currentUser.staff);
postAtts.canReplyAsNewTopic = details.can_reply_as_new_topic; postAtts.canReplyAsNewTopic = details.can_reply_as_new_topic;
postAtts.canReviewTopic = !!details.can_review_topic;
postAtts.isWarning = topic.is_warning; postAtts.isWarning = topic.is_warning;
postAtts.links = post.get("internalLinks"); postAtts.links = post.get("internalLinks");
postAtts.replyDirectlyBelow = postAtts.replyDirectlyBelow =
@ -208,22 +199,17 @@ export default function transformPost(
if (post.actions_summary) { if (post.actions_summary) {
postAtts.actionsSummary = post.actions_summary postAtts.actionsSummary = post.actions_summary
.filter(a => { .filter(a => {
return a.actionType.name_key !== "like" && a.count > 0; return a.actionType.name_key !== "like" && a.acted;
}) })
.map(a => { .map(a => {
const acted = a.acted;
const action = a.actionType.name_key; const action = a.actionType.name_key;
const count = a.count;
return { return {
id: a.id, id: a.id,
postId: post.id, postId: post.id,
action, action,
acted,
count,
canUndo: a.can_undo, canUndo: a.can_undo,
canIgnoreFlags: a.can_defer_flags, description: I18n.t(`post.actions.by_you.${action}`)
description: actionDescription(action, acted, count)
}; };
}); });
} }

View File

@ -33,8 +33,6 @@ export default RestModel.extend({
act(post, opts) { act(post, opts) {
if (!opts) opts = {}; if (!opts) opts = {};
const action = this.get("actionType.name_key");
// Mark it as acted // Mark it as acted
this.setProperties({ this.setProperties({
acted: true, acted: true,
@ -43,13 +41,7 @@ export default RestModel.extend({
can_undo: true can_undo: true
}); });
if (action === "notify_moderators" || action === "notify_user") {
this.set("can_undo", false);
this.set("can_defer_flags", false);
}
// Create our post action // Create our post action
const self = this;
return ajax("/post_actions", { return ajax("/post_actions", {
type: "POST", type: "POST",
data: { data: {
@ -62,8 +54,8 @@ export default RestModel.extend({
}, },
returnXHR: true returnXHR: true
}) })
.then(function(data) { .then(data => {
if (!self.get("flagTopic")) { if (!this.get("flagTopic")) {
post.updateActionsSummary(data.result); post.updateActionsSummary(data.result);
} }
const remaining = parseInt( const remaining = parseInt(
@ -74,9 +66,9 @@ export default RestModel.extend({
); );
return { acted: true, remaining, max }; return { acted: true, remaining, max };
}) })
.catch(function(error) { .catch(error => {
popupAjaxError(error); popupAjaxError(error);
self.removeAction(post); this.removeAction(post);
}); });
}, },
@ -92,12 +84,5 @@ export default RestModel.extend({
post.updateActionsSummary(result); post.updateActionsSummary(result);
return { acted: false }; return { acted: false };
}); });
},
deferFlags(post) {
return ajax("/post_actions/defer_flags", {
type: "POST",
data: { post_action_type_id: this.get("id"), id: post.get("id") }
}).then(() => this.set("count", 0));
} }
}); });

View File

@ -74,57 +74,11 @@ createWidget("action-link", {
} }
}); });
createWidget("actions-summary-item", {
tagName: "div.post-action",
buildKey: attrs => `actions-summary-item-${attrs.id}`,
defaultState() {
return { users: null };
},
template: hbs`
{{#if state.users}}
{{small-user-list users=state.users description=(concat "post.actions.people." attrs.action)}}
{{else}}
{{action-link action="whoActed" text=attrs.description}}
{{/if}}
{{#if attrs.canUndo}}
{{action-link action="undo" className="undo" text=(i18n (concat "post.actions.undo." attrs.action))}}
{{/if}}
{{#if attrs.canIgnoreFlags}}
{{action-link action="deferFlags" className="defer-flags" text=(i18n "post.actions.defer_flags" count=attrs.count)}}
{{/if}}
`,
whoActed() {
const attrs = this.attrs;
const state = this.state;
return this.store
.find("post-action-user", {
id: attrs.postId,
post_action_type_id: attrs.id
})
.then(users => {
state.users = users.map(avatarAtts);
});
},
undo() {
this.sendWidgetAction("undoPostAction", this.attrs.id);
},
deferFlags() {
this.sendWidgetAction("deferPostActionFlags", this.attrs.id);
}
});
export default createWidget("actions-summary", { export default createWidget("actions-summary", {
tagName: "section.post-actions", tagName: "section.post-actions",
template: hbs` template: hbs`
{{#each attrs.actionsSummary as |as|}} {{#each attrs.actionsSummary as |as|}}
{{actions-summary-item attrs=as}} <div class='post-action'>{{as.description}}</div>
<div class='clearfix'></div> <div class='clearfix'></div>
{{/each}} {{/each}}
{{#if attrs.deleted_at}} {{#if attrs.deleted_at}}

View File

@ -64,7 +64,8 @@ registerButton("like", attrs => {
const button = { const button = {
action: "like", action: "like",
icon: attrs.liked ? "d-liked" : "d-unliked", icon: attrs.liked ? "d-liked" : "d-unliked",
className className,
before: "like-count"
}; };
// If the user has already liked the post and doesn't have permission // If the user has already liked the post and doesn't have permission
@ -99,7 +100,7 @@ registerButton("like-count", attrs => {
return { return {
action: "toggleWhoLiked", action: "toggleWhoLiked",
title, title,
className: `like-count highlight-action ${additionalClass}`, className: `button-count like-count highlight-action ${additionalClass}`,
contents: count, contents: count,
icon, icon,
iconRight: true, iconRight: true,
@ -108,14 +109,30 @@ registerButton("like-count", attrs => {
} }
}); });
registerButton("flag-count", attrs => {
let className = "button-count";
if (attrs.reviewableScorePendingCount > 0) {
className += " has-pending";
}
return {
className,
contents: h("span", attrs.reviewableScoreCount.toString()),
url: `/review/${attrs.reviewableId}`
};
});
registerButton("flag", attrs => { registerButton("flag", attrs => {
if (attrs.canFlag && !attrs.hidden) { if (attrs.reviewableId || (attrs.canFlag && !attrs.hidden)) {
return { let button = {
action: "showFlags", action: "showFlags",
title: "post.controls.flag", title: "post.controls.flag",
icon: "flag", icon: "flag",
className: "create-flag" className: "create-flag"
}; };
if (attrs.reviewableId) {
button.before = "flag-count";
}
return button;
} }
}); });
@ -323,7 +340,12 @@ export default createWidget("post-menu", {
attachButton(name) { attachButton(name) {
let buttonAtts = buildButton(name, this); let buttonAtts = buildButton(name, this);
if (buttonAtts) { if (buttonAtts) {
return this.attach(this.settings.buttonType, buttonAtts); let button = this.attach(this.settings.buttonType, buttonAtts);
if (buttonAtts.before) {
let before = this.attachButton(buttonAtts.before);
return h("div.double-button", [before, button]);
}
return button;
} }
}, },
@ -359,22 +381,21 @@ export default createWidget("post-menu", {
replaceButton(orderedButtons, "reply", "wiki-edit"); replaceButton(orderedButtons, "reply", "wiki-edit");
} }
orderedButtons orderedButtons.forEach(i => {
.filter(x => x !== "like-count" && x !== "like") const button = this.attachButton(i, attrs);
.forEach(i => {
const button = this.attachButton(i, attrs);
if (button) { if (button) {
allButtons.push(button); allButtons.push(button);
if ( if (
(attrs.yours && button.attrs.alwaysShowYours) || (attrs.yours && button.attrs && button.attrs.alwaysShowYours) ||
hiddenButtons.indexOf(i) === -1 (attrs.reviewableId && i === "flag") ||
) { hiddenButtons.indexOf(i) === -1
visibleButtons.push(button); ) {
} visibleButtons.push(button);
} }
}); }
});
if (!this.settings.collapseButtons) { if (!this.settings.collapseButtons) {
visibleButtons = allButtons; visibleButtons = allButtons;
@ -397,13 +418,6 @@ export default createWidget("post-menu", {
visibleButtons.splice(visibleButtons.length - 1, 0, showMore); visibleButtons.splice(visibleButtons.length - 1, 0, showMore);
} }
visibleButtons.unshift(
h("div.like-button", [
this.attachButton("like-count", attrs),
this.attachButton("like", attrs)
])
);
Object.values(_extraButtons).forEach(builder => { Object.values(_extraButtons).forEach(builder => {
if (builder) { if (builder) {
const buttonAtts = builder(attrs, this.state, this.siteSettings); const buttonAtts = builder(attrs, this.state, this.siteSettings);

View File

@ -711,21 +711,5 @@ export default createWidget("post", {
bootbox.alert(I18n.t("post.few_likes_left")); bootbox.alert(I18n.t("post.few_likes_left"));
kvs.set({ key: "lastWarnedLikes", value: new Date().getTime() }); kvs.set({ key: "lastWarnedLikes", value: new Date().getTime() });
} }
},
undoPostAction(typeId) {
const post = this.model;
return post
.get("actions_summary")
.findBy("id", typeId)
.undo(post);
},
deferPostActionFlags(typeId) {
const post = this.model;
return post
.get("actions_summary")
.findBy("id", typeId)
.deferFlags(post);
} }
}); });

View File

@ -1,3 +1,14 @@
.button-count.has-pending {
span {
background-color: $danger;
color: $secondary;
border-radius: 10px;
padding: 0.25em 0.5em;
display: inline-block;
font-size: 0.8em;
}
}
.placeholder-avatar { .placeholder-avatar {
display: inline-block; display: inline-block;
width: 45px; width: 45px;
@ -243,6 +254,7 @@ blockquote {
} }
.post-action { .post-action {
color: $primary-medium;
.undo-action, .undo-action,
.act-action { .act-action {
margin-left: 5px; margin-left: 5px;

View File

@ -46,19 +46,23 @@ section.post-menu-area {
nav.post-controls { nav.post-controls {
padding: 0; padding: 0;
.like-button {
// Like button wrapper // Some buttons can be doubled up, like likes or flags
.double-button {
display: inline-flex; display: inline-flex;
color: $primary-low-mid; color: $primary-low-mid;
margin-right: 0.15em; margin-right: 0.15em;
&:hover { &:hover {
// Like button wrapper on hover
button { button {
background: $primary-low; background: $primary-low;
color: $primary-medium; color: $primary-medium;
} }
} }
button { button {
// It looks really confusing when one half a double button has an inner shadow on click.
&:active {
box-shadow: none;
}
margin-left: 0; margin-left: 0;
margin-right: 0; margin-right: 0;
&.my-likes { &.my-likes {
@ -93,7 +97,7 @@ nav.post-controls {
// Disabled like button // Disabled like button
cursor: not-allowed; cursor: not-allowed;
} }
&.like-count { &.button-count {
// Like count button // Like count button
&:not(.my-likes) { &:not(.my-likes) {
padding-right: 0; padding-right: 0;

View File

@ -49,16 +49,6 @@ class PostActionsController < ApplicationController
end end
end end
def defer_flags
guardian.ensure_can_defer_flags!(@post)
if reviewable = @post.reviewable_flag
reviewable.perform(current_user, :ignore)
end
render json: { success: true }
end
private private
def fetch_post_from_params def fetch_post_from_params

View File

@ -470,10 +470,6 @@ class Post < ActiveRecord::Base
post_actions.where(post_action_type_id: PostActionType.flag_types_without_custom.values, deleted_at: nil).count != 0 post_actions.where(post_action_type_id: PostActionType.flag_types_without_custom.values, deleted_at: nil).count != 0
end end
def active_flags
post_actions.active.where(post_action_type_id: PostActionType.flag_types_without_custom.values)
end
def reviewable_flag def reviewable_flag
ReviewableFlaggedPost.pending.find_by(target: self) ReviewableFlaggedPost.pending.find_by(target: self)
end end

View File

@ -73,7 +73,10 @@ class PostSerializer < BasicPostSerializer
:notice_args, :notice_args,
:last_wiki_edit, :last_wiki_edit,
:locked, :locked,
:excerpt :excerpt,
:reviewable_id,
:reviewable_score_count,
:reviewable_score_pending_count
def initialize(object, opts) def initialize(object, opts)
super(object, opts) super(object, opts)
@ -251,14 +254,6 @@ class PostSerializer < BasicPostSerializer
summary.delete(:can_act) summary.delete(:can_act)
end end
# The following only applies if you're logged in
if summary[:can_act] && scope.current_user.present?
summary[:can_defer_flags] = true if scope.is_staff? &&
PostActionType.flag_types_without_custom.values.include?(id) &&
active_flags.present? && active_flags.has_key?(id) &&
active_flags[id] > 0
end
if actions.present? && actions.has_key?(id) if actions.present? && actions.has_key?(id)
summary[:acted] = true summary[:acted] = true
summary[:can_undo] = true if scope.can_delete?(actions[id]) summary[:can_undo] = true if scope.can_delete?(actions[id])
@ -416,7 +411,62 @@ class PostSerializer < BasicPostSerializer
object.hidden object.hidden
end end
private # If we have a topic view, it has bulk values for the reviewable content we can use
def reviewable_id
if @topic_view.present?
for_post = @topic_view.reviewable_counts[object.id]
return for_post ? for_post[:reviewable_id] : 0
end
reviewable&.id
end
def include_reviewable_id?
can_review_topic?
end
def reviewable_score_count
if @topic_view.present?
for_post = @topic_view.reviewable_counts[object.id]
return for_post ? for_post[:total] : 0
end
reviewable_scores.size
end
def include_reviewable_score_count?
can_review_topic?
end
def reviewable_score_pending_count
if @topic_view.present?
for_post = @topic_view.reviewable_counts[object.id]
return for_post ? for_post[:pending] : 0
end
reviewable_scores.count { |rs| rs.pending? }
end
def include_reviewable_score_pending_count?
can_review_topic?
end
private
def can_review_topic?
return @can_review_topic unless @can_review_topic.nil?
@can_review_topic = @topic_view&.can_review_topic
@can_review_topic ||= scope.can_review_topic?(object.topic)
@can_review_topic
end
def reviewable
@reviewable ||= Reviewable.where(target: object).includes(:reviewable_scores).first
end
def reviewable_scores
reviewable&.reviewable_scores&.to_a || []
end
def user_custom_fields_object def user_custom_fields_object
(@topic_view&.user_custom_fields || @options[:user_custom_fields] || {}) (@topic_view&.user_custom_fields || @options[:user_custom_fields] || {})
@ -432,10 +482,6 @@ class PostSerializer < BasicPostSerializer
@post_actions ||= (@topic_view&.all_post_actions || {})[object.id] @post_actions ||= (@topic_view&.all_post_actions || {})[object.id]
end end
def active_flags
@active_flags ||= (@topic_view&.all_active_flags || {})[object.id]
end
def post_custom_fields def post_custom_fields
@post_custom_fields ||= if @topic_view @post_custom_fields ||= if @topic_view
(@topic_view.post_custom_fields || {})[object.id] || {} (@topic_view.post_custom_fields || {})[object.id] || {}

View File

@ -11,7 +11,8 @@ class TopicViewDetailsSerializer < ApplicationSerializer
:can_create_post, :can_create_post,
:can_reply_as_new_topic, :can_reply_as_new_topic,
:can_flag_topic, :can_flag_topic,
:can_convert_topic] :can_convert_topic,
:can_review_topic]
end end
attributes( attributes(
@ -77,6 +78,10 @@ class TopicViewDetailsSerializer < ApplicationSerializer
define_method(ca) { true } define_method(ca) { true }
end end
def include_can_review_topic?
scope.can_review_topic?(object.topic)
end
def include_can_move_posts? def include_can_move_posts?
scope.can_move_posts?(object.topic) scope.can_move_posts?(object.topic)
end end

View File

@ -190,10 +190,9 @@ basic:
post_menu: post_menu:
client: true client: true
type: list type: list
default: "like-count|like|share|flag|edit|bookmark|delete|admin|reply" default: "like|share|flag|edit|bookmark|delete|admin|reply"
allow_any: false allow_any: false
choices: choices:
- like-count
- like - like
- edit - edit
- flag - flag

View File

@ -0,0 +1,9 @@
class RemoveLikeCountFromPostMenu < ActiveRecord::Migration[5.2]
def up
execute(<<~SQL)
UPDATE site_settings
SET value = REGEXP_REPLACE(REPLACE(REPLACE(value, 'like-count', ''), '||', '|'), '^\\|', '')
WHERE name = 'post_menu'
SQL
end
end

View File

@ -79,10 +79,6 @@ module PostGuardian
can_see_post?(post) && is_staff? can_see_post?(post) && is_staff?
end end
def can_defer_flags?(post)
can_see_post?(post) && is_staff? && post
end
# Can we see who acted on a post in a particular way? # Can we see who acted on a post in a particular way?
def can_see_post_actors?(topic, post_action_type_id) def can_see_post_actors?(topic, post_action_type_id)
return true if is_admin? return true if is_admin?

View File

@ -10,6 +10,16 @@ module TopicGuardian
) )
end end
def can_review_topic?(topic)
return false if anonymous? || topic.nil?
return true if is_staff?
SiteSetting.enable_category_group_review? &&
topic.category.present? &&
topic.category.reviewable_by_group_id.present? &&
GroupUser.where(group_id: topic.category.reviewable_by_group_id, user_id: user.id).exists?
end
def can_create_shared_draft? def can_create_shared_draft?
is_staff? && SiteSetting.shared_drafts_enabled? is_staff? && SiteSetting.shared_drafts_enabled?
end end

View File

@ -15,7 +15,8 @@ class TopicView
:print, :print,
:message_bus_last_id, :message_bus_last_id,
:queued_posts_enabled, :queued_posts_enabled,
:personal_message :personal_message,
:can_review_topic
) )
attr_accessor( attr_accessor(
@ -100,6 +101,7 @@ class TopicView
@draft_key = @topic.draft_key @draft_key = @topic.draft_key
@draft_sequence = DraftSequence.current(@user, @draft_key) @draft_sequence = DraftSequence.current(@user, @draft_key)
@can_review_topic = @guardian.can_review_topic?(@topic)
@queued_posts_enabled = NewPostManager.queue_enabled? @queued_posts_enabled = NewPostManager.queue_enabled?
@personal_message = @topic.private_message? @personal_message = @topic.private_message?
end end
@ -410,16 +412,32 @@ class TopicView
@all_post_actions ||= PostAction.counts_for(@posts, @user) @all_post_actions ||= PostAction.counts_for(@posts, @user)
end end
def all_active_flags
@all_active_flags ||= ReviewableFlaggedPost.counts_for(@posts)
end
def links def links
@links ||= TopicLink.topic_map(@guardian, @topic.id) @links ||= TopicLink.topic_map(@guardian, @topic.id)
end end
def reviewable_counts
if @reviewable_counts.blank?
# Create a hash with counts by post so we can quickly look up whether there is reviewable content.
@reviewable_counts = {}
Reviewable.
where(target_type: 'Post', target_id: filtered_post_ids).
includes(:reviewable_scores).each do |r|
for_post = (@reviewable_counts[r.target_id] ||= { total: 0, pending: 0, reviewable_id: r.id })
r.reviewable_scores.each do |s|
for_post[:total] += 1
for_post[:pending] += 1 if s.pending?
end
end
end
@reviewable_counts
end
def pending_posts def pending_posts
ReviewableQueuedPost.pending.where(created_by: @user, topic: @topic).order(:created_at) @pending_posts ||= ReviewableQueuedPost.pending.where(created_by: @user, topic: @topic).order(:created_at)
end end
def actions_summary def actions_summary

View File

@ -249,29 +249,6 @@ describe Guardian do
end end
end end
describe "can_defer_flags" do
let(:post) { Fabricate(:post) }
let(:user) { post.user }
let(:moderator) { Fabricate(:moderator) }
it "returns false when the user is nil" do
expect(Guardian.new(nil).can_defer_flags?(post)).to be_falsey
end
it "returns false when the post is nil" do
expect(Guardian.new(moderator).can_defer_flags?(nil)).to be_falsey
end
it "returns false when the user is not a moderator" do
expect(Guardian.new(user).can_defer_flags?(post)).to be_falsey
end
it "returns true when the user is a moderator" do
expect(Guardian.new(moderator).can_defer_flags?(post)).to be_truthy
end
end
describe 'can_send_private_message' do describe 'can_send_private_message' do
let(:user) { Fabricate(:user) } let(:user) { Fabricate(:user) }
let(:another_user) { Fabricate(:user) } let(:another_user) { Fabricate(:user) }
@ -1672,6 +1649,28 @@ describe Guardian do
end end
end end
context "can_review_topic?" do
it 'returns false with a nil object' do
expect(Guardian.new(user).can_review_topic?(nil)).to eq(false)
end
it 'returns true for a staff user' do
expect(Guardian.new(moderator).can_review_topic?(topic)).to eq(true)
end
it 'returns false for a regular user' do
expect(Guardian.new(user).can_review_topic?(topic)).to eq(false)
end
it 'returns false for a regular user' do
SiteSetting.enable_category_group_review = true
group = Fabricate(:group)
GroupUser.create!(group_id: group.id, user_id: user.id)
topic.category.update!(reviewable_by_group_id: group.id)
expect(Guardian.new(user).can_review_topic?(topic)).to eq(true)
end
end
context 'can_move_posts?' do context 'can_move_posts?' do
it 'returns false with a nil object' do it 'returns false with a nil object' do

View File

@ -307,28 +307,6 @@ describe TopicView do
end end
end end
context '.all_active_flags' do
it 'is blank at first' do
expect(topic_view.all_active_flags).to be_blank
end
it 'returns the active flags' do
PostActionCreator.off_topic(moderator, p1)
PostActionCreator.off_topic(evil_trout, p1)
expect(topic_view.all_active_flags[p1.id][PostActionType.types[:off_topic]]).to eq(2)
end
it 'returns only the active flags' do
reviewable = PostActionCreator.off_topic(moderator, p1).reviewable
PostActionCreator.off_topic(evil_trout, p1)
reviewable.perform(moderator, :ignore)
expect(topic_view.all_active_flags[p1.id]).to eq(nil)
end
end
context '.read?' do context '.read?' do
it 'tracks correctly' do it 'tracks correctly' do
# anon is assumed to have read everything # anon is assumed to have read everything

View File

@ -261,62 +261,4 @@ RSpec.describe PostActionsController do
end end
end end
end end
describe '#defer_flags' do
let(:flagged_post) { Fabricate(:post, user: Fabricate(:coding_horror)) }
let!(:reviewable) do
PostActionCreator.spam(Fabricate(:user), flagged_post).reviewable
end
context "not logged in" do
it "should not allow them to clear flags" do
post "/post_actions/defer_flags.json", params: { id: flagged_post.id }
expect(response.status).to eq(403)
expect(reviewable.reload).not_to be_ignored
end
end
context 'logged in' do
let!(:user) { sign_in(Fabricate(:moderator)) }
it "raises an error without a post_action_type_id" do
post "/post_actions/defer_flags.json", params: { id: flagged_post.id }
expect(response.status).to eq(400)
expect(reviewable.reload).not_to be_ignored
end
it "raises an error when the user doesn't have access" do
sign_in(Fabricate(:user))
post "/post_actions/defer_flags.json", params: {
id: flagged_post.id, post_action_type_id: PostActionType.types[:spam]
}
expect(response).to be_forbidden
expect(reviewable.reload).not_to be_ignored
end
context "success" do
it "performs the ignore" do
post "/post_actions/defer_flags.json", params: {
id: flagged_post.id, post_action_type_id: PostActionType.types[:spam]
}
expect(response.status).to eq(200)
expect(reviewable.reload).to be_ignored
end
it "works with a deleted post" do
flagged_post.trash!(user)
post "/post_actions/defer_flags.json", params: {
id: flagged_post.id, post_action_type_id: PostActionType.types[:spam]
}
expect(response.status).to eq(200)
expect(reviewable.reload).to be_ignored
end
end
end
end
end end

View File

@ -44,6 +44,18 @@ describe PostSerializer do
end end
end end
context "a post with reviewable content" do
let!(:post) { Fabricate(:post, user: Fabricate(:user)) }
let!(:reviewable) { PostActionCreator.spam(Fabricate(:user), post).reviewable }
it "includes the reviewable data" do
json = PostSerializer.new(post, scope: Guardian.new(Fabricate(:moderator)), root: false).as_json
expect(json[:reviewable_id]).to eq(reviewable.id)
expect(json[:reviewable_score_count]).to eq(1)
expect(json[:reviewable_score_pending_count]).to eq(1)
end
end
context "a post by a nuked user" do context "a post by a nuked user" do
let!(:post) { Fabricate(:post, user: Fabricate(:user), deleted_at: Time.zone.now) } let!(:post) { Fabricate(:post, user: Fabricate(:user), deleted_at: Time.zone.now) }

View File

@ -134,6 +134,27 @@ describe TopicViewSerializer do
end end
end end
context "with flags" do
let!(:post) { Fabricate(:post, topic: topic) }
let!(:other_post) { Fabricate(:post, topic: topic) }
it "will return reviewable counts on posts" do
r = PostActionCreator.inappropriate(Fabricate(:user), post).reviewable
r.perform(admin, :agree_and_keep)
PostActionCreator.spam(Fabricate(:user), post)
json = serialize_topic(topic, admin)
p0 = json[:post_stream][:posts][0]
expect(p0[:id]).to eq(post.id)
expect(p0[:reviewable_score_count]).to eq(2)
expect(p0[:reviewable_score_pending_count]).to eq(1)
p1 = json[:post_stream][:posts][1]
expect(p1[:reviewable_score_count]).to eq(0)
expect(p1[:reviewable_score_pending_count]).to eq(0)
end
end
describe "pending posts" do describe "pending posts" do
context "when the queue is enabled" do context "when the queue is enabled" do
before do before do
@ -185,6 +206,7 @@ describe TopicViewSerializer do
expect(details[:notification_level]).to be_present expect(details[:notification_level]).to be_present
expect(details[:can_move_posts]).to eq(true) expect(details[:can_move_posts]).to eq(true)
expect(details[:can_flag_topic]).to eq(true) expect(details[:can_flag_topic]).to eq(true)
expect(details[:can_review_topic]).to eq(true)
expect(details[:links][0][:clicks]).to eq(100) expect(details[:links][0][:clicks]).to eq(100)
participant = details[:participants].find { |p| p[:id] == user.id } participant = details[:participants].find { |p| p[:id] == user.id }

View File

@ -31,13 +31,13 @@ export default {
raw: raw:
"Any plans to support localization of UI elements, so that I (for example) could set up a completely German speaking forum?", "Any plans to support localization of UI elements, so that I (for example) could set up a completely German speaking forum?",
actions_summary: [ actions_summary: [
{ id: 2, count: 0, hidden: false, can_act: true, can_defer_flags: false }, { id: 2, count: 0, hidden: false, can_act: true },
{ id: 3, count: 0, hidden: false, can_act: true, can_defer_flags: false }, { id: 3, count: 0, hidden: false, can_act: true },
{ id: 4, count: 0, hidden: false, can_act: true, can_defer_flags: false }, { id: 4, count: 0, hidden: false, can_act: true },
{ id: 5, count: 0, hidden: true, can_act: true, can_defer_flags: false }, { id: 5, count: 0, hidden: true, can_act: true },
{ id: 6, count: 0, hidden: false, can_act: true, can_defer_flags: false }, { id: 6, count: 0, hidden: false, can_act: true },
{ id: 7, count: 0, hidden: false, can_act: true, can_defer_flags: false }, { id: 7, count: 0, hidden: false, can_act: true },
{ id: 8, count: 0, hidden: false, can_act: true, can_defer_flags: false } { id: 8, count: 0, hidden: false, can_act: true }
], ],
moderator: false, moderator: false,
admin: false, admin: false,

File diff suppressed because it is too large Load Diff

View File

@ -2,73 +2,6 @@ import { moduleForWidget, widgetTest } from "helpers/widget-test";
moduleForWidget("actions-summary"); moduleForWidget("actions-summary");
widgetTest("listing actions", {
template: '{{mount-widget widget="actions-summary" args=args}}',
beforeEach() {
this.set("args", {
actionsSummary: [
{ id: 1, action: "off_topic", description: "very off topic" },
{ id: 2, action: "spam", description: "suspicious message" }
]
});
},
async test(assert) {
assert.equal(find(".post-actions .post-action").length, 2);
await click(".post-action:eq(0) .action-link a");
assert.equal(
find(".post-action:eq(0) img.avatar").length,
1,
"clicking it shows the user"
);
}
});
widgetTest("undo", {
template:
'{{mount-widget widget="actions-summary" args=args undoPostAction=undoPostAction}}',
beforeEach() {
this.set("args", {
actionsSummary: [
{ action: "off_topic", description: "very off topic", canUndo: true }
]
});
this.set("undoPostAction", () => (this.undid = true));
},
async test(assert) {
assert.equal(find(".post-actions .post-action").length, 1);
await click(".action-link.undo");
assert.ok(this.undid, "it triggered the action");
}
});
widgetTest("deferFlags", {
template:
'{{mount-widget widget="actions-summary" args=args deferPostActionFlags=(action "deferPostActionFlags")}}',
beforeEach() {
this.set("args", {
actionsSummary: [
{
action: "off_topic",
description: "very off topic",
canIgnoreFlags: true,
count: 1
}
]
});
this.on("deferPostActionFlags", () => (this.deferred = true));
},
async test(assert) {
assert.equal(find(".post-actions .post-action").length, 1);
await click(".action-link.defer-flags");
assert.ok(this.deferred, "it triggered the action");
}
});
widgetTest("post deleted", { widgetTest("post deleted", {
template: '{{mount-widget widget="actions-summary" args=args}}', template: '{{mount-widget widget="actions-summary" args=args}}',
beforeEach() { beforeEach() {