Add a way to flag a topic

This commit is contained in:
Neil Lalonde 2014-02-05 17:54:16 -05:00
parent 11ee4e7328
commit 6bbc3ec3e0
23 changed files with 251 additions and 26 deletions

View file

@ -42,6 +42,10 @@ Discourse.AdminFlagsController = Ember.ArrayController.extend({
}); });
}, },
doneTopicFlags: function(item) {
this.send('disagreeFlags', item);
},
/** /**
Deletes a post Deletes a post

View file

@ -62,6 +62,14 @@ Discourse.FlaggedPost = Discourse.Post.extend({
return !_.every(this.get('post_actions'), function(action) { return action.name_key !== 'spam'; }); return !_.every(this.get('post_actions'), function(action) { return action.name_key !== 'spam'; });
}.property('post_actions.@each.name_key'), }.property('post_actions.@each.name_key'),
topicFlagged: function() {
return _.any(this.get('post_actions'), function(action) { return action.targets_topic; });
}.property('post_actions.@each.targets_topic'),
postAuthorFlagged: function() {
return _.any(this.get('post_actions'), function(action) { return !action.targets_topic; });
}.property('post_actions.@each.targets_topic'),
canDeleteAsSpammer: function() { canDeleteAsSpammer: function() {
return (Discourse.User.currentProp('staff') && this.get('flaggedForSpam') && this.get('user.can_delete_all_posts') && this.get('user.can_be_deleted')); return (Discourse.User.currentProp('staff') && this.get('flaggedForSpam') && this.get('user.can_delete_all_posts') && this.get('user.can_be_deleted'));
}.property('flaggedForSpam'), }.property('flaggedForSpam'),

View file

@ -26,9 +26,20 @@
{{#each flaggedPost in content}} {{#each flaggedPost in content}}
<tr {{bind-attr class="flaggedPost.extraClasses"}}> <tr {{bind-attr class="flaggedPost.extraClasses"}}>
<td class='user'>{{#if flaggedPost.user}}{{#link-to 'adminUser' flaggedPost.user}}{{avatar flaggedPost.user imageSize="small"}}{{/link-to}}{{/if}}</td> <td class='user'>
{{#if flaggedPost.postAuthorFlagged}}
{{#if flaggedPost.user}}
{{#link-to 'adminUser' flaggedPost.user}}{{avatar flaggedPost.user imageSize="small"}}{{/link-to}}
{{/if}}
{{/if}}
</td>
<td class='excerpt'>{{#if flaggedPost.topicHidden}}<i title='{{i18n topic_statuses.invisible.help}}' class='fa fa-eye-slash'></i> {{/if}}<h3><a href='{{unbound flaggedPost.url}}'>{{flaggedPost.title}}</a></h3><br>{{{flaggedPost.excerpt}}} <td class='excerpt'>
{{#if flaggedPost.topicHidden}}<i title='{{i18n topic_statuses.invisible.help}}' class='fa fa-eye-slash'></i> {{/if}}<h3><a href='{{unbound flaggedPost.url}}'>{{flaggedPost.title}}</a></h3>
<br>
{{#if flaggedPost.postAuthorFlagged}}
{{{flaggedPost.excerpt}}}
{{/if}}
</td> </td>
<td class='flaggers'> <td class='flaggers'>
@ -51,6 +62,15 @@
</tr> </tr>
{{#if flaggedPost.topicFlagged}}
<tr>
<td></td>
<td class='message'><div>{{{i18n admin.flags.topic_flagged}}}</div></td>
<td></td>
<td></td>
</tr>
{{/if}}
{{#each flaggedPost.messages}} {{#each flaggedPost.messages}}
<tr> <tr>
<td></td> <td></td>
@ -74,6 +94,11 @@
<tr> <tr>
<td colspan="4" class="action"> <td colspan="4" class="action">
{{#if adminActiveFlagsView}} {{#if adminActiveFlagsView}}
{{#if flaggedPost.topicFlagged}}
<a href='{{unbound flaggedPost.url}}'>{{i18n admin.flags.visit_topic}}</a><br>
{{/if}}
{{#if flaggedPost.postAuthorFlagged}}
{{#if flaggedPost.postHidden}} {{#if flaggedPost.postHidden}}
<button title='{{i18n admin.flags.disagree_unhide_title}}' class='btn' {{action disagreeFlags flaggedPost}}><i class="fa fa-thumbs-o-down"></i> {{i18n admin.flags.disagree_unhide}}</button> <button title='{{i18n admin.flags.disagree_unhide_title}}' class='btn' {{action disagreeFlags flaggedPost}}><i class="fa fa-thumbs-o-down"></i> {{i18n admin.flags.disagree_unhide}}</button>
<button title='{{i18n admin.flags.defer_title}}' class='btn' {{action deferFlags flaggedPost}}><i class="fa fa-external-link"></i> {{i18n admin.flags.defer}}</button> <button title='{{i18n admin.flags.defer_title}}' class='btn' {{action deferFlags flaggedPost}}><i class="fa fa-external-link"></i> {{i18n admin.flags.defer}}</button>
@ -87,6 +112,9 @@
{{/if}} {{/if}}
<button title='{{i18n admin.flags.delete_post_title}}' class='btn' {{action deletePost flaggedPost}}><i class="fa fa-trash-o"></i> {{i18n admin.flags.delete_post}}</button> <button title='{{i18n admin.flags.delete_post_title}}' class='btn' {{action deletePost flaggedPost}}><i class="fa fa-trash-o"></i> {{i18n admin.flags.delete_post}}</button>
{{else}}
<button title='{{i18n admin.flags.clear_topic_flags_title}}' class='btn' {{action doneTopicFlags flaggedPost}}>{{i18n admin.flags.clear_topic_flags}}</button>
{{/if}}
{{/if}} {{/if}}
</td> </td>
</tr> </tr>

View file

@ -13,6 +13,30 @@ Discourse.FlagController = Discourse.ObjectController.extend(Discourse.ModalFunc
this.set('selected', null); this.set('selected', null);
}, },
flagsAvailable: function() {
if (!this.get('flagTopic')) {
return this.get('model.flagsAvailable');
} else {
var self = this,
lookup = Em.Object.create();
_.each(this.get("actions_summary"),function(a) {
var actionSummary;
a.flagTopic = self.get('model');
a.actionType = Discourse.Site.current().topicFlagTypeById(a.id);
actionSummary = Discourse.ActionSummary.create(a);
lookup.set(a.actionType.get('name_key'), actionSummary);
});
this.set('topicActionByName', lookup);
return Discourse.Site.currentProp('topic_flag_types').filter(function(item) {
return _.any(self.get("actions_summary"), function(a) {
return (a.id === item.get('id') && a.can_act);
});
});
}
}.property('post', 'flagTopic', 'actions_summary.@each.can_act'),
submitEnabled: function() { submitEnabled: function() {
var selected = this.get('selected'); var selected = this.get('selected');
if (!selected) return false; if (!selected) return false;
@ -29,6 +53,8 @@ Discourse.FlagController = Discourse.ObjectController.extend(Discourse.ModalFunc
// Staff accounts can "take action" // Staff accounts can "take action"
canTakeAction: function() { canTakeAction: function() {
if (this.get("flagTopic")) return false;
// We can only take actions on non-custom flags // We can only take actions on non-custom flags
if (this.get('selected.is_custom_flag')) return false; if (this.get('selected.is_custom_flag')) return false;
return Discourse.User.currentProp('staff'); return Discourse.User.currentProp('staff');
@ -36,9 +62,9 @@ Discourse.FlagController = Discourse.ObjectController.extend(Discourse.ModalFunc
submitText: function(){ submitText: function(){
if (this.get('selected.is_custom_flag')) { if (this.get('selected.is_custom_flag')) {
return I18n.t("flagging.notify_action"); return I18n.t(this.get('flagTopic') ? "flagging_topic.notify_action" : "flagging.notify_action");
} else { } else {
return I18n.t("flagging.action"); return I18n.t(this.get('flagTopic') ? "flagging_topic.action" : "flagging.action");
} }
}.property('selected.is_custom_flag'), }.property('selected.is_custom_flag'),
@ -50,7 +76,12 @@ Discourse.FlagController = Discourse.ObjectController.extend(Discourse.ModalFunc
createFlag: function(opts) { createFlag: function(opts) {
var self = this; var self = this;
var postAction = this.get('actionByName.' + this.get('selected.name_key')); var postAction; // an instance of ActionSummary
if (!this.get('flagTopic')) {
postAction = this.get('actionByName.' + this.get('selected.name_key'));
} else {
postAction = this.get('topicActionByName.' + this.get('selected.name_key'));
}
var params = this.get('selected.is_custom_flag') ? {message: this.get('message')} : {}; var params = this.get('selected.is_custom_flag') ? {message: this.get('message')} : {};
if (opts) params = $.extend(params, opts); if (opts) params = $.extend(params, opts);
@ -58,6 +89,7 @@ Discourse.FlagController = Discourse.ObjectController.extend(Discourse.ModalFunc
this.send('hideModal'); this.send('hideModal');
postAction.act(params).then(function() { postAction.act(params).then(function() {
self.send('closeModal'); self.send('closeModal');
if (self.get('flagTopic')) { bootbox.alert(I18n.t('topic.flag_topic.success_message')); }
}, function(errors) { }, function(errors) {
self.send('showModal'); self.send('showModal');
self.displayErrors(errors); self.displayErrors(errors);
@ -70,6 +102,8 @@ Discourse.FlagController = Discourse.ObjectController.extend(Discourse.ModalFunc
}, },
canDeleteSpammer: function() { canDeleteSpammer: function() {
if (this.get("flagTopic")) return false;
if (Discourse.User.currentProp('staff') && this.get('selected.name_key') === 'spam') { if (Discourse.User.currentProp('staff') && this.get('selected.name_key') === 'spam') {
return this.get('userDetails.can_be_deleted') && this.get('userDetails.can_delete_all_posts'); return this.get('userDetails.can_be_deleted') && this.get('userDetails.can_delete_all_posts');
} else { } else {

View file

@ -70,10 +70,11 @@ Discourse.ActionSummary = Discourse.Model.extend({
return Discourse.ajax("/post_actions", { return Discourse.ajax("/post_actions", {
type: 'POST', type: 'POST',
data: { data: {
id: this.get('post.id'), id: this.get('flagTopic') ? this.get('flagTopic.id') : this.get('post.id'),
post_action_type_id: this.get('id'), post_action_type_id: this.get('id'),
message: opts.message, message: opts.message,
take_action: opts.takeAction take_action: opts.takeAction,
flag_topic: this.get('flagTopic') ? true : false
} }
}).then(null, function (error) { }).then(null, function (error) {
actionSummary.removeAction(); actionSummary.removeAction();

View file

@ -30,6 +30,10 @@ Discourse.Site = Discourse.Model.extend({
return this.get("postActionByIdLookup.action" + id); return this.get("postActionByIdLookup.action" + id);
}, },
topicFlagTypeById: function(id) {
return this.get("topicFlagByIdLookup.action" + id);
},
updateCategory: function(newCategory) { updateCategory: function(newCategory) {
var existingCategory = this.get('categories').findProperty('id', Em.get(newCategory, 'id')); var existingCategory = this.get('categories').findProperty('id', Em.get(newCategory, 'id'));
if (existingCategory) existingCategory.mergeAttributes(newCategory); if (existingCategory) existingCategory.mergeAttributes(newCategory);
@ -82,6 +86,16 @@ Discourse.Site.reopenClass(Discourse.Singleton, {
return actionType; return actionType;
}); });
} }
if (result.topic_flag_types) {
result.topicFlagByIdLookup = Em.Object.create();
result.topic_flag_types = _.map(result.topic_flag_types,function(p) {
var actionType = Discourse.PostActionType.create(p);
result.topicFlagByIdLookup.set("action" + p.id, actionType);
return actionType;
});
}
if (result.archetypes) { if (result.archetypes) {
result.archetypes = _.map(result.archetypes,function(a) { result.archetypes = _.map(result.archetypes,function(a) {
return Discourse.Archetype.create(a); return Discourse.Archetype.create(a);

View file

@ -316,6 +316,26 @@ Discourse.Topic.reopenClass({
MUTE: 0 MUTE: 0
}, },
createActionSummary: function(result) {
if (result.actions_summary) {
var lookup = Em.Object.create();
result.actions_summary = result.actions_summary.map(function(a) {
a.post = result;
a.actionType = Discourse.Site.current().postActionTypeById(a.id);
var actionSummary = Discourse.ActionSummary.create(a);
lookup.set(a.actionType.get('name_key'), actionSummary);
return actionSummary;
});
result.set('actionByName', lookup);
}
},
create: function() {
var result = this._super.apply(this, arguments);
this.createActionSummary(result);
return result;
},
/** /**
Find similar topics to a given title and body Find similar topics to a given title and body

View file

@ -27,6 +27,12 @@ Discourse.TopicRoute = Discourse.Route.extend({
this.controllerFor('flag').setProperties({ selected: null }); this.controllerFor('flag').setProperties({ selected: null });
}, },
showFlagTopic: function(topic) {
//Discourse.Route.showModal(this, 'flagTopic', topic);
Discourse.Route.showModal(this, 'flag', topic);
this.controllerFor('flag').setProperties({ selected: null, flagTopic: true });
},
showAutoClose: function() { showAutoClose: function() {
Discourse.Route.showModal(this, 'editTopicAutoClose', this.modelFor('topic')); Discourse.Route.showModal(this, 'editTopicAutoClose', this.modelFor('topic'));
this.controllerFor('modal').set('modalClass', 'edit-auto-close-modal'); this.controllerFor('modal').set('modalClass', 'edit-auto-close-modal');

View file

@ -0,0 +1,22 @@
/**
A button for flagging a topic
@class FlagTopicButton
@extends Discourse.ButtonView
@namespace Discourse
@module Discourse
**/
Discourse.FlagTopicButton = Discourse.ButtonView.extend({
classNames: ['flag-topic'],
textKey: 'topic.flag_topic.title',
helpKey: 'topic.flag_topic.help',
click: function() {
this.get('controller').send('showFlagTopic', this.get('controller.content'));
},
renderIcon: function(buffer) {
buffer.push("<i class='fa fa-flag'></i>");
}
});

View file

@ -8,7 +8,10 @@
**/ **/
Discourse.FlagView = Discourse.ModalBodyView.extend({ Discourse.FlagView = Discourse.ModalBodyView.extend({
templateName: 'modal/flag', templateName: 'modal/flag',
title: I18n.t('flagging.title'),
title: function() {
return this.get('controller.flagTopic') ? I18n.t('flagging_topic.title') : I18n.t('flagging.title');
}.property('controller.flagTopic'),
selectedChanged: function() { selectedChanged: function() {
var flagView = this; var flagView = this;

View file

@ -20,7 +20,6 @@ Discourse.TopicFooterButtonsView = Discourse.ContainerView.extend({
var topic = this.get('topic'); var topic = this.get('topic');
if (Discourse.User.current()) { if (Discourse.User.current()) {
if (!topic.get('isPrivateMessage')) { if (!topic.get('isPrivateMessage')) {
// We hide some controls from private messages // We hide some controls from private messages
if (this.get('topic.details.can_invite_to')) { if (this.get('topic.details.can_invite_to')) {
this.attachViewClass(Discourse.InviteReplyButton); this.attachViewClass(Discourse.InviteReplyButton);
@ -28,6 +27,9 @@ Discourse.TopicFooterButtonsView = Discourse.ContainerView.extend({
this.attachViewClass(Discourse.StarButton); this.attachViewClass(Discourse.StarButton);
this.attachViewClass(Discourse.ShareButton); this.attachViewClass(Discourse.ShareButton);
this.attachViewClass(Discourse.ClearPinButton); this.attachViewClass(Discourse.ClearPinButton);
if (this.get('topic.details.can_flag_topic')) {
this.attachViewClass(Discourse.FlagTopicButton);
}
} }
this.attachViewClass(Discourse.ReplyButton); this.attachViewClass(Discourse.ReplyButton);
this.attachViewClass(Discourse.NotificationsButton); this.attachViewClass(Discourse.NotificationsButton);

View file

@ -19,6 +19,7 @@
//= require ./discourse/controllers/controller //= require ./discourse/controllers/controller
//= require ./discourse/controllers/object_controller //= require ./discourse/controllers/object_controller
//= require ./discourse/views/modal/modal_body_view //= require ./discourse/views/modal/modal_body_view
//= require ./discourse/views/modal/flag_view
//= require ./discourse/views/combobox_view //= require ./discourse/views/combobox_view
//= require ./discourse/views/buttons/button_view //= require ./discourse/views/buttons/button_view
//= require ./discourse/views/buttons/dropdown_button_view //= require ./discourse/views/buttons/dropdown_button_view

View file

@ -369,11 +369,11 @@ table {
} }
td { vertical-align: top; } td { vertical-align: top; }
th { text-align: left; } th { text-align: left; }
.user { width: 40px; } .user { width: 40px; padding-top: 12px; }
.excerpt { .excerpt {
max-width: 740px; max-width: 740px;
width: 740px; width: 740px;
padding: 0 10px 10px 0; padding: 8px;
word-wrap: break-word; word-wrap: break-word;
.fa,h3 { display: inline-block; } .fa,h3 { display: inline-block; }

View file

@ -12,6 +12,7 @@ class PostActionsController < ApplicationController
args = {} args = {}
args[:message] = params[:message] if params[:message].present? args[:message] = params[:message] if params[:message].present?
args[:take_action] = true if guardian.is_staff? and params[:take_action] == 'true' args[:take_action] = true if guardian.is_staff? and params[:take_action] == 'true'
args[:flag_topic] = true if params[:flag_topic]
post_action = PostAction.act(current_user, @post, @post_action_type_id, args) post_action = PostAction.act(current_user, @post, @post_action_type_id, args)
@ -63,7 +64,18 @@ class PostActionsController < ApplicationController
def fetch_post_from_params def fetch_post_from_params
params.require(:id) params.require(:id)
finder = Post.where(id: params[:id])
post_id = if params[:flag_topic]
begin
Topic.find(params[:id]).posts.first.id
rescue
raise Discourse::NotFound
end
else
params[:id]
end
finder = Post.where(id: post_id)
# Include deleted posts if the user is a moderator (to guardian ?) # Include deleted posts if the user is a moderator (to guardian ?)
finder = finder.with_deleted if current_user.try(:moderator?) finder = finder.with_deleted if current_user.try(:moderator?)

View file

@ -12,6 +12,7 @@ class PostAction < ActiveRecord::Base
belongs_to :user belongs_to :user
belongs_to :post_action_type belongs_to :post_action_type
belongs_to :related_post, class_name: 'Post' belongs_to :related_post, class_name: 'Post'
belongs_to :target_user, class_name: 'User'
rate_limit :post_action_rate_limiter rate_limit :post_action_rate_limiter
@ -128,7 +129,8 @@ class PostAction < ActiveRecord::Base
post_action_type_id: post_action_type_id, post_action_type_id: post_action_type_id,
message: opts[:message], message: opts[:message],
staff_took_action: opts[:take_action] || false, staff_took_action: opts[:take_action] || false,
related_post_id: related_post_id ) related_post_id: related_post_id,
targets_topic: !!opts[:flag_topic] )
rescue ActiveRecord::RecordNotUnique rescue ActiveRecord::RecordNotUnique
# can happen despite being .create # can happen despite being .create
# since already bookmarked # since already bookmarked
@ -319,6 +321,7 @@ end
# staff_took_action :boolean default(FALSE), not null # staff_took_action :boolean default(FALSE), not null
# defer :boolean # defer :boolean
# defer_by :integer # defer_by :integer
# targets_topic :boolean default(FALSE)
# #
# Indexes # Indexes
# #

View file

@ -28,6 +28,10 @@ class PostActionType < ActiveRecord::Base
@notify_flag_type_ids ||= types.only(:off_topic, :spam, :inappropriate, :notify_moderators).values @notify_flag_type_ids ||= types.only(:off_topic, :spam, :inappropriate, :notify_moderators).values
end end
def topic_flag_types
@topic_flag_types ||= types.only(:spam, :inappropriate, :notify_moderators)
end
def is_flag?(sym) def is_flag?(sym)
flag_types.valid?(sym) flag_types.valid?(sym)
end end

View file

@ -17,6 +17,10 @@ class Site
PostActionType.ordered PostActionType.ordered
end end
def topic_flag_types
post_action_types.where(name_key: ['inappropriate', 'spam', 'notify_moderators'])
end
def notification_types def notification_types
Notification.types Notification.types
end end

View file

@ -12,6 +12,7 @@ class SiteSerializer < ApplicationSerializer
has_many :categories, serializer: BasicCategorySerializer, embed: :objects has_many :categories, serializer: BasicCategorySerializer, embed: :objects
has_many :post_action_types, embed: :objects has_many :post_action_types, embed: :objects
has_many :topic_flag_types, serializer: TopicFlagTypeSerializer, embed: :objects
has_many :trust_levels, embed: :objects has_many :trust_levels, embed: :objects
has_many :archetypes, embed: :objects, serializer: ArchetypeSerializer has_many :archetypes, embed: :objects, serializer: ArchetypeSerializer

View file

@ -0,0 +1,9 @@
class TopicFlagTypeSerializer < PostActionTypeSerializer
protected
def i18n(field, vars={})
I18n.t("topic_flag_types.#{object.name_key}.#{field}", vars)
end
end

View file

@ -35,7 +35,8 @@ class TopicViewSerializer < ApplicationSerializer
:details, :details,
:highest_post_number, :highest_post_number,
:last_read_post_number, :last_read_post_number,
:deleted_by :deleted_by,
:actions_summary
# Define a delegator for each attribute of the topic we want # Define a delegator for each attribute of the topic we want
attributes *topic_attributes attributes *topic_attributes
@ -97,6 +98,7 @@ class TopicViewSerializer < ApplicationSerializer
result[:can_invite_to] = true if scope.can_invite_to?(object.topic) result[:can_invite_to] = true if scope.can_invite_to?(object.topic)
result[:can_create_post] = true if scope.can_create?(Post, object.topic) result[:can_create_post] = true if scope.can_create?(Post, object.topic)
result[:can_reply_as_new_topic] = true if scope.can_reply_as_new_topic?(object.topic) result[:can_reply_as_new_topic] = true if scope.can_reply_as_new_topic?(object.topic)
result[:can_flag_topic] = actions_summary.any? { |a| a[:can_act] }
result result
end end
@ -144,5 +146,17 @@ class TopicViewSerializer < ApplicationSerializer
PinnedCheck.new(object.topic, object.topic_user).pinned? PinnedCheck.new(object.topic, object.topic_user).pinned?
end end
def actions_summary
result = []
return [] unless post = object.posts.try(:first)
PostActionType.topic_flag_types.each do |sym, id|
result << { id: id,
count: 0,
hidden: false,
can_act: scope.post_can_act?(post, sym)}
# TODO: other keys? :can_clear_flags, :acted, :can_undo
end
result
end
end end

View file

@ -788,6 +788,11 @@ en:
title: 'Share' title: 'Share'
help: 'share a link to this topic' help: 'share a link to this topic'
flag_topic:
title: 'Flag'
help: 'privately flag this topic for attention or send a private notification about it'
success_message: 'You successfully flagged this topic.'
inviting: "Inviting..." inviting: "Inviting..."
invite_private: invite_private:
@ -1077,6 +1082,11 @@ en:
more: "{{n}} to go..." more: "{{n}} to go..."
left: "{{n}} remaining" left: "{{n}} remaining"
flagging_topic:
title: "Why are you privately flagging this topic?"
action: "Flag Topic"
notify_action: "Notify"
topic_map: topic_map:
title: "Topic Summary" title: "Topic Summary"
links_shown: "show all {{totalLinks}} links..." links_shown: "show all {{totalLinks}} links..."
@ -1239,12 +1249,16 @@ en:
disagree: "Disagree" disagree: "Disagree"
disagree_title: "Disagree with flag, remove any flags from this post" disagree_title: "Disagree with flag, remove any flags from this post"
delete_spammer_title: "Delete the user and all its posts and topics." delete_spammer_title: "Delete the user and all its posts and topics."
clear_topic_flags: "Done"
clear_topic_flags_title: "The topic has been investigated and issues have been resolved. Click Done to remove the flags."
flagged_by: "Flagged by" flagged_by: "Flagged by"
system: "System" system: "System"
error: "Something went wrong" error: "Something went wrong"
view_message: "Reply" view_message: "Reply"
no_results: "There are no flags." no_results: "There are no flags."
topic_flagged: "This <strong>topic</strong> has been flagged."
visit_topic: "Visit the topic to investigate and take action."
summary: summary:
action_type_3: action_type_3:

View file

@ -360,6 +360,22 @@ en:
description: 'Vote for this post' description: 'Vote for this post'
long_form: 'voted for this post' long_form: 'voted for this post'
topic_flag_types:
spam:
title: 'Spam'
description: 'This topic is an advertisement. It is not useful or relevant to this site, but promotional in nature.'
long_form: 'flagged this as spam'
inappropriate:
title: 'Inappropriate'
description: 'This topic contains content that a reasonable person would consider offensive, abusive, or a violation of <a href="/faq">our community guidelines</a>.'
long_form: 'flagged this as inappropriate'
notify_moderators:
title: 'Notify moderators'
description: 'This topic requires general moderator attention based on the <a href="/faq">FAQ</a>, <a href="%{tos_url}">TOS</a>, or for another reason not listed above.'
long_form: 'notified moderators'
email_title: 'The topic "%{title}" requires moderator attention'
email_body: "%{link}\n\n%{message}"
flagging: flagging:
you_must_edit: '<p>Your post was flagged by the community. Please see your private messages.</p>' you_must_edit: '<p>Your post was flagged by the community. Please see your private messages.</p>'
user_must_edit: '<p>Flagged content temporarily hidden.</p>' user_must_edit: '<p>Flagged content temporarily hidden.</p>'

View file

@ -0,0 +1,5 @@
class AddTargetsTopicToPostActions < ActiveRecord::Migration
def change
add_column :post_actions, :targets_topic, :boolean, default: false
end
end