FEATURE: custom flag can require additional message (#27908)

Allow admin to create custom flag which requires an additional message.

I decided to rename the old `custom_flag` into `require_message` as it is more descriptive.
This commit is contained in:
Krzysztof Kotlarek 2024-07-18 10:10:22 +10:00 committed by GitHub
parent 58b7dde599
commit c975c7fe1b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 240 additions and 100 deletions

View File

@ -1,14 +1,15 @@
import Component from "@glimmer/component"; import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking"; import { tracked } from "@glimmer/tracking";
import { Input } from "@ember/component"; import { fn, hash } from "@ember/helper";
import { hash } from "@ember/helper";
import { TextArea } from "@ember/legacy-built-in-components"; import { TextArea } from "@ember/legacy-built-in-components";
import { on } from "@ember/modifier";
import { action } from "@ember/object"; import { action } from "@ember/object";
import { LinkTo } from "@ember/routing"; import { LinkTo } from "@ember/routing";
import { service } from "@ember/service"; import { service } from "@ember/service";
import { isEmpty } from "@ember/utils"; import { isEmpty } from "@ember/utils";
import { not } from "truth-helpers"; import { not } from "truth-helpers";
import DButton from "discourse/components/d-button"; import DButton from "discourse/components/d-button";
import withEventValue from "discourse/helpers/with-event-value";
import { ajax } from "discourse/lib/ajax"; import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error"; import { popupAjaxError } from "discourse/lib/ajax-error";
import dIcon from "discourse-common/helpers/d-icon"; import dIcon from "discourse-common/helpers/d-icon";
@ -23,6 +24,7 @@ export default class AdminFlagsForm extends Component {
@service site; @service site;
@tracked enabled = true; @tracked enabled = true;
@tracked requireMessage = false;
@tracked name; @tracked name;
@tracked description; @tracked description;
@tracked appliesTo; @tracked appliesTo;
@ -33,6 +35,7 @@ export default class AdminFlagsForm extends Component {
this.name = this.args.flag.name; this.name = this.args.flag.name;
this.description = this.args.flag.description; this.description = this.args.flag.description;
this.appliesTo = this.args.flag.applies_to; this.appliesTo = this.args.flag.applies_to;
this.requireMessage = this.args.flag.require_message;
this.enabled = this.args.flag.enabled; this.enabled = this.args.flag.enabled;
} }
} }
@ -73,6 +76,16 @@ export default class AdminFlagsForm extends Component {
this.isUpdate ? this.update() : this.create(); this.isUpdate ? this.update() : this.create();
} }
@action
onToggleRequireMessage(e) {
this.requireMessage = e.target.checked;
}
@action
onToggleEnabled(e) {
this.enabled = e.target.checked;
}
@bind @bind
create() { create() {
return ajax(`/admin/config/flags`, { return ajax(`/admin/config/flags`, {
@ -98,6 +111,7 @@ export default class AdminFlagsForm extends Component {
this.args.flag.name = response.flag.name; this.args.flag.name = response.flag.name;
this.args.flag.description = response.flag.description; this.args.flag.description = response.flag.description;
this.args.flag.applies_to = response.flag.applies_to; this.args.flag.applies_to = response.flag.applies_to;
this.args.flag.require_message = response.flag.require_message;
this.args.flag.enabled = response.flag.enabled; this.args.flag.enabled = response.flag.enabled;
this.router.transitionTo("adminConfig.flags"); this.router.transitionTo("adminConfig.flags");
}) })
@ -112,6 +126,7 @@ export default class AdminFlagsForm extends Component {
name: this.name, name: this.name,
description: this.description, description: this.description,
applies_to: this.appliesTo, applies_to: this.appliesTo,
require_message: this.requireMessage,
enabled: this.enabled, enabled: this.enabled,
}; };
} }
@ -132,12 +147,13 @@ export default class AdminFlagsForm extends Component {
<label for="name"> <label for="name">
{{i18n "admin.config_areas.flags.form.name"}} {{i18n "admin.config_areas.flags.form.name"}}
</label> </label>
<Input <input
name="name" name="name"
@type="text" type="text"
@value={{this.name}} value={{this.name}}
maxlength="200" maxlength="200"
class="admin-flag-form__name" class="admin-flag-form__name"
{{on "input" (withEventValue (fn (mut this.name)))}}
/> />
</div> </div>
@ -164,9 +180,31 @@ export default class AdminFlagsForm extends Component {
/> />
</div> </div>
<div class="control-group">
<label class="checkbox-label admin-flag-form__require-reason">
<input
{{on "input" this.onToggleRequireMessage}}
type="checkbox"
checked={{this.requireMessage}}
/>
<div>
{{i18n "admin.config_areas.flags.form.require_message"}}
<div class="admin-flag-form__require-message-description">
{{i18n
"admin.config_areas.flags.form.require_message_description"
}}
</div>
</div>
</label>
</div>
<div class="control-group"> <div class="control-group">
<label class="checkbox-label admin-flag-form__enabled"> <label class="checkbox-label admin-flag-form__enabled">
<Input @type="checkbox" @checked={{this.enabled}} /> <input
{{on "input" this.onToggleEnabled}}
type="checkbox"
checked={{this.enabled}}
/>
{{i18n "admin.config_areas.flags.form.enabled"}} {{i18n "admin.config_areas.flags.form.enabled"}}
</label> </label>
</div> </div>

View File

@ -35,7 +35,7 @@ export default Component.extend({
return flag === selectedFlag; return flag === selectedFlag;
}, },
showMessageInput: and("flag.is_custom_flag", "selected"), showMessageInput: and("flag.require_message", "selected"),
showConfirmation: and("flag.isIllegal", "selected"), showConfirmation: and("flag.isIllegal", "selected"),
showDescription: not("showMessageInput"), showDescription: not("showMessageInput"),
isNotifyUser: equal("flag.name_key", "notify_user"), isNotifyUser: equal("flag.name_key", "notify_user"),

View File

@ -73,7 +73,7 @@ export default class Flag extends Component {
} }
get submitLabel() { get submitLabel() {
if (this.selected?.is_custom_flag) { if (this.selected?.require_message) {
return this.args.model.flagTarget.customSubmitLabel(); return this.args.model.flagTarget.customSubmitLabel();
} }
@ -97,7 +97,7 @@ export default class Flag extends Component {
return false; return false;
} }
if (!this.selected.is_custom_flag) { if (!this.selected.require_message) {
return true; return true;
} }
@ -119,7 +119,7 @@ export default class Flag extends Component {
get canTakeAction() { get canTakeAction() {
return ( return (
!this.args.model.flagTarget.targetsTopic() && !this.args.model.flagTarget.targetsTopic() &&
!this.selected?.is_custom_flag && !this.selected?.require_message &&
this.currentUser.staff this.currentUser.staff
); );
} }
@ -184,7 +184,7 @@ export default class Flag extends Component {
@action @action
createFlag(opts = {}) { createFlag(opts = {}) {
if (this.selected.is_custom_flag) { if (this.selected.require_message) {
opts.message = this.message; opts.message = this.message;
} }
this.args.model.flagTarget.create(this, opts); this.args.model.flagTarget.create(this, opts);

View File

@ -1,9 +1,8 @@
import { equal, not } from "@ember/object/computed"; import { equal } from "@ember/object/computed";
import RestModel from "discourse/models/rest"; import RestModel from "discourse/models/rest";
export const MAX_MESSAGE_LENGTH = 500; export const MAX_MESSAGE_LENGTH = 500;
export default class PostActionType extends RestModel { export default class PostActionType extends RestModel {
@not("is_custom_flag") notCustomFlag;
@equal("name_key", "illegal") isIllegal; @equal("name_key", "illegal") isIllegal;
} }

View File

@ -568,7 +568,7 @@ export default {
is_flag: false, is_flag: false,
icon: null, icon: null,
id: 1, id: 1,
is_custom_flag: false, require_message: false,
}, },
{ {
name_key: "like", name_key: "like",
@ -578,7 +578,7 @@ export default {
is_flag: false, is_flag: false,
icon: "heart", icon: "heart",
id: 2, id: 2,
is_custom_flag: false, require_message: false,
}, },
{ {
name_key: "off_topic", name_key: "off_topic",
@ -589,7 +589,7 @@ export default {
is_flag: true, is_flag: true,
icon: null, icon: null,
id: 3, id: 3,
is_custom_flag: false, require_message: false,
enabled: true, enabled: true,
applies_to: ["Post", "Chat::Message"] applies_to: ["Post", "Chat::Message"]
}, },
@ -603,7 +603,7 @@ export default {
is_flag: true, is_flag: true,
icon: null, icon: null,
id: 4, id: 4,
is_custom_flag: false, require_message: false,
enabled: true, enabled: true,
applies_to: ["Post", "Topic", "Chat::Message"] applies_to: ["Post", "Topic", "Chat::Message"]
}, },
@ -615,7 +615,7 @@ export default {
is_flag: false, is_flag: false,
icon: null, icon: null,
id: 5, id: 5,
is_custom_flag: false, require_message: false,
enabled: true enabled: true
}, },
{ {
@ -627,7 +627,7 @@ export default {
is_flag: true, is_flag: true,
icon: null, icon: null,
id: 8, id: 8,
is_custom_flag: false, require_message: false,
enabled: true, enabled: true,
applies_to: ["Post", "Topic", "Chat::Message"] applies_to: ["Post", "Topic", "Chat::Message"]
}, },
@ -641,7 +641,7 @@ export default {
is_flag: true, is_flag: true,
icon: null, icon: null,
id: 6, id: 6,
is_custom_flag: true, require_message: true,
enabled: true, enabled: true,
applies_to: ["Post", "Topic", "Chat::Message"] applies_to: ["Post", "Topic", "Chat::Message"]
}, },
@ -654,7 +654,7 @@ export default {
is_flag: true, is_flag: true,
icon: null, icon: null,
id: 7, id: 7,
is_custom_flag: true, require_message: true,
enabled: true, enabled: true,
applies_to: ["Post", "Topic", "Chat::Message"] applies_to: ["Post", "Topic", "Chat::Message"]
}, },
@ -668,7 +668,7 @@ export default {
is_flag: true, is_flag: true,
icon: null, icon: null,
id: 4, id: 4,
is_custom_flag: false, require_message: false,
enabled: true, enabled: true,
applies_to: ["Post", "Topic", "Chat::Message"] applies_to: ["Post", "Topic", "Chat::Message"]
}, },
@ -680,7 +680,7 @@ export default {
is_flag: true, is_flag: true,
icon: null, icon: null,
id: 8, id: 8,
is_custom_flag: false, require_message: false,
enabled: true, enabled: true,
applies_to: ["Post", "Topic", "Chat::Message"] applies_to: ["Post", "Topic", "Chat::Message"]
}, },
@ -692,7 +692,7 @@ export default {
is_flag: true, is_flag: true,
icon: null, icon: null,
id: 7, id: 7,
is_custom_flag: true, require_message: true,
enabled: true, enabled: true,
applies_to: ["Post", "Topic", "Chat::Message"] applies_to: ["Post", "Topic", "Chat::Message"]
}, },

View File

@ -278,7 +278,7 @@ PreloadStore.store("site", {
is_flag: false, is_flag: false,
icon: null, icon: null,
id: 1, id: 1,
is_custom_flag: false, require_message: false,
}, },
{ {
name_key: "like", name_key: "like",
@ -288,7 +288,7 @@ PreloadStore.store("site", {
is_flag: false, is_flag: false,
icon: "heart", icon: "heart",
id: 2, id: 2,
is_custom_flag: false, require_message: false,
}, },
{ {
name_key: "off_topic", name_key: "off_topic",
@ -299,7 +299,7 @@ PreloadStore.store("site", {
is_flag: true, is_flag: true,
icon: null, icon: null,
id: 3, id: 3,
is_custom_flag: false, require_message: false,
}, },
{ {
name_key: "inappropriate", name_key: "inappropriate",
@ -310,7 +310,7 @@ PreloadStore.store("site", {
is_flag: true, is_flag: true,
icon: null, icon: null,
id: 4, id: 4,
is_custom_flag: false, require_message: false,
}, },
{ {
name_key: "vote", name_key: "vote",
@ -320,7 +320,7 @@ PreloadStore.store("site", {
is_flag: false, is_flag: false,
icon: null, icon: null,
id: 5, id: 5,
is_custom_flag: false, require_message: false,
}, },
{ {
name_key: "spam", name_key: "spam",
@ -331,7 +331,7 @@ PreloadStore.store("site", {
is_flag: true, is_flag: true,
icon: null, icon: null,
id: 8, id: 8,
is_custom_flag: false, require_message: false,
}, },
{ {
name_key: "notify_user", name_key: "notify_user",
@ -342,7 +342,7 @@ PreloadStore.store("site", {
is_flag: true, is_flag: true,
icon: null, icon: null,
id: 6, id: 6,
is_custom_flag: true, require_message: true,
}, },
{ {
name_key: "notify_moderators", name_key: "notify_moderators",
@ -353,7 +353,7 @@ PreloadStore.store("site", {
is_flag: true, is_flag: true,
icon: null, icon: null,
id: 7, id: 7,
is_custom_flag: true, require_message: true,
}, },
], ],
archetypes: [{ id: "regular", name: "Regular Topic", options: [] }], archetypes: [{ id: "regular", name: "Regular Topic", options: [] }],

View File

@ -56,6 +56,15 @@
&__applies-to.select-kit.multi-select { &__applies-to.select-kit.multi-select {
width: 60%; width: 60%;
} }
&__require-message-description {
clear: both;
flex-basis: 100%;
font-size: var(--font-down-2);
margin-top: 0.25em;
}
label {
flex-wrap: wrap;
}
} }
.admin-flags__header { .admin-flags__header {

View File

@ -76,7 +76,7 @@ module Reports::ModeratorsActivity
disagreed_by_id, disagreed_by_id,
deferred_by_id deferred_by_id
FROM post_actions FROM post_actions
WHERE post_action_type_id IN (#{PostActionType.flag_types_without_custom.values.join(",")}) WHERE post_action_type_id IN (#{PostActionType.flag_types_without_additional_message.values.join(",")})
AND created_at >= '#{report.start_date}' AND created_at >= '#{report.start_date}'
AND created_at <= '#{report.end_date}' AND created_at <= '#{report.end_date}'
), ),

View File

@ -1,6 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
class Flag < ActiveRecord::Base class Flag < ActiveRecord::Base
# TODO(2025-01-15): krisk remove
self.ignored_columns = ["custom_type"]
DEFAULT_VALID_APPLIES_TO = %w[Post Topic] DEFAULT_VALID_APPLIES_TO = %w[Post Topic]
MAX_SYSTEM_FLAG_ID = 1000 MAX_SYSTEM_FLAG_ID = 1000
MAX_NAME_LENGTH = 200 MAX_NAME_LENGTH = 200
@ -62,7 +65,7 @@ end
# description :text # description :text
# notify_type :boolean default(FALSE), not null # notify_type :boolean default(FALSE), not null
# auto_action_type :boolean default(FALSE), not null # auto_action_type :boolean default(FALSE), not null
# custom_type :boolean default(FALSE), not null # require_message :boolean default(FALSE), not null
# applies_to :string not null, is an Array # applies_to :string not null, is an Array
# position :integer not null # position :integer not null
# enabled :boolean default(TRUE), not null # enabled :boolean default(TRUE), not null

View File

@ -556,7 +556,7 @@ class Post < ActiveRecord::Base
def flags def flags
post_actions.where( post_actions.where(
post_action_type_id: PostActionType.flag_types_without_custom.values, post_action_type_id: PostActionType.flag_types_without_additional_message.values,
deleted_at: nil, deleted_at: nil,
) )
end end

View File

@ -59,9 +59,9 @@ class PostActionType < ActiveRecord::Base
@public_type_ids ||= public_types.values @public_type_ids ||= public_types.values
end end
def flag_types_without_custom def flag_types_without_additional_message
return flag_settings.without_custom_types if overridden_by_plugin_or_skipped_db? return flag_settings.without_additional_message_types if overridden_by_plugin_or_skipped_db?
flag_enum(all_flags.reject(&:custom_type)) flag_enum(all_flags.reject(&:require_message))
end end
def flag_types def flag_types
@ -106,9 +106,9 @@ class PostActionType < ActiveRecord::Base
flag_enum(all_flags.filter(&:enabled)) flag_enum(all_flags.filter(&:enabled))
end end
def custom_types def additional_message_types
return flag_settings.custom_types if overridden_by_plugin_or_skipped_db? return flag_settings.with_additional_message if overridden_by_plugin_or_skipped_db?
flag_enum(all_flags.select(&:custom_type)) flag_enum(all_flags.select(&:require_message))
end end
def names def names

View File

@ -1225,7 +1225,7 @@ class User < ActiveRecord::Base
def flags_given_count def flags_given_count
PostAction.where( PostAction.where(
user_id: id, user_id: id,
post_action_type_id: PostActionType.flag_types_without_custom.values, post_action_type_id: PostActionType.flag_types_without_additional_message.values,
).count ).count
end end
@ -1236,7 +1236,10 @@ class User < ActiveRecord::Base
def flags_received_count def flags_received_count
posts posts
.includes(:post_actions) .includes(:post_actions)
.where("post_actions.post_action_type_id" => PostActionType.flag_types_without_custom.values) .where(
"post_actions.post_action_type_id" =>
PostActionType.flag_types_without_additional_message.values,
)
.count .count
end end

View File

@ -1,5 +1,5 @@
# frozen_string_literal: true # frozen_string_literal: true
class FlagSerializer < ApplicationSerializer class FlagSerializer < ApplicationSerializer
attributes :id, :name, :name_key, :description, :applies_to, :position, :enabled attributes :id, :name, :name_key, :description, :applies_to, :position, :require_message, :enabled
end end

View File

@ -8,7 +8,7 @@ class PostActionTypeSerializer < ApplicationSerializer
:description, :description,
:short_description, :short_description,
:is_flag, :is_flag,
:is_custom_flag, :require_message,
:enabled, :enabled,
:applies_to, :applies_to,
:is_used, :is_used,
@ -16,8 +16,8 @@ class PostActionTypeSerializer < ApplicationSerializer
include ConfigurableUrls include ConfigurableUrls
def is_custom_flag def require_message
!!PostActionType.custom_types[object.id] !!PostActionType.additional_message_types[object.id]
end end
def is_flag def is_flag

View File

@ -15,6 +15,7 @@ class Flags::CreateFlag
class Contract class Contract
attribute :name, :string attribute :name, :string
attribute :description, :string attribute :description, :string
attribute :require_message, :boolean
attribute :enabled, :boolean attribute :enabled, :boolean
attribute :applies_to attribute :applies_to
validates :name, presence: true validates :name, presence: true
@ -26,11 +27,12 @@ class Flags::CreateFlag
private private
def instantiate_flag(name:, description:, applies_to:, enabled:) def instantiate_flag(name:, description:, applies_to:, require_message:, enabled:)
Flag.new( Flag.new(
name: name, name: name,
description: description, description: description,
applies_to: applies_to, applies_to: applies_to,
require_message: require_message,
enabled: enabled, enabled: enabled,
notify_type: true, notify_type: true,
) )
@ -51,6 +53,7 @@ class Flags::CreateFlag
name: flag.name, name: flag.name,
description: flag.description, description: flag.description,
applies_to: flag.applies_to, applies_to: flag.applies_to,
require_message: flag.require_message,
enabled: flag.enabled, enabled: flag.enabled,
}, },
) )

View File

@ -17,6 +17,7 @@ class Flags::UpdateFlag
class Contract class Contract
attribute :name, :string attribute :name, :string
attribute :description, :string attribute :description, :string
attribute :require_message, :boolean
attribute :enabled, :boolean attribute :enabled, :boolean
attribute :applies_to attribute :applies_to
validates :name, presence: true validates :name, presence: true
@ -44,8 +45,14 @@ class Flags::UpdateFlag
guardian.can_edit_flag?(flag) guardian.can_edit_flag?(flag)
end end
def update(flag:, name:, description:, applies_to:, enabled:) def update(flag:, name:, description:, applies_to:, require_message:, enabled:)
flag.update!(name: name, description: description, applies_to: applies_to, enabled: enabled) flag.update!(
name: name,
description: description,
applies_to: applies_to,
require_message: require_message,
enabled: enabled,
)
end end
def log(guardian:, flag:) def log(guardian:, flag:)
@ -55,6 +62,7 @@ class Flags::UpdateFlag
name: flag.name, name: flag.name,
description: flag.description, description: flag.description,
applies_to: flag.applies_to, applies_to: flag.applies_to,
require_message: flag.require_message,
enabled: flag.enabled, enabled: flag.enabled,
}, },
) )

View File

@ -5531,6 +5531,8 @@ en:
non_editable: "You cannot edit this flag because it is a system flag or has already been used in the review system, however you can still disable it." non_editable: "You cannot edit this flag because it is a system flag or has already been used in the review system, however you can still disable it."
delete_flag: "Delete flag" delete_flag: "Delete flag"
non_deletable: "You cannot delete this flag because it is a system flag or has already been used in the review system, however you can still disable it." non_deletable: "You cannot delete this flag because it is a system flag or has already been used in the review system, however you can still disable it."
require_message: "Prompt users to provide additional reasons"
require_message_description: "When this flag is selected, a text field will be displayed for the user to provide additional detail about why they are flagging the content"
more_options: more_options:
title: "More options" title: "More options"
move_up: "Move up" move_up: "Move up"

View File

@ -5,7 +5,7 @@ Flag.seed do |s|
s.name = "notify_user" s.name = "notify_user"
s.notify_type = false s.notify_type = false
s.auto_action_type = false s.auto_action_type = false
s.custom_type = true s.require_message = true
s.applies_to = %w[Post Chat::Message] s.applies_to = %w[Post Chat::Message]
end end
Flag.seed do |s| Flag.seed do |s|
@ -13,7 +13,7 @@ Flag.seed do |s|
s.name = "off_topic" s.name = "off_topic"
s.notify_type = true s.notify_type = true
s.auto_action_type = true s.auto_action_type = true
s.custom_type = false s.require_message = false
s.applies_to = %w[Post Chat::Message] s.applies_to = %w[Post Chat::Message]
end end
Flag.seed do |s| Flag.seed do |s|
@ -21,7 +21,7 @@ Flag.seed do |s|
s.name = "inappropriate" s.name = "inappropriate"
s.notify_type = true s.notify_type = true
s.auto_action_type = true s.auto_action_type = true
s.custom_type = false s.require_message = false
s.applies_to = %w[Post Topic Chat::Message] s.applies_to = %w[Post Topic Chat::Message]
end end
Flag.seed do |s| Flag.seed do |s|
@ -29,7 +29,7 @@ Flag.seed do |s|
s.name = "spam" s.name = "spam"
s.notify_type = true s.notify_type = true
s.auto_action_type = true s.auto_action_type = true
s.custom_type = false s.require_message = false
s.applies_to = %w[Post Topic Chat::Message] s.applies_to = %w[Post Topic Chat::Message]
end end
Flag.seed do |s| Flag.seed do |s|
@ -37,7 +37,7 @@ Flag.seed do |s|
s.name = "illegal" s.name = "illegal"
s.notify_type = true s.notify_type = true
s.auto_action_type = false s.auto_action_type = false
s.custom_type = true s.require_message = true
s.applies_to = %w[Post Topic Chat::Message] s.applies_to = %w[Post Topic Chat::Message]
end end
Flag.seed do |s| Flag.seed do |s|
@ -45,7 +45,7 @@ Flag.seed do |s|
s.name = "notify_moderators" s.name = "notify_moderators"
s.notify_type = true s.notify_type = true
s.auto_action_type = false s.auto_action_type = false
s.custom_type = true s.require_message = true
s.applies_to = %w[Post Topic Chat::Message] s.applies_to = %w[Post Topic Chat::Message]
end end
Flag.unscoped.seed do |s| Flag.unscoped.seed do |s|
@ -53,7 +53,7 @@ Flag.unscoped.seed do |s|
s.name = "needs_approval" s.name = "needs_approval"
s.notify_type = false s.notify_type = false
s.auto_action_type = false s.auto_action_type = false
s.custom_type = false s.require_message = false
s.score_type = true s.score_type = true
s.applies_to = %w[] s.applies_to = %w[]
end end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
class DuplicateFlagsCustomTypeToRequireMessage < ActiveRecord::Migration[7.0]
def up
add_column :flags, :require_message, :boolean, default: false, null: false
Migration::ColumnDropper.mark_readonly("flags", "custom_type")
DB.exec <<~SQL
UPDATE flags
SET require_message = custom_type
SQL
end
def down
Migration::ColumnDropper.drop_readonly("flags", "custom_type")
remove_column :flags, :require_message
end
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
class DropCustomTypeFromFlags < ActiveRecord::Migration[7.0]
DROPPED_COLUMNS ||= { flags: %i[custom_type] }
def up
DROPPED_COLUMNS.each { |table, columns| Migration::ColumnDropper.execute_drop(table, columns) }
end
def down
raise ActiveRecord::IrreversibleMigration
end
end

View File

@ -73,7 +73,7 @@ module BadgeQueries
SELECT pa.user_id, min(pa.id) id SELECT pa.user_id, min(pa.id) id
FROM post_actions pa FROM post_actions pa
JOIN badge_posts p on p.id = pa.post_id JOIN badge_posts p on p.id = pa.post_id
WHERE post_action_type_id IN (#{PostActionType.flag_types_without_custom.values.join(",")}) AND WHERE post_action_type_id IN (#{PostActionType.flag_types_without_additional_message.values.join(",")}) AND
(:backfill OR pa.post_id IN (:post_ids) ) (:backfill OR pa.post_id IN (:post_ids) )
GROUP BY pa.user_id GROUP BY pa.user_id
) x ) x

View File

@ -2,11 +2,11 @@
class FlagSettings class FlagSettings
attr_reader( attr_reader(
:without_custom_types, :without_additional_message_types,
:notify_types, :notify_types,
:topic_flag_types, :topic_flag_types,
:auto_action_types, :auto_action_types,
:custom_types, :additional_message_types,
:names, :names,
) )
@ -15,8 +15,8 @@ class FlagSettings
@topic_flag_types = Enum.new @topic_flag_types = Enum.new
@notify_types = Enum.new @notify_types = Enum.new
@auto_action_types = Enum.new @auto_action_types = Enum.new
@custom_types = Enum.new @additional_message_types = Enum.new
@without_custom_types = Enum.new @without_additional_message_types = Enum.new
@names = Enum.new @names = Enum.new
end end
@ -26,7 +26,7 @@ class FlagSettings
topic_type: nil, topic_type: nil,
notify_type: nil, notify_type: nil,
auto_action_type: nil, auto_action_type: nil,
custom_type: nil, require_message: nil,
name: nil name: nil
) )
@all_flag_types[name_key] = id @all_flag_types[name_key] = id
@ -35,10 +35,10 @@ class FlagSettings
@auto_action_types[name_key] = id if !!auto_action_type @auto_action_types[name_key] = id if !!auto_action_type
@names[id] = name if name @names[id] = name if name
if !!custom_type if !!require_message
@custom_types[name_key] = id @additional_message_types[name_key] = id
else else
@without_custom_types[name_key] = id @without_additional_message_types[name_key] = id
end end
end end

View File

@ -38,7 +38,8 @@ module PostGuardian
taken = opts[:taken_actions].try(:keys).to_a taken = opts[:taken_actions].try(:keys).to_a
is_flag = is_flag =
PostActionType.notify_flag_types[action_key] || PostActionType.custom_types[action_key] PostActionType.notify_flag_types[action_key] ||
PostActionType.additional_message_types[action_key]
already_taken_this_action = taken.any? && taken.include?(PostActionType.types[action_key]) already_taken_this_action = taken.any? && taken.include?(PostActionType.types[action_key])
already_did_flagging = taken.any? && (taken & PostActionType.notify_flag_types.values).any? already_did_flagging = taken.any? && (taken & PostActionType.notify_flag_types.values).any?

View File

@ -115,7 +115,9 @@ class PostActionCreator
# create meta topic / post if needed # create meta topic / post if needed
if @message.present? && if @message.present? &&
%i[notify_moderators notify_user spam illegal].include?(@post_action_name) (PostActionType.additional_message_types.keys | %i[spam illegal]).include?(
@post_action_name,
)
creator = create_message_creator creator = create_message_creator
# We need to check if the creator exists because it's possible `create_message_creator` returns nil # We need to check if the creator exists because it's possible `create_message_creator` returns nil
# in the event that a `post_action_notify_user_handler` evaluated to false, haulting the post creation. # in the event that a `post_action_notify_user_handler` evaluated to false, haulting the post creation.
@ -325,6 +327,7 @@ class PostActionCreator
"post_action_types.#{@post_action_name}.email_title", "post_action_types.#{@post_action_name}.email_title",
title: @post.topic.title, title: @post.topic.title,
locale: SiteSetting.default_locale, locale: SiteSetting.default_locale,
default: I18n.t("post_action_types.illegal.email_title"),
) )
body = body =
@ -333,6 +336,7 @@ class PostActionCreator
message: @message, message: @message,
link: "#{Discourse.base_url}#{@post.url}", link: "#{Discourse.base_url}#{@post.url}",
locale: SiteSetting.default_locale, locale: SiteSetting.default_locale,
default: I18n.t("post_action_types.illegal.email_body"),
) )
create_args = { create_args = {
@ -342,18 +346,9 @@ class PostActionCreator
raw: body, raw: body,
} }
if %i[notify_moderators spam illegal].include?(@post_action_name) if @post_action_name == :notify_user
create_args[:subtype] = TopicSubtype.notify_moderators
create_args[:target_group_names] = [Group[:moderators].name]
if SiteSetting.enable_category_group_moderation? &&
@post.topic&.category&.reviewable_by_group_id?
create_args[:target_group_names] << @post.topic.category.reviewable_by_group.name
end
else
create_args[:subtype] = TopicSubtype.notify_user create_args[:subtype] = TopicSubtype.notify_user
if @post_action_name == :notify_user
create_args[:target_usernames] = @post.user.username create_args[:target_usernames] = @post.user.username
# Evaluate DiscoursePluginRegistry.post_action_notify_user_handlers. # Evaluate DiscoursePluginRegistry.post_action_notify_user_handlers.
@ -363,10 +358,13 @@ class PostActionCreator
handler.call(@created_by, @post, @message) handler.call(@created_by, @post, @message)
end end
return if handler_values.any? { |value| value == false } return if handler_values.any? { |value| value == false }
elsif @post_action_name != :notify_moderators else
# this is a hack to allow a PM with no recipients, we should think through create_args[:subtype] = TopicSubtype.notify_moderators
# a cleaner technique, a PM with myself is valid for flagging create_args[:target_group_names] = [Group[:moderators].name]
"x"
if SiteSetting.enable_category_group_moderation? &&
@post.topic&.category&.reviewable_by_group_id?
create_args[:target_group_names] << @post.topic.category.reviewable_by_group.name
end end
end end

