allow pms to be targetted at groups
This commit is contained in:
Sam 2013-05-02 15:15:17 +10:00
parent e59ab32210
commit 65cd00cf25
27 changed files with 176 additions and 61 deletions

View file

@ -12,10 +12,11 @@ Discourse.AdminDashboardController = Ember.Controller.extend({
problemsCheckInterval: '1 minute ago', problemsCheckInterval: '1 minute ago',
foundProblems: function() { foundProblems: function() {
return(this.get('problems') && this.get('problems').length > 0); return(Discourse.currentUser.admin && this.get('problems') && this.get('problems').length > 0);
}.property('problems'), }.property('problems'),
thereWereProblems: function() { thereWereProblems: function() {
if(!Discourse.currentUser.admin) { return false }
if( this.get('foundProblems') ) { if( this.get('foundProblems') ) {
this.set('hadProblems', true); this.set('hadProblems', true);
return true; return true;

View file

@ -4,14 +4,18 @@
<ul class="nav nav-pills"> <ul class="nav nav-pills">
<li>{{#linkTo 'admin.dashboard'}}{{i18n admin.dashboard.title}}{{/linkTo}}</li> <li>{{#linkTo 'admin.dashboard'}}{{i18n admin.dashboard.title}}{{/linkTo}}</li>
<li>{{#linkTo 'admin.site_settings'}}{{i18n admin.site_settings.title}}{{/linkTo}}</li> {{#if Discourse.currentUser.admin}}
<li>{{#linkTo 'adminSiteContents'}}{{i18n admin.site_content.title}}{{/linkTo}}</li> <li>{{#linkTo 'admin.site_settings'}}{{i18n admin.site_settings.title}}{{/linkTo}}</li>
<li>{{#linkTo 'adminSiteContents'}}{{i18n admin.site_content.title}}{{/linkTo}}</li>
{{/if}}
<li>{{#linkTo 'adminUsersList.active'}}{{i18n admin.users.title}}{{/linkTo}}</li> <li>{{#linkTo 'adminUsersList.active'}}{{i18n admin.users.title}}{{/linkTo}}</li>
<!--<li>{{#linkTo 'admin.groups'}}{{i18n admin.groups.title}}{{/linkTo}}</li>--> <!--<li>{{#linkTo 'admin.groups'}}{{i18n admin.groups.title}}{{/linkTo}}</li>-->
<li>{{#linkTo 'admin.email_logs'}}{{i18n admin.email_logs.title}}{{/linkTo}}</li> <li>{{#linkTo 'admin.email_logs'}}{{i18n admin.email_logs.title}}{{/linkTo}}</li>
<li>{{#linkTo 'adminFlags.active'}}{{i18n admin.flags.title}}{{/linkTo}}</li> <li>{{#linkTo 'adminFlags.active'}}{{i18n admin.flags.title}}{{/linkTo}}</li>
<li>{{#linkTo 'admin.customize'}}{{i18n admin.customize.title}}{{/linkTo}}</li> {{#if Discourse.currentUser.admin}}
<li>{{#linkTo 'admin.api'}}{{i18n admin.api.title}}{{/linkTo}}</li> <li>{{#linkTo 'admin.customize'}}{{i18n admin.customize.title}}{{/linkTo}}</li>
<li>{{#linkTo 'admin.api'}}{{i18n admin.api.title}}{{/linkTo}}</li>
{{/if}}
</ul> </ul>
<div class='boxed white admin-content'> <div class='boxed white admin-content'>

View file

@ -31,9 +31,11 @@
<div class='field'>{{i18n user.ip_address.title}}</div> <div class='field'>{{i18n user.ip_address.title}}</div>
<div class='value'>{{content.ip_address}}</div> <div class='value'>{{content.ip_address}}</div>
<div class='controls'> <div class='controls'>
{{#if Discourse.currentUser.admin}}
<button class='btn' {{action refreshBrowsers target="content"}}> <button class='btn' {{action refreshBrowsers target="content"}}>
{{i18n admin.user.refresh_browsers}} {{i18n admin.user.refresh_browsers}}
</button> </button>
{{/if}}
</div> </div>
</div> </div>

View file

@ -60,7 +60,7 @@ Discourse = Ember.Application.createWithMixins({
if (user) { if (user) {
bus.callbackInterval = Discourse.SiteSettings.polling_interval; bus.callbackInterval = Discourse.SiteSettings.polling_interval;
bus.enableLongPolling = true; bus.enableLongPolling = true;
if (user.admin) { if (user.admin || user.moderator) {
bus.subscribe("/flagged_counts", function(data) { bus.subscribe("/flagged_counts", function(data) {
user.set('site_flagged_posts_count', data.total); user.set('site_flagged_posts_count', data.total);
}); });

View file

@ -13,7 +13,7 @@ Discourse.UserController = Discourse.ObjectController.extend({
}).property('content.username', 'Discourse.currentUser.username'), }).property('content.username', 'Discourse.currentUser.username'),
canSeePrivateMessages: (function() { canSeePrivateMessages: (function() {
return this.get('viewingSelf') || Discourse.get('currentUser.admin'); return this.get('viewingSelf') || Discourse.get('currentUser.moderator');
}).property('viewingSelf', 'Discourse.currentUser') }).property('viewingSelf', 'Discourse.currentUser')
}); });

View file

@ -65,7 +65,7 @@
<section class='d-dropdown' id='site-map-dropdown'> <section class='d-dropdown' id='site-map-dropdown'>
<ul> <ul>
{{#if Discourse.currentUser.admin}} {{#if Discourse.currentUser.moderator}}
<li><a href="/admin"><i class='icon-cog'></i>{{i18n admin_title}}</a></li> <li><a href="/admin"><i class='icon-cog'></i>{{i18n admin_title}}</a></li>
<li><a href="/admin/flags/active"><i class='icon-flag'></i>{{i18n flags_title}}</a></li> <li><a href="/admin/flags/active"><i class='icon-flag'></i>{{i18n flags_title}}</a></li>
{{/if}} {{/if}}

View file

@ -134,6 +134,6 @@
{{render share}} {{render share}}
{{render quoteButton}} {{render quoteButton}}
{{#if Discourse.currentUser.admin}} {{#if Discourse.currentUser.moderator}}
{{render topicAdminMenu content}} {{render topicAdminMenu content}}
{{/if}} {{/if}}

View file

@ -7,7 +7,7 @@
{{#if viewingSelf}} {{#if viewingSelf}}
<button {{action "logout" target="Discourse"}} class='btn'>{{i18n user.log_out}}</button> <button {{action "logout" target="Discourse"}} class='btn'>{{i18n user.log_out}}</button>
{{/if}} {{/if}}
{{#if Discourse.currentUser.admin}} {{#if Discourse.currentUser.moderator}}
<a href="{{unbound content.adminPath}}" class='btn'><i class="icon-wrench"></i>&nbsp;{{i18n admin.user.show_admin_profile}}</a> <a href="{{unbound content.adminPath}}" class='btn'><i class="icon-wrench"></i>&nbsp;{{i18n admin.user.show_admin_profile}}</a>
{{/if}} {{/if}}
<ul class="nav nav-pills"> <ul class="nav nav-pills">

View file

@ -1,7 +1,7 @@
class Admin::AdminController < ApplicationController class Admin::AdminController < ApplicationController
before_filter :ensure_logged_in before_filter :ensure_logged_in
before_filter :ensure_is_admin before_filter :ensure_is_moderator
def index def index
render nothing: true render nothing: true
@ -9,8 +9,8 @@ class Admin::AdminController < ApplicationController
protected protected
def ensure_is_admin def ensure_is_moderator
raise Discourse::InvalidAccess.new unless current_user.admin? raise Discourse::InvalidAccess.new unless current_user.moderator?
end end
end end

View file

@ -34,6 +34,10 @@ module ApplicationHelper
current_user.try(:admin?) current_user.try(:admin?)
end end
def moderator?
current_user.try(:moderator?)
end
# Creates open graph and twitter card meta data # Creates open graph and twitter card meta data
def crawlable_meta_data(opts=nil) def crawlable_meta_data(opts=nil)

View file

@ -25,8 +25,8 @@ class PostAction < ActiveRecord::Base
'topics.deleted_at' => nil).count('DISTINCT posts.id') 'topics.deleted_at' => nil).count('DISTINCT posts.id')
$redis.set('posts_flagged_count', posts_flagged_count) $redis.set('posts_flagged_count', posts_flagged_count)
admins = User.admins.select(:id).map {|u| u.id} user_ids = User.where("admin = 't' or moderator = 't'").select(:id).map {|u| u.id}
MessageBus.publish('/flagged_counts', { total: posts_flagged_count }, { user_ids: admins }) MessageBus.publish('/flagged_counts', { total: posts_flagged_count }, { user_ids: user_ids })
end end
def self.flagged_posts_count def self.flagged_posts_count

View file

@ -60,8 +60,8 @@ class PostAlertObserver < ActiveRecord::Observer
def after_create_post(post) def after_create_post(post)
if post.topic.private_message? if post.topic.private_message?
# If it's a private message, notify the topic_allowed_users # If it's a private message, notify the topic_allowed_users
post.topic.topic_allowed_users.reject{ |a| a.user_id == post.user_id }.each do |a| post.topic.all_allowed_users.reject{ |a| a.id == post.user_id }.each do |a|
create_notification(a.user, Notification.types[:private_message], post) create_notification(a, Notification.types[:private_message], post)
end end
else else
# If it's not a private message, notify the users # If it's not a private message, notify the users

View file

@ -38,6 +38,10 @@ class Topic < ActiveRecord::Base
belongs_to :category belongs_to :category
has_many :posts has_many :posts
has_many :topic_allowed_users has_many :topic_allowed_users
has_many :topic_allowed_groups
has_many :allowed_group_users, through: :allowed_groups, source: :users
has_many :allowed_groups, through: :topic_allowed_groups, source: :group
has_many :allowed_users, through: :topic_allowed_users, source: :user has_many :allowed_users, through: :topic_allowed_users, source: :user
has_one :hot_topic has_one :hot_topic
@ -94,6 +98,12 @@ class Topic < ActiveRecord::Base
end end
end end
# all users (in groups or directly targetted) that are going to get the pm
def all_allowed_users
# TODO we should probably change this from 3 queries to 1
User.where('id in (?)', allowed_users.select('users.id').to_a + allowed_group_users.select('users.id').to_a)
end
# Additional rate limits on topics: per day and private messages per day # Additional rate limits on topics: per day and private messages per day
def limit_topics_per_day def limit_topics_per_day
RateLimiter.new(user, "topics-per-day:#{Date.today.to_s}", SiteSetting.max_topics_per_day, 1.day.to_i) RateLimiter.new(user, "topics-per-day:#{Date.today.to_s}", SiteSetting.max_topics_per_day, 1.day.to_i)

View file

@ -0,0 +1,7 @@
class TopicAllowedGroup < ActiveRecord::Base
belongs_to :topic
belongs_to :group
attr_accessible :group_id, :user_id
validates_uniqueness_of :topic_id, scope: :group_id
end

View file

@ -51,8 +51,8 @@ class TopicList
def has_rank_details? def has_rank_details?
# Only admins can see rank details # Only moderators can see rank details
return false unless @current_user.try(:admin?) return false unless @current_user.try(:moderator?)
# Only show them on 'Hot' # Only show them on 'Hot'
return @filter == :hot return @filter == :hot

View file

@ -15,8 +15,15 @@ class CurrentUserSerializer < BasicUserSerializer
# we probably want to move this into site, but that json is cached so hanging it off current user seems okish # we probably want to move this into site, but that json is cached so hanging it off current user seems okish
def moderator
# TODO we probably want better terminology
#
# we have admins / moderators and users who are either moderators or admins denoted by moderator?
object.moderator?
end
def include_site_flagged_posts_count? def include_site_flagged_posts_count?
object.admin object.moderator?
end end
def topic_count def topic_count

View file

@ -88,7 +88,7 @@ class PostSerializer < ApplicationSerializer
end end
def cooked def cooked
if object.hidden && !scope.is_admin? if object.hidden && !scope.is_moderator?
if scope.current_user && object.user_id == scope.current_user.id if scope.current_user && object.user_id == scope.current_user.id
I18n.t('flagging.you_must_edit') I18n.t('flagging.you_must_edit')
else else
@ -154,7 +154,7 @@ class PostSerializer < ApplicationSerializer
# The following only applies if you're logged in # The following only applies if you're logged in
if action_summary[:can_act] && scope.current_user.present? if action_summary[:can_act] && scope.current_user.present?
action_summary[:can_clear_flags] = scope.is_admin? && PostActionType.flag_types.values.include?(id) action_summary[:can_clear_flags] = scope.is_moderator? && PostActionType.flag_types.values.include?(id)
end end
if post_actions.present? && post_actions.has_key?(id) if post_actions.present? && post_actions.has_key?(id)
@ -163,7 +163,7 @@ class PostSerializer < ApplicationSerializer
end end
# anonymize flags # anonymize flags
if !scope.is_admin? && PostActionType.flag_types.values.include?(id) if !scope.is_moderator? && PostActionType.flag_types.values.include?(id)
action_summary[:count] = action_summary[:acted] ? 1 : 0 action_summary[:count] = action_summary[:acted] ? 1 : 0
end end

View file

@ -18,7 +18,7 @@
<%# load the selected locale before any other scripts %> <%# load the selected locale before any other scripts %>
<%= javascript_include_tag "locales/#{I18n.locale}" %> <%= javascript_include_tag "locales/#{I18n.locale}" %>
<%= javascript_include_tag "application" %> <%= javascript_include_tag "application" %>
<%- if admin? %> <%- if moderator? %>
<%= javascript_include_tag "admin"%> <%= javascript_include_tag "admin"%>
<%- end %> <%- end %>

View file

@ -2,7 +2,7 @@
<%=stylesheet_link_tag "application"%> <%=stylesheet_link_tag "application"%>
<%- end %> <%- end %>
<%- if admin? %> <%- if moderator? %>
<%= stylesheet_link_tag "admin"%> <%= stylesheet_link_tag "admin"%>
<%-end%> <%-end%>
<%=SiteCustomization.custom_stylesheet(session[:preview_style])%> <%=SiteCustomization.custom_stylesheet(session[:preview_style])%>

View file

@ -1,6 +1,7 @@
require 'sidekiq/web' require 'sidekiq/web'
require_dependency 'admin_constraint' require_dependency 'admin_constraint'
require_dependency 'moderator_constraint'
require_dependency 'homepage_constraint' require_dependency 'homepage_constraint'
# This used to be User#username_format, but that causes a preload of the User object # This used to be User#username_format, but that causes a preload of the User object
@ -21,13 +22,14 @@ Discourse::Application.routes.draw do
end end
get 'srv/status' => 'forums#status' get 'srv/status' => 'forums#status'
namespace :admin, constraints: AdminConstraint.new do namespace :admin, constraints: ModeratorConstraint.new do
get '' => 'admin#index' get '' => 'admin#index'
resources :site_settings resources :site_settings, constraints: AdminConstraint.new
get 'reports/:type' => 'reports#show' get 'reports/:type' => 'reports#show'
resources :groups resources :groups, constraints: AdminConstraint.new
resources :users, id: USERNAME_ROUTE_FORMAT do resources :users, id: USERNAME_ROUTE_FORMAT do
collection do collection do
get 'list/:query' => 'users#index' get 'list/:query' => 'users#index'
@ -36,35 +38,35 @@ Discourse::Application.routes.draw do
put 'ban' put 'ban'
put 'delete_all_posts' put 'delete_all_posts'
put 'unban' put 'unban'
put 'revoke_admin' put 'revoke_admin', constraints: AdminConstraint.new
put 'grant_admin' put 'grant_admin', constraints: AdminConstraint.new
put 'revoke_moderation' put 'revoke_moderation', constraints: AdminConstraint.new
put 'grant_moderation' put 'grant_moderation', constraints: AdminConstraint.new
put 'approve' put 'approve'
post 'refresh_browsers' post 'refresh_browsers', constraints: AdminConstraint.new
end end
resources :impersonate resources :impersonate, constraints: AdminConstraint.new
resources :email_logs do resources :email_logs do
collection do collection do
post 'test' post 'test'
end end
end end
get 'customize' => 'site_customizations#index' get 'customize' => 'site_customizations#index', constraints: AdminConstraint.new
get 'flags' => 'flags#index' get 'flags' => 'flags#index'
get 'flags/:filter' => 'flags#index' get 'flags/:filter' => 'flags#index'
post 'flags/clear/:id' => 'flags#clear' post 'flags/clear/:id' => 'flags#clear'
resources :site_customizations resources :site_customizations, constraints: AdminConstraint.new
resources :site_contents resources :site_contents, constraints: AdminConstraint.new
resources :site_content_types resources :site_content_types, constraints: AdminConstraint.new
resources :export resources :export, constraints: AdminConstraint.new
get 'version_check' => 'versions#show' get 'version_check' => 'versions#show'
resources :dashboard, only: [:index] do resources :dashboard, only: [:index] do
collection do collection do
get 'problems' get 'problems'
end end
end end
resources :api, only: [:index] do resources :api, only: [:index], constraints: AdminConstraint.new do
collection do collection do
post 'generate_key' post 'generate_key'
end end

View file

@ -1,6 +1,7 @@
class AddTopicAllowedGroups < ActiveRecord::Migration class AddTopicAllowedGroups < ActiveRecord::Migration
def change def change
create_table :topic_allowed_groups do |t| create_table :topic_allowed_groups do |t|
# oops
t.integer :group_id, :integer, null: false t.integer :group_id, :integer, null: false
t.integer :topic_id, :integer, null: false t.integer :topic_id, :integer, null: false
end end

View file

@ -0,0 +1,6 @@
class FixTopicAllowedGroups < ActiveRecord::Migration
def change
# big oops
remove_column :topic_allowed_groups, :integer
end
end

View file

@ -207,7 +207,7 @@ class Guardian
end end
def can_see_private_messages?(user_id) def can_see_private_messages?(user_id)
return true if is_admin? return true if is_moderator?
return false if @user.blank? return false if @user.blank?
@user.id == user_id @user.id == user_id
end end
@ -263,7 +263,7 @@ class Guardian
def can_edit_user?(user) def can_edit_user?(user)
return true if user == @user return true if user == @user
@user.admin? @user.moderator?
end end
def can_edit_topic?(topic) def can_edit_topic?(topic)
@ -311,12 +311,12 @@ class Guardian
return post_action.created_at > SiteSetting.post_undo_action_window_mins.minutes.ago return post_action.created_at > SiteSetting.post_undo_action_window_mins.minutes.ago
end end
def can_send_private_message?(target_user) def can_send_private_message?(target)
return false unless User === target_user return false unless User === target || Group === target
return false if @user.blank? return false if @user.blank?
# Can't send message to yourself # Can't send message to yourself
return false if @user.id == target_user.id return false if User === target && @user.id == target.id
# Have to be a basic level at least # Have to be a basic level at least
return false unless @user.has_trust_level?(:basic) return false unless @user.has_trust_level?(:basic)
@ -336,15 +336,15 @@ class Guardian
return false unless topic return false unless topic
return true if @user && @user.moderator? return true if @user && @user.moderator?
return false if topic.deleted_at.present? return false if topic.deleted_at
if topic.category && topic.category.secure if topic.category && topic.category.secure
return false unless @user && can_see_category?(topic.category) return false unless @user && can_see_category?(topic.category)
end end
if topic.private_message? if topic.private_message?
return false if @user.blank? return false unless @user
return true if topic.allowed_users.include?(@user) return true if topic.all_allowed_users.where(id: @user.id).exists?
return is_admin? return is_admin?
end end
true true
@ -375,11 +375,11 @@ class Guardian
def post_can_act?(post, action_key, opts={}) def post_can_act?(post, action_key, opts={})
return false if @user.blank? return false if @user.blank?
return false if post.blank? return false if post.blank?
return false if post.topic.archived?
taken = opts[:taken_actions] taken = opts[:taken_actions]
taken = taken.keys if taken taken = taken.keys if taken
# we always allow flagging
if PostActionType.is_flag?(action_key) if PostActionType.is_flag?(action_key)
return false unless @user.has_trust_level?(:basic) return false unless @user.has_trust_level?(:basic)
@ -390,6 +390,9 @@ class Guardian
return false if taken && taken.include?(PostActionType.types[action_key]) return false if taken && taken.include?(PostActionType.types[action_key])
end end
# nothing else on archived posts
return false if post.topic.archived?
case action_key case action_key
when :like when :like
return false if post.user == @user return false if post.user == @user

View file

@ -0,0 +1,10 @@
require_dependency 'current_user'
class ModeratorConstraint
def matches?(request)
return false unless request.session[:current_user_id].present?
User.where("admin = 't' or moderator = 't'").where(id: request.session[:current_user_id].to_i).exists?
end
end

View file

@ -56,17 +56,14 @@ class PostCreator
topic.subtype = TopicSubtype.user_to_user unless topic.subtype topic.subtype = TopicSubtype.user_to_user unless topic.subtype
usernames = @opts[:target_usernames].split(',') unless @opts[:target_usernames].present? || @opts[:target_group_names].present?
User.where(username: usernames).each do |u| topic.errors.add(:archetype, :cant_send_pm)
@errors = topic.errors
unless guardian.can_send_private_message?(u) raise ActiveRecord::Rollback.new
topic.errors.add(:archetype, :cant_send_pm)
@errors = topic.errors
raise ActiveRecord::Rollback.new
end
topic.topic_allowed_users.build(user_id: u.id)
end end
add_users(topic,@opts[:target_usernames])
add_groups(topic,@opts[:target_group_names])
topic.topic_allowed_users.build(user_id: @user.id) topic.topic_allowed_users.build(user_id: @user.id)
end end
@ -148,4 +145,35 @@ class PostCreator
PostCreator.new(user, opts).create PostCreator.new(user, opts).create
end end
protected
def add_users(topic, usernames)
return unless usernames
usernames = usernames.split(',')
User.where(username: usernames).each do |u|
unless guardian.can_send_private_message?(u)
topic.errors.add(:archetype, :cant_send_pm)
@errors = topic.errors
raise ActiveRecord::Rollback.new
end
topic.topic_allowed_users.build(user_id: u.id)
end
end
def add_groups(topic, groups)
return unless groups
groups = groups.split(',')
Group.where(name: groups).each do |g|
unless guardian.can_send_private_message?(g)
topic.errors.add(:archetype, :cant_send_pm)
@errors = topic.errors
raise ActiveRecord::Rollback.new
end
topic.topic_allowed_groups.build(group_id: g.id)
end
end
end end

View file

@ -485,8 +485,8 @@ describe Guardian do
Guardian.new(user).can_edit?(user).should be_true Guardian.new(user).can_edit?(user).should be_true
end end
it 'returns false as a moderator' do it 'returns true as a moderator' do
Guardian.new(moderator).can_edit?(user).should be_false Guardian.new(moderator).can_edit?(user).should be_true
end end
it 'returns true as an admin' do it 'returns true as an admin' do

View file

@ -190,5 +190,35 @@ describe PostCreator do
end end
end end
context 'private message to group' do
let(:target_user1) { Fabricate(:coding_horror) }
let(:target_user2) { Fabricate(:moderator) }
let(:group) do
g = Fabricate.build(:group)
g.add(target_user1)
g.add(target_user2)
g.save
g
end
let(:unrelated) { Fabricate(:user) }
let(:post) do
PostCreator.create(user, title: 'hi there welcome to my topic',
raw: "this is my awesome message @#{unrelated.username_lower}",
archetype: Archetype.private_message,
target_group_names: group.name)
end
it 'acts correctly' do
post.topic.archetype.should == Archetype.private_message
post.topic.topic_allowed_users.count.should == 1
post.topic.topic_allowed_groups.count.should == 1
# does not notify an unrelated user
unrelated.notifications.count.should == 0
post.topic.subtype.should == TopicSubtype.user_to_user
target_user1.notifications.count.should == 1
target_user2.notifications.count.should == 1
end
end
end end