FEATURE: Allow group moderators to close/archive topics

* FEATURE: Allow group moderators to close/archive topics
This commit is contained in:
jbrw 2020-07-14 12:36:19 -04:00 committed by GitHub
parent cc6d722de1
commit 06073fe8c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 215 additions and 38 deletions

View File

@ -119,7 +119,9 @@ export default function transformPost(
postAtts.canManage = currentUser && currentUser.get("canManageTopic");
postAtts.canViewRawEmail =
currentUser && (currentUser.id === post.user_id || currentUser.staff);
postAtts.canReplyAsNewTopic = details.can_reply_as_new_topic;
postAtts.canArchiveTopic = !!details.can_archive_topic;
postAtts.canCloseTopic = !!details.can_close_topic;
postAtts.canReplyAsNewTopic = !!details.can_reply_as_new_topic;
postAtts.canReviewTopic = !!details.can_review_topic;
postAtts.canPublishPage =
!!details.can_publish_page && post.post_number === 1;

View File

@ -81,7 +81,7 @@
<section>
<h3>{{i18n "category.settings_sections.moderation"}}</h3>
{{#if siteSettings.enable_category_group_review}}
{{#if siteSettings.enable_category_group_moderation}}
<section class="field">
<label for="reviewable-by-group">
{{i18n "category.reviewable_by_group"}}

View File

@ -158,7 +158,9 @@ export default createWidget("topic-admin-menu", {
label: "actions.recover"
});
}
}
if (this.currentUser && details.get("can_close_topic")) {
if (topic.get("closed")) {
this.addActionButton({
className: "topic-admin-open",
@ -176,7 +178,9 @@ export default createWidget("topic-admin-menu", {
label: "actions.close"
});
}
}
if (this.currentUser && this.currentUser.get("canManageTopic")) {
this.addActionButton({
className: "topic-admin-status-update",
buttonClass: "popup-menu-btn",
@ -212,7 +216,9 @@ export default createWidget("topic-admin-menu", {
icon: "anchor",
label: "actions.reset_bump_date"
});
}
if (this.currentUser && details.get("can_archive_topic")) {
if (!isPrivateMessage) {
this.addActionButton({
className: "topic-admin-archive",
@ -222,7 +228,9 @@ export default createWidget("topic-admin-menu", {
label: topic.get("archived") ? "actions.unarchive" : "actions.archive"
});
}
}
if (this.currentUser && this.currentUser.get("canManageTopic")) {
this.addActionButton({
className: "topic-admin-visible",
buttonClass: "popup-menu-btn",

View File

@ -335,7 +335,7 @@ class CategoriesController < ApplicationController
allowed_tags: [],
allowed_tag_groups: []
)
if SiteSetting.enable_category_group_review?
if SiteSetting.enable_category_group_moderation?
result[:reviewable_by_group_id] = Group.find_by(name: params[:reviewable_by_group_name])&.id
end

View File

@ -40,7 +40,7 @@ class ReviewableClaimedTopicsController < ApplicationController
def notify_users(topic, claimed_by)
user_ids = User.staff.pluck(:id)
if SiteSetting.enable_category_group_review? && group_id = topic.category&.reviewable_by_group_id.presence
if SiteSetting.enable_category_group_moderation? && group_id = topic.category&.reviewable_by_group_id.presence
user_ids.concat(GroupUser.where(group_id: group_id).pluck(:user_id))
user_ids.uniq!
end

View File

@ -412,7 +412,16 @@ class TopicsController < ApplicationController
check_for_status_presence(:status, status)
@topic = Topic.find_by(id: topic_id)
guardian.ensure_can_moderate!(@topic)
case status
when 'closed'
guardian.ensure_can_close_topic!(@topic)
when 'archived'
guardian.ensure_can_archive_topic!(@topic)
else
guardian.ensure_can_moderate!(@topic)
end
@topic.update_status(status, enabled, current_user, until: params[:until])
render json: success_json.merge!(

View File

@ -10,7 +10,7 @@ class Jobs::NotifyReviewable < ::Jobs::Base
notify_admins
notify_moderators if reviewable.reviewable_by_moderator?
if SiteSetting.enable_category_group_review? && reviewable.reviewable_by_group.present?
if SiteSetting.enable_category_group_moderation? && reviewable.reviewable_by_group.present?
notify_group(reviewable.reviewable_by_group)
end
end

View File

@ -16,7 +16,7 @@ module Jobs
user = topic_timer.user
if Guardian.new(user).can_close?(topic)
if Guardian.new(user).can_close_topic?(topic)
if state == false && topic.auto_close_threshold_reached?
topic.set_or_create_timer(
TopicTimer.types[:open],

View File

@ -768,7 +768,7 @@ class Category < ActiveRecord::Base
end
def update_reviewables
if SiteSetting.enable_category_group_review? && saved_change_to_reviewable_by_group_id?
if SiteSetting.enable_category_group_moderation? && saved_change_to_reviewable_by_group_id?
Reviewable.where(category_id: id).update_all(reviewable_by_group_id: reviewable_by_group_id)
end
end

View File

@ -281,7 +281,7 @@ class Reviewable < ActiveRecord::Base
end
def apply_review_group
return unless SiteSetting.enable_category_group_review? &&
return unless SiteSetting.enable_category_group_moderation? &&
category.present? &&
category.reviewable_by_group_id
@ -419,7 +419,7 @@ class Reviewable < ActiveRecord::Base
end
return result if user.admin?
group_ids = SiteSetting.enable_category_group_review? ? user.group_users.pluck(:group_id) : []
group_ids = SiteSetting.enable_category_group_moderation? ? user.group_users.pluck(:group_id) : []
result.where(
'(reviewables.reviewable_by_moderator AND :staff) OR (reviewables.reviewable_by_group_id IN (:group_ids))',

View File

@ -292,7 +292,7 @@ protected
user_ids = User.staff.pluck(:id)
if SiteSetting.enable_category_group_review? && group_id = topic.category&.reviewable_by_group_id.presence
if SiteSetting.enable_category_group_moderation? && group_id = topic.category&.reviewable_by_group_id.presence
user_ids.concat(GroupUser.where(group_id: group_id).pluck(:user_id))
user_ids.uniq!
end

View File

@ -26,7 +26,7 @@ class CategorySerializer < SiteCategorySerializer
end
def include_reviewable_by_group_name?
SiteSetting.enable_category_group_review? && object.reviewable_by_group_id.present?
SiteSetting.enable_category_group_moderation? && object.reviewable_by_group_id.present?
end
def group_permissions

View File

@ -16,7 +16,9 @@ class TopicViewDetailsSerializer < ApplicationSerializer
:can_convert_topic,
:can_review_topic,
:can_edit_tags,
:can_publish_page]
:can_publish_page,
:can_close_topic,
:can_archive_topic]
end
attributes(
@ -134,6 +136,14 @@ class TopicViewDetailsSerializer < ApplicationSerializer
!scope.can_edit?(object.topic) && scope.can_edit_tags?(object.topic)
end
def include_can_close_topic?
scope.can_close_topic?(object.topic)
end
def include_can_archive_topic?
scope.can_archive_topic?(object.topic)
end
def include_can_publish_page?
scope.can_publish_page?(object.topic)
end

View File

@ -2888,7 +2888,7 @@ en:
default_list_filter: "Default List Filter:"
allow_badges_label: "Allow badges to be awarded in this category"
edit_permissions: "Edit Permissions"
reviewable_by_group: "In addition to staff, posts and flags in this category can be also be reviewed by:"
reviewable_by_group: "In addition to staff, content in this category can be also be reviewed by:"
review_group_name: "group name"
require_topic_approval: "Require moderator approval of all new topics"
require_reply_approval: "Require moderator approval of all new replies"

View File

@ -2016,7 +2016,7 @@ en:
staff_user_custom_fields: "A list of user custom fields that can be retrieved for staff members with the API."
enable_user_directory: "Provide a directory of users for browsing"
enable_group_directory: "Provide a directory of groups for browsing"
enable_category_group_review: "Allow groups to review content in specific categories"
enable_category_group_moderation: "Allow groups to moderate content in specific categories"
group_in_subject: "Set %%{optional_pm} in email subject to name of first group in PM, see: <a href='https://meta.discourse.org/t/customize-subject-format-for-standard-emails/20801' target='_blank'>Customize subject format for standard emails</a>"
allow_anonymous_posting: "Allow users to switch to anonymous mode"
anonymous_posting_min_trust_level: "Minimum trust level required to enable anonymous posting"

View File

@ -600,7 +600,7 @@ groups:
default: true
group_in_subject:
default: false
enable_category_group_review:
enable_category_group_moderation:
client: true
default: false

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
class RenameCategoryGroupModerationSetting < ActiveRecord::Migration[6.0]
def up
execute "UPDATE site_settings SET name = 'enable_category_group_moderation' WHERE name = 'enable_category_group_review'"
execute "UPDATE user_histories SET subject = 'enable_category_group_moderation' WHERE subject = 'enable_category_group_review'"
end
def down
execute "UPDATE site_settings SET name = 'enable_category_group_review' WHERE name = 'enable_category_group_moderation'"
execute "UPDATE user_histories SET subject = 'enable_category_group_review' WHERE subject = 'enable_category_group_moderation'"
end
end

View File

@ -165,7 +165,6 @@ class Guardian
end
alias :can_move_posts? :can_moderate?
alias :can_see_flags? :can_moderate?
alias :can_close? :can_moderate?
def can_tag?(topic)
return false if topic.blank?

View File

@ -17,7 +17,7 @@ module TopicGuardian
return false if anonymous? || topic.nil?
return true if is_staff?
SiteSetting.enable_category_group_review? &&
SiteSetting.enable_category_group_moderation? &&
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?
@ -203,4 +203,18 @@ module TopicGuardian
false
end
def can_perform_action_available_to_group_moderators?(topic)
return false if anonymous? || topic.nil?
return true if is_staff?
return true if @user.has_trust_level?(TrustLevel[4])
SiteSetting.enable_category_group_moderation? &&
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
alias :can_archive_topic? :can_perform_action_available_to_group_moderators?
alias :can_close_topic? :can_perform_action_available_to_group_moderators?
end

View File

@ -147,7 +147,7 @@ module UserGuardian
def can_see_review_queue?
is_staff? || (
SiteSetting.enable_category_group_review &&
SiteSetting.enable_category_group_moderation &&
Reviewable
.where(reviewable_by_group_id: @user.group_users.pluck(:group_id))
.where('category_id IS NULL or category_id IN (?)', allowed_category_ids)

View File

@ -380,7 +380,7 @@ describe UserGuardian do
group = Fabricate(:group)
group.add(user)
guardian = Guardian.new(user)
SiteSetting.enable_category_group_review = true
SiteSetting.enable_category_group_moderation = true
Fabricate(:reviewable_flagged_post, reviewable_by_group: group, category: nil)
@ -391,7 +391,7 @@ describe UserGuardian do
group = Fabricate(:group)
group.add(user)
guardian = Guardian.new(user)
SiteSetting.enable_category_group_review = false
SiteSetting.enable_category_group_moderation = false
Fabricate(:reviewable_flagged_post, reviewable_by_group: group, category: nil)
@ -402,7 +402,7 @@ describe UserGuardian do
group = Fabricate(:group)
group.add(user)
guardian = Guardian.new(user)
SiteSetting.enable_category_group_review = true
SiteSetting.enable_category_group_moderation = true
category = Fabricate(:category, read_restricted: true)
Fabricate(:reviewable_flagged_post, reviewable_by_group: group, category: category)

View File

@ -1726,8 +1726,8 @@ describe Guardian 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
it 'returns true for a group member with reviewable status' do
SiteSetting.enable_category_group_moderation = true
group = Fabricate(:group)
GroupUser.create!(group_id: group.id, user_id: user.id)
topic.category.update!(reviewable_by_group_id: group.id)
@ -1735,6 +1735,50 @@ describe Guardian do
end
end
context "can_close_topic?" do
it 'returns false with a nil object' do
expect(Guardian.new(user).can_close_topic?(nil)).to eq(false)
end
it 'returns true for a staff user' do
expect(Guardian.new(moderator).can_close_topic?(topic)).to eq(true)
end
it 'returns false for a regular user' do
expect(Guardian.new(user).can_close_topic?(topic)).to eq(false)
end
it 'returns true for a group member with reviewable status' do
SiteSetting.enable_category_group_moderation = 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_close_topic?(topic)).to eq(true)
end
end
context "can_archive_topic?" do
it 'returns false with a nil object' do
expect(Guardian.new(user).can_archive_topic?(nil)).to eq(false)
end
it 'returns true for a staff user' do
expect(Guardian.new(moderator).can_archive_topic?(topic)).to eq(true)
end
it 'returns false for a regular user' do
expect(Guardian.new(user).can_archive_topic?(topic)).to eq(false)
end
it 'returns true for a group member with reviewable status' do
SiteSetting.enable_category_group_moderation = 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_archive_topic?(topic)).to eq(true)
end
end
context "can_create_topic?" do
it 'returns true for staff user' do
expect(Guardian.new(moderator).can_create_topic?(topic)).to eq(true)

View File

@ -11,7 +11,7 @@ describe Jobs::NotifyReviewable do
let(:group) { group_user.group }
it "will notify users of new reviewable content" do
SiteSetting.enable_category_group_review = true
SiteSetting.enable_category_group_moderation = true
GroupUser.create!(group_id: group.id, user_id: moderator.id)
@ -52,7 +52,7 @@ describe Jobs::NotifyReviewable do
end
it "won't notify a group when disabled" do
SiteSetting.enable_category_group_review = false
SiteSetting.enable_category_group_moderation = false
GroupUser.create!(group_id: group.id, user_id: moderator.id)
r3 = Fabricate(:reviewable, reviewable_by_moderator: true, reviewable_by_group: group)
@ -64,7 +64,7 @@ describe Jobs::NotifyReviewable do
end
it "respects visibility" do
SiteSetting.enable_category_group_review = true
SiteSetting.enable_category_group_moderation = true
Reviewable.set_priorities(medium: 2.0)
SiteSetting.reviewable_default_visibility = 'medium'

View File

@ -58,13 +58,13 @@ describe Category do
fab!(:user) { Fabricate(:user) }
it "will add the group to the reviewable" do
SiteSetting.enable_category_group_review = true
SiteSetting.enable_category_group_moderation = true
reviewable = PostActionCreator.spam(user, post).reviewable
expect(reviewable.reviewable_by_group_id).to eq(group.id)
end
it "will add the group to the reviewable even if created manually" do
SiteSetting.enable_category_group_review = true
SiteSetting.enable_category_group_moderation = true
reviewable = ReviewableFlaggedPost.create!(
created_by: user,
payload: { raw: 'test raw' },
@ -74,7 +74,7 @@ describe Category do
end
it "will not add add the group to the reviewable" do
SiteSetting.enable_category_group_review = false
SiteSetting.enable_category_group_moderation = false
reviewable = PostActionCreator.spam(user, post).reviewable
expect(reviewable.reviewable_by_group_id).to be_nil
end
@ -87,7 +87,7 @@ describe Category do
end
it "will remove the reviewable_by_group if the category is updated" do
SiteSetting.enable_category_group_review = true
SiteSetting.enable_category_group_moderation = true
reviewable = PostActionCreator.spam(user, post).reviewable
category.reviewable_by_group_id = nil
category.save!

View File

@ -105,7 +105,7 @@ RSpec.describe Reviewable, type: :model do
end
it "works with the reviewable by group" do
SiteSetting.enable_category_group_review = true
SiteSetting.enable_category_group_moderation = true
group = Fabricate(:group)
reviewable.reviewable_by_group_id = group.id
reviewable.save!
@ -121,7 +121,7 @@ RSpec.describe Reviewable, type: :model do
end
it "doesn't allow review by group when disabled" do
SiteSetting.enable_category_group_review = false
SiteSetting.enable_category_group_moderation = false
group = Fabricate(:group)
reviewable.reviewable_by_group_id = group.id
reviewable.save!

View File

@ -135,7 +135,7 @@ describe CategoriesController do
describe "success" do
it "works" do
SiteSetting.enable_category_group_review = true
SiteSetting.enable_category_group_moderation = true
readonly = CategoryGroup.permission_types[:readonly]
create_post = CategoryGroup.permission_types[:create_post]

View File

@ -655,7 +655,7 @@ RSpec.describe TopicsController do
expect(response.status).to eq(403)
end
describe 'when logged in' do
describe 'when logged in as a moderator' do
let(:topic) { Fabricate(:topic) }
before do
sign_in(moderator)
@ -703,6 +703,48 @@ RSpec.describe TopicsController do
expect(body['topic_status_update']).to eq(nil)
end
end
describe 'when logged in as a group member with reviewable status' do
fab!(:group_user) { Fabricate(:group_user) }
fab!(:category) { Fabricate(:category, reviewable_by_group: group_user.group) }
fab!(:topic) { Fabricate(:topic, category: category) }
let(:user) { group_user.user }
let(:group) { group_user.group }
before do
sign_in(user)
SiteSetting.enable_category_group_moderation = true
end
it 'should allow a group moderator to close a topic' do
put "/t/#{topic.id}/status.json", params: {
status: 'closed', enabled: 'true'
}
expect(response.status).to eq(200)
expect(topic.reload.closed).to eq(true)
end
it 'should allow a group moderator to archive a topic' do
put "/t/#{topic.id}/status.json", params: {
status: 'archived', enabled: 'true'
}
expect(response.status).to eq(200)
expect(topic.reload.archived).to eq(true)
end
it 'should not allow a group moderator to pin a topic' do
put "/t/#{topic.id}/status.json", params: {
status: 'pinned', enabled: 'true'
}
expect(response.status).to eq(403)
expect(topic.reload.pinned_at).to eq(nil)
end
end
end
describe '#destroy_timings' do

View File

@ -7,13 +7,13 @@ describe CategorySerializer do
fab!(:category) { Fabricate(:category, reviewable_by_group_id: group.id) }
it "includes the reviewable by group name if enabled" do
SiteSetting.enable_category_group_review = true
SiteSetting.enable_category_group_moderation = true
json = described_class.new(category, scope: Guardian.new, root: false).as_json
expect(json[:reviewable_by_group_name]).to eq(group.name)
end
it "doesn't include the reviewable by group name if disabled" do
SiteSetting.enable_category_group_review = false
SiteSetting.enable_category_group_moderation = false
json = described_class.new(category, scope: Guardian.new, root: false).as_json
expect(json[:reviewable_by_group_name]).to be_blank
end

View File

@ -0,0 +1,28 @@
import { acceptance, updateCurrentUser } from "helpers/qunit-helpers";
acceptance("Topic - Admin Menu Anonymous Users", { loggedIn: false });
QUnit.test("Enter as a regular user", async assert => {
await visit("/t/internationalization-localization/280");
assert.ok(exists("#topic"), "The topic was rendered");
assert.ok(!exists(".toggle-admin-menu"), "The admin menu button was not rendered");
});
acceptance("Topic - Admin Menu", { loggedIn: true });
QUnit.test("Enter as a user with group moderator permissions", async assert => {
updateCurrentUser({ moderator: false, admin: false, trust_level: 1 });
await visit("/t/topic-for-group-moderators/2480");
assert.ok(exists("#topic"), "The topic was rendered");
assert.ok(exists(".toggle-admin-menu"), "The admin menu button was rendered");
});
QUnit.test("Enter as a user with group moderator permissions", async assert => {
updateCurrentUser({ moderator: true, admin: true, trust_level: 4 });
await visit("/t/topic-for-group-moderators/2480");
assert.ok(exists("#topic"), "The topic was rendered");
assert.ok(exists(".toggle-admin-menu"), "The admin menu button was rendered");
});

View File

@ -273,7 +273,7 @@ QUnit.test(
updateCurrentUser({ moderator: true, canManageTopic: true });
const futureDateInputSelector = selectKit(".future-date-input-selector");
await visit("/t/internationalization-localization");
await visit("/t/topic-for-group-moderators/2480");
await click(".toggle-admin-menu");
await click(".topic-admin-status-update button");
await futureDateInputSelector.expand();

View File

@ -255,6 +255,14 @@ export function applyDefaultHandlers(pretender) {
pretender.get("/t/12.json", () => response(fixturesByUrl["/t/12/1.json"]));
pretender.put("/t/1234/re-pin", success);
pretender.get("/t/2480.json", () => {
const json = fixturesByUrl["/t/34/1.json"];
json.details.can_archive_topic = true;
json.details.can_close_topic = true;
return response(json);
});
pretender.get("/t/id_for/:slug", () => {
return response({
id: 280,