View File

@ -542,7 +542,7 @@ class PostRevisor
flaggers = [] flaggers = []
@post @post
.post_actions .post_actions
.where(post_action_type_id: PostActionType.flag_types_without_custom.values) .where(post_action_type_id: PostActionType.flag_types_without_additional_message.values)
.each do |action| .each do |action|
flaggers << action.user if action.user flaggers << action.user if action.user
action.remove_act!(Discourse.system_user) action.remove_act!(Discourse.system_user)

View File

@ -21,7 +21,7 @@ RSpec.describe FlagSettings do
settings.add(4, :inappropriate, topic_type: true) settings.add(4, :inappropriate, topic_type: true)
expect(settings.flag_types).to include(:inappropriate) expect(settings.flag_types).to include(:inappropriate)
expect(settings.topic_flag_types).to include(:inappropriate) expect(settings.topic_flag_types).to include(:inappropriate)
expect(settings.without_custom_types).to include(:inappropriate) expect(settings.without_additional_message_types).to include(:inappropriate)
end end
it "will add a notify type" do it "will add a notify type" do
@ -36,11 +36,11 @@ RSpec.describe FlagSettings do
expect(settings.auto_action_types).to include(:notify_moderators) expect(settings.auto_action_types).to include(:notify_moderators)
end end
it "will add a custom type" do it "will add a type which requires message" do
settings.add(7, :notify_user, custom_type: true) settings.add(7, :notify_user, require_message: true)
expect(settings.flag_types).to include(:notify_user) expect(settings.flag_types).to include(:notify_user)
expect(settings.custom_types).to include(:notify_user) expect(settings.additional_message_types).to include(:notify_user)
expect(settings.without_custom_types).to be_empty expect(settings.without_additional_message_types).to be_empty
end end
end end
end end

View File

@ -767,7 +767,7 @@ RSpec.describe PostAction do
# flags are already being tested # flags are already being tested
all_types_except_flags = all_types_except_flags =
PostActionType.types.except(*PostActionType.flag_types_without_custom.keys) PostActionType.types.except(*PostActionType.flag_types_without_additional_message.keys)
all_types_except_flags.values.each do |action| all_types_except_flags.values.each do |action|
it "prevents user to act twice at the same time" do it "prevents user to act twice at the same time" do
expect(PostActionCreator.new(eviltrout, post, action).perform).to be_success expect(PostActionCreator.new(eviltrout, post, action).perform).to be_success
@ -783,6 +783,25 @@ RSpec.describe PostAction do
expect(result.reviewable_score.meta_topic_id).to be_nil expect(result.reviewable_score.meta_topic_id).to be_nil
end end
it "does not create a message for custom flag when message is not required" do
flag_without_message =
Fabricate(:flag, name: "flag without message", notify_type: true, require_message: false)
result =
PostActionCreator.new(
Discourse.system_user,
post,
PostActionType.types[:flag_without_message],
message: "WAT",
).perform
expect(result).to be_success
expect(result.post_action.related_post_id).to be_nil
expect(result.reviewable_score.meta_topic_id).to be_nil
flag_without_message.destroy!
end
%i[notify_moderators notify_user spam].each do |post_action_type| %i[notify_moderators notify_user spam].each do |post_action_type|
it "creates a message for #{post_action_type}" do it "creates a message for #{post_action_type}" do
result = result =
@ -797,6 +816,25 @@ RSpec.describe PostAction do
end end
end end
it "creates a message for custom flags when message is required" do
flag_with_message =
Fabricate(:flag, name: "flag with message", notify_type: true, require_message: true)
result =
PostActionCreator.new(
Discourse.system_user,
post,
PostActionType.types[:flag_with_message],
message: "WAT",
).perform
expect(result).to be_success
expect(result.post_action.related_post_id).to be_present
expect(result.reviewable_score.meta_topic_id).to be_present
flag_with_message.destroy!
end
it "should raise the right errors when it fails to create a post" do it "should raise the right errors when it fails to create a post" do
user = Fabricate(:user) user = Fabricate(:user)
UserSilencer.new(user, Discourse.system_user).silence UserSilencer.new(user, Discourse.system_user).silence

View File

@ -351,7 +351,7 @@
"is_flag": { "is_flag": {
"type": "boolean" "type": "boolean"
}, },
"is_custom_flag": { "require_message": {
"type": "boolean" "type": "boolean"
}, },
"enabled": { "enabled": {
@ -371,7 +371,7 @@
"description", "description",
"short_description", "short_description",
"is_flag", "is_flag",
"is_custom_flag", "require_message",
"enabled", "enabled",
"applies_to", "applies_to",
"is_used" "is_used"
@ -403,7 +403,7 @@
"is_flag": { "is_flag": {
"type": "boolean" "type": "boolean"
}, },
"is_custom_flag": { "require_message": {
"type": "boolean" "type": "boolean"
}, },
"enabled": { "enabled": {
@ -423,7 +423,7 @@
"description", "description",
"short_description", "short_description",
"is_flag", "is_flag",
"is_custom_flag", "require_message",
"enabled", "enabled",
"applies_to", "applies_to",
"is_used" "is_used"

View File

@ -7,6 +7,7 @@ RSpec.describe(Flags::CreateFlag) do
name: name, name: name,
description: description, description: description,
applies_to: applies_to, applies_to: applies_to,
require_message: require_message,
enabled: enabled, enabled: enabled,
) )
end end
@ -15,6 +16,7 @@ RSpec.describe(Flags::CreateFlag) do
let(:description) { "custom flag description" } let(:description) { "custom flag description" }
let(:applies_to) { ["Topic"] } let(:applies_to) { ["Topic"] }
let(:enabled) { true } let(:enabled) { true }
let(:require_message) { true }
context "when user is not allowed to perform the action" do context "when user is not allowed to perform the action" do
fab!(:current_user) { Fabricate(:user) } fab!(:current_user) { Fabricate(:user) }
@ -72,6 +74,7 @@ RSpec.describe(Flags::CreateFlag) do
expect(flag.name).to eq("custom flag name") expect(flag.name).to eq("custom flag name")
expect(flag.description).to eq("custom flag description") expect(flag.description).to eq("custom flag description")
expect(flag.applies_to).to eq(["Topic"]) expect(flag.applies_to).to eq(["Topic"])
expect(flag.require_message).to be true
expect(flag.enabled).to be true expect(flag.enabled).to be true
end end
@ -80,7 +83,7 @@ RSpec.describe(Flags::CreateFlag) do
expect(UserHistory.last).to have_attributes( expect(UserHistory.last).to have_attributes(
custom_type: "create_flag", custom_type: "create_flag",
details: details:
"name: custom flag name\ndescription: custom flag description\napplies_to: [\"Topic\"]\nenabled: true", "name: custom flag name\ndescription: custom flag description\napplies_to: [\"Topic\"]\nrequire_message: true\nenabled: true",
) )
end end
end end

View File

@ -10,6 +10,7 @@ RSpec.describe(Flags::UpdateFlag) do
name: name, name: name,
description: description, description: description,
applies_to: applies_to, applies_to: applies_to,
require_message: require_message,
enabled: enabled, enabled: enabled,
) )
end end
@ -19,6 +20,7 @@ RSpec.describe(Flags::UpdateFlag) do
let(:name) { "edited custom flag name" } let(:name) { "edited custom flag name" }
let(:description) { "edited custom flag description" } let(:description) { "edited custom flag description" }
let(:applies_to) { ["Topic"] } let(:applies_to) { ["Topic"] }
let(:require_message) { true }
let(:enabled) { false } let(:enabled) { false }
context "when user is not allowed to perform the action" do context "when user is not allowed to perform the action" do
@ -74,6 +76,7 @@ RSpec.describe(Flags::UpdateFlag) do
expect(flag.reload.name).to eq("edited custom flag name") expect(flag.reload.name).to eq("edited custom flag name")
expect(flag.description).to eq("edited custom flag description") expect(flag.description).to eq("edited custom flag description")
expect(flag.applies_to).to eq(["Topic"]) expect(flag.applies_to).to eq(["Topic"])
expect(flag.require_message).to be true
expect(flag.enabled).to be false expect(flag.enabled).to be false
end end
@ -82,7 +85,7 @@ RSpec.describe(Flags::UpdateFlag) do
expect(UserHistory.last).to have_attributes( expect(UserHistory.last).to have_attributes(
custom_type: "update_flag", custom_type: "update_flag",
details: details:
"name: edited custom flag name\ndescription: edited custom flag description\napplies_to: [\"Topic\"]\nenabled: false", "name: edited custom flag name\ndescription: edited custom flag description\napplies_to: [\"Topic\"]\nrequire_message: true\nenabled: false",
) )
end end
end end