added 2 new flag types: notify user and notify moderators

fixed up messed up user navigation
refactored
This commit is contained in:
Sam 2013-04-12 17:55:45 +10:00
parent 0f362c5474
commit e969eb14e8
20 changed files with 201 additions and 74 deletions

View file

@ -9,7 +9,7 @@
Discourse.ActionSummary = Discourse.Model.extend({
// Description for the action
description: (function() {
description: function() {
var action = this.get('actionType.name_key');
if (this.get('acted')) {
if (this.get('count') <= 1) {
@ -20,12 +20,12 @@ Discourse.ActionSummary = Discourse.Model.extend({
} else {
return Em.String.i18n('post.actions.by_others.' + action, { count: this.get('count') });
}
}).property('count', 'acted', 'actionType'),
}.property('count', 'acted', 'actionType'),
canAlsoAction: (function() {
canAlsoAction: function() {
if (this.get('hidden')) return false;
return this.get('can_act');
}).property('can_act', 'hidden'),
}.property('can_act', 'hidden'),
// Remove it
removeAction: function() {
@ -37,12 +37,13 @@ Discourse.ActionSummary = Discourse.Model.extend({
// Perform this action
act: function(opts) {
var action = this.get('actionType.name_key');
// Mark it as acted
this.set('acted', true);
this.set('count', this.get('count') + 1);
this.set('can_act', false);
this.set('can_undo', true);
this.set('can_undo', action != 'notify_moderators' && action != 'notify_user');
// Add ourselves to the users who liked it if present
if (this.present('users')) {
@ -108,5 +109,4 @@ Discourse.ActionSummary = Discourse.Model.extend({
});
});
}
});

View file

@ -112,9 +112,10 @@ Discourse.Post = Discourse.Model.extend({
flagsAvailable: (function() {
var _this = this;
return Discourse.get('site.flagTypes').filter(function(item) {
flags = Discourse.get('site.flagTypes').filter(function(item) {
return _this.get("actionByName." + (item.get('name_key')) + ".can_act");
});
return flags;
}).property('Discourse.site.flagTypes', 'actions_summary.@each.can_act'),
actionsHistory: (function() {

View file

@ -7,5 +7,30 @@
@module Discourse
**/
Discourse.PostActionType = Discourse.Model.extend({
});
Discourse.BoundPostActionType = Discourse.PostActionType.extend({
customPlaceholder: function(){
return Em.String.i18n("flagging.custom_placeholder_" + this.get('name_key'));
}.property('name_key'),
formattedName: function(){
return this.get('name').replace("{{username}}", this.get('post.username'));
}.property('name'),
messageChanged: function() {
var len, message, minLen, _ref;
minLen = 10;
len = ((_ref = this.get('message')) ? _ref.length : void 0) || 0;
this.set("customMessageLengthClasses", "too-short custom-message-length");
if (len === 0) {
message = Em.String.i18n("flagging.custom_message.at_least", { n: minLen });
} else if (len < minLen) {
message = Em.String.i18n("flagging.custom_message.more", { n: minLen - len });
} else {
message = Em.String.i18n("flagging.custom_message.left", { n: 500 - len });
this.set("customMessageLengthClasses", "ok custom-message-length");
}
this.set("customMessageLength", message);
}.observes("message")
});

View file

@ -17,14 +17,14 @@ Discourse.Site = Discourse.Model.extend({
return result;
}).property('notification_types'),
flagTypes: (function() {
flagTypes: function() {
var postActionTypes;
postActionTypes = this.get('post_action_types');
if (!postActionTypes) {
return [];
}
return postActionTypes.filterProperty('is_flag', true);
}).property('post_action_types.@each'),
}.property('post_action_types.@each'),
postActionTypeById: function(id) {
return this.get("postActionByIdLookup.action" + id);
@ -49,7 +49,7 @@ Discourse.Site.reopenClass({
result.postActionByIdLookup = Em.Object.create();
result.post_action_types = result.post_action_types.map(function(p) {
var actionType;
actionType = Discourse.Model.create(p);
actionType = Discourse.PostActionType.create(p);
result.postActionByIdLookup.set("action" + p.id, actionType);
return actionType;
});

View file

@ -14,8 +14,12 @@ Discourse.UserActivityRoute = Discourse.Route.extend({
setupController: function(controller) {
var userController = this.controllerFor('user');
userController.set('filter', null);
controller.set('content', userController.get('content'));
var user = userController.get('content');
controller.set('content', user);
user.set('filter', null);
if (user.get('streamFilter')) {
user.filterStream(null);
}
}
});

View file

@ -1,12 +1,12 @@
<div class="modal-body">
<div class="modal-body flag-modal">
{{#if view.post.flagsAvailable}}
<form>
{{#each view.post.flagsAvailable}}
{{#each view.boundFlags}}
<div class='controls'>
<label class='radio'>
<input type='radio' id="radio_{{unbound name_key}}" {{action changePostActionType this target="view"}} name='post_action_type_index'> <strong>{{name}}</strong>
<input type='radio' id="radio_{{unbound name_key}}" {{action changePostActionType this target="view"}} name='post_action_type_index'> <strong>{{formattedName}}</strong>
{{#if is_custom_flag}}
{{#unless view.isCustomFlag}}
{{#unless selected}}
<div class='description'>{{{description}}}</div>
{{/unless}}
{{else}}
@ -16,9 +16,9 @@
{{/if}}
</label>
{{#if is_custom_flag}}
{{#if view.isCustomFlag}}
{{view Ember.TextArea name="message" class="flag-message" placeholderBinding="view.customPlaceholder" valueBinding="view.customFlagMessage"}}
<div {{bindAttr class="view.customMessageLengthClasses"}}>{{view.customMessageLength}}</div>
{{#if selected}}
{{view Ember.TextArea name="message" class="flag-message" placeholderBinding="customPlaceholder" valueBinding="message"}}
<div {{bindAttr class="customMessageLengthClasses"}}>{{customMessageLength}}</div>
{{/if}}
{{/if}}
</div>

View file

@ -10,23 +10,47 @@ Discourse.FlagView = Discourse.ModalBodyView.extend({
templateName: 'flag',
title: Em.String.i18n('flagging.title'),
// trick to bind user / post to flag
boundFlags: function(){
var _this = this;
var original = this.get('post.flagsAvailable');
if(original){
return $.map(original, function(v){
var b = Discourse.BoundPostActionType.create(v);
b.set('post', _this.get('post'));
return b;
});
}
}.property('post.flagsAvailable'),
changePostActionType: function(action) {
if (this.get('postActionTypeId') === action.id) return false;
this.get('boundFlags').each(function(f){
f.set('selected', false);
});
action.set('selected', true);
this.set('postActionTypeId', action.id);
this.set('isCustomFlag', action.is_custom_flag);
this.set('selected', action);
Em.run.next(function() {
$("#radio_" + action.name_key).prop('checked', 'true');
$('#radio_' + action.name_key).prop('checked', 'true');
});
return false;
},
createFlag: function() {
var actionType, _ref,
_this = this;
actionType = Discourse.get("site").postActionTypeById(this.get('postActionTypeId'));
if (_ref = this.get("post.actionByName." + (actionType.get('name_key')))) {
_ref.act({
message: this.get('customFlagMessage')
var _this = this;
var action = this.get('selected');
var postAction = this.get('post.actionByName.' + (action.get('name_key')));
actionType = Discourse.get('site').postActionTypeById(this.get('postActionTypeId'));
if (postAction) {
postAction.act({
message: action.get('message')
}).then(function() {
return $('#discourse-modal').modal('hide');
}, function(errors) {
@ -36,49 +60,24 @@ Discourse.FlagView = Discourse.ModalBodyView.extend({
return false;
},
customPlaceholder: (function() {
return Em.String.i18n("flagging.custom_placeholder");
}).property(),
showSubmit: (function() {
var m;
if (this.get("postActionTypeId")) {
if (this.get("isCustomFlag")) {
m = this.get("customFlagMessage");
if (this.get('postActionTypeId')) {
if (this.get('isCustomFlag')) {
m = this.get('selected.message');
return m && m.length >= 10 && m.length <= 500;
} else {
return true;
}
}
return false;
}).property("isCustomFlag", "customFlagMessage", "postActionTypeId"),
customFlagMessageChanged: (function() {
var len, message, minLen, _ref;
minLen = 10;
len = ((_ref = this.get('customFlagMessage')) ? _ref.length : void 0) || 0;
this.set("customMessageLengthClasses", "too-short custom-message-length");
if (len === 0) {
message = Em.String.i18n("flagging.custom_message.at_least", { n: minLen });
} else if (len < minLen) {
message = Em.String.i18n("flagging.custom_message.more", { n: minLen - len });
} else {
message = Em.String.i18n("flagging.custom_message.left", { n: 500 - len });
this.set("customMessageLengthClasses", "ok custom-message-length");
}
this.set("customMessageLength", message);
}).observes("customFlagMessage"),
}).property('isCustomFlag', 'selected.customMessageLength', 'postActionTypeId'),
didInsertElement: function() {
var $flagModal;
this.customFlagMessageChanged();
this.set('postActionTypeId', null);
$flagModal = $('#flag-modal');
// Would be nice if there were an EmberJs radio button to do this for us. Oh well, one should be coming
// in an upcoming release.
$("input[type='radio']", $flagModal).prop('checked', false);
this.$("input[type='radio']").prop('checked', false);
}
});

View file

@ -164,3 +164,11 @@
}
}
}
.flag-modal {
max-height: 450px;
}
.custom-message-length {
margin-bottom: 10px;
}

View file

@ -73,6 +73,29 @@ class PostAction < ActiveRecord::Base
def self.act(user, post, post_action_type_id, message = nil)
begin
title, target_usernames,body = nil
if message
[:notify_moderators, :notify_user].each do |k|
if post_action_type_id == PostActionType.types[k]
target_usernames = target_moderators(user)
title = I18n.t("post_action_types.#{k}.email_title",
title: post.topic.title)
body = I18n.t("post_action_types.#{k}.email_body",
message: message,
link: "#{Discourse.base_url}#{post.url}")
end
end
end
if target_usernames.present?
PostCreator.new(user,
target_usernames: target_usernames,
archetype: Archetype.private_message,
title: title,
raw: body
).create
end
create(post_id: post.id, user_id: user.id, post_action_type_id: post_action_type_id, message: message)
rescue ActiveRecord::RecordNotUnique
# can happen despite being .create
@ -101,6 +124,10 @@ class PostAction < ActiveRecord::Base
PostActionType.flag_types.values.include?(post_action_type_id)
end
def is_private_message?
post_action_type_id == PostActionType.types[:notify_user] ||
post_action_type_id == PostActionType.types[:notify_moderators]
end
# A custom rate limiter for this model
def post_action_rate_limiter
return unless is_flag? || is_bookmark? || is_like?
@ -169,4 +196,16 @@ class PostAction < ActiveRecord::Base
end
end
end
protected
def self.target_moderators(me)
User
.where("moderator = 't' or admin = 't'")
.where('id <> ?', [me.id])
.select('username')
.map{|u| u.username}
.join(',')
end
end

View file

@ -10,15 +10,15 @@ class PostActionType < ActiveRecord::Base
def types
@types ||= Enum.new(:bookmark, :like, :off_topic, :inappropriate, :vote,
:custom_flag, :spam)
:notify_user, :notify_moderators, :spam)
end
def auto_action_flag_types
@auto_action_flag_types ||= flag_types.except(:custom_flag)
@auto_action_flag_types ||= flag_types.except(:notify_user, :notify_moderators)
end
def flag_types
@flag_types ||= types.only(:off_topic, :spam, :inappropriate, :custom_flag)
@flag_types ||= types.only(:off_topic, :spam, :inappropriate, :notify_user, :notify_moderators)
end
def is_flag?(sym)

View file

@ -3,7 +3,8 @@ class PostActionTypeSerializer < ApplicationSerializer
attributes :name_key, :name, :description, :long_form, :is_flag, :icon, :id, :is_custom_flag
def is_custom_flag
object.id == PostActionType.types[:custom_flag]
object.id == PostActionType.types[:notify_user] ||
object.id == PostActionType.types[:notify_moderators]
end
def name

View file

@ -66,7 +66,8 @@ class UserSerializer < BasicUserSerializer
end
def stream
UserAction.stream(user_id: object.id, offset: 0, limit: 60, guardian: scope)
UserAction.stream(user_id: object.id, offset: 0, limit: 60,
guardian: scope, ignore_private_messages: true)
end
def can_edit

View file

@ -609,7 +609,6 @@ en:
off_topic: "Undo flag"
spam: "Undo flag"
inappropriate: "Undo flag"
custom_flag: "Undo flag"
bookmark: "Undo bookmark"
like: "Undo like"
vote: "Undo vote"
@ -617,7 +616,8 @@ en:
off_topic: "{{icons}} marked this as off-topic"
spam: "{{icons}} marked this as spam"
inappropriate: "{{icons}} marked this as inappropriate"
custom_flag: "{{icons}} flagged this"
notify_moderators: "{{icons}} flagged this"
notify_user: "{{icons}} started a private conversation"
bookmark: "{{icons}} bookmarked this"
like: "{{icons}} liked this"
vote: "{{icons}} voted for this"
@ -625,7 +625,8 @@ en:
off_topic: "You flagged this as off-topic"
spam: "You flagged this as spam"
inappropriate: "You flagged this as inappropriate"
custom_flag: "You flagged this for moderation"
notify_moderators: "You flagged this for moderation"
notify_user: "You started a private conversation with this user"
bookmark: "You bookmarked this post"
like: "You liked this"
vote: "You voted for this post"
@ -639,9 +640,12 @@ en:
inappropriate:
one: "You and 1 other flagged this as inappropriate"
other: "You and {{count}} other people flagged this as inappropriate"
custom_flag:
notify_moderators:
one: "You and 1 other flagged this for moderation"
other: "You and {{count}} other people flagged this for moderation"
notify_user:
one: "You and 1 other started a private conversation with this user"
other: "You and {{count}} other started a private conversation with this user"
bookmark:
one: "You and 1 other bookmarked this post"
other: "You and {{count}} other people bookmarked this post"
@ -661,9 +665,12 @@ en:
inappropriate:
one: "1 person flagged this as inappropriate"
other: "{{count}} people flagged this as inappropriate"
custom_flag:
notify_moderators:
one: "1 person flagged this for moderation"
other: "{{count}} people flagged this for moderation"
notify_user:
one: "1 person started a private conversation with this user"
other: "{{count}} started a private conversation with this user"
bookmark:
one: "1 person bookmarked this post"
other: "{{count}} people bookmarked this post"
@ -714,7 +721,8 @@ en:
title: 'Why are you flagging this post?'
action: 'Flag Post'
cant: "Sorry, you can't flag this post at this time."
custom_placeholder: "Why does this post require moderator attention? Let us know specifically what you are concerned about, and provide relevant links where possible."
custom_placeholder_notify_user: "Why are you contacting this user privately?"
custom_placeholder_notify_moderators: "Why does this post require moderator attention? Let us know specifically what you are concerned about, and provide relevant links where possible."
custom_message:
at_least: "enter at least {{n}} characters"
more: "{{n}} to go..."

View file

@ -227,10 +227,18 @@ en:
title: 'Inappropriate'
description: 'This post contains content that a reasonable person would consider offensive, abusive, or hate speech.'
long_form: 'flagged this as inappropriate'
custom_flag:
title: 'Other'
notify_user:
title: 'Notify {{username}}'
description: 'This post contains something I want to talk to this user directly and privately about.'
long_form: 'notified user'
email_title: 'Regarding "%{title}"'
email_body: "%{link}\n\n%{message}"
notify_moderators:
title: 'Notify moderators'
description: 'This post requires general moderator attention based on the <a href="/faq">FAQ</a>, <a href="/tos">TOS</a>, or for another reason not listed above.'
long_form: 'flagged this for moderation'
long_form: 'notified moderators'
email_title: 'Regarding "%{title}"'
email_body: "%{link}\n\n%{message}"
bookmark:
title: 'Bookmark'
description: 'Bookmark this post'

View file

@ -42,8 +42,15 @@ PostActionType.seed do |s|
end
PostActionType.seed do |s|
s.id = PostActionType.types[:custom_flag]
s.name_key = 'custom_flag'
s.id = PostActionType.types[:notify_user]
s.name_key = 'notify_user'
s.is_flag = true
s.position = 7
end
PostActionType.seed do |s|
s.id = PostActionType.types[:notify_moderators]
s.name_key = 'notify_moderators'
s.is_flag = true
s.position = 8
end

View file

@ -0,0 +1,6 @@
class CorrectCountsOnPosts < ActiveRecord::Migration
def change
rename_column :posts, :custom_flag_count, :notify_moderators_count
add_column :posts, :notify_user_count, :integer, default: 0, null: false
end
end

View file

@ -0,0 +1,6 @@
class CorrectCountsOnTopics < ActiveRecord::Migration
def change
rename_column :topics, :custom_flag_count, :notify_moderators_count
add_column :topics, :notify_user_count, :integer, default: 0, null: false
end
end

View file

@ -294,6 +294,7 @@ class Guardian
# You can only undo your own actions
return false unless @user
return false unless post_action.user_id == @user.id
return false if post_action.is_private_message?
# Make sure they want to delete it within the window
return post_action.created_at > SiteSetting.post_undo_action_window_mins.minutes.ago

View file

@ -13,6 +13,15 @@ describe PostAction do
let(:post) { Fabricate(:post) }
let(:bookmark) { PostAction.new(user_id: post.user_id, post_action_type_id: PostActionType.types[:bookmark] , post_id: post.id) }
describe "messaging" do
it "sends an email to all moderators if selected" do
PostAction.stubs(:create)
PostAction.expects(:target_moderators).returns("bob")
PostCreator.any_instance.expects(:create).returns(nil)
PostAction.act(build(:user), build(:post), PostActionType.types[:notify_moderators], "this is my special message");
end
end
describe "flag counts" do
before do
PostAction.update_flagged_posts_count

View file

@ -101,6 +101,10 @@ Spork.each_run do
Rails.cache.reconnect
end
def build(*args)
Fabricate.build(*args)
end
# --- Instructions ---
# Sort the contents of this file into a Spork.prefork and a Spork.each_run
# block.