diff --git a/app/assets/javascripts/discourse/controllers/user.js.es6 b/app/assets/javascripts/discourse/controllers/user.js.es6 index e3a055627b7..4a0e666cfa0 100644 --- a/app/assets/javascripts/discourse/controllers/user.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user.js.es6 @@ -19,10 +19,7 @@ export default ObjectController.extend(CanCheckEmails, { linkWebsite: Em.computed.not('isBasic'), - canSeePrivateMessages: function() { - return this.get('viewingSelf') || Discourse.User.currentProp('admin'); - }.property('viewingSelf'), - + canSeePrivateMessages: Ember.computed.or('viewingSelf', 'currentUser.admin'), canSeeNotificationHistory: Em.computed.alias('canSeePrivateMessages'), showBadges: function() { diff --git a/app/assets/javascripts/discourse/models/user_action.js b/app/assets/javascripts/discourse/models/user_action.js index ba555e84838..2c5060258d5 100644 --- a/app/assets/javascripts/discourse/models/user_action.js +++ b/app/assets/javascripts/discourse/models/user_action.js @@ -1,12 +1,3 @@ -/** - A data model representing actions users have taken - - @class UserAction - @extends Discourse.Model - @namespace Discourse - @module Discourse -**/ - var UserActionTypes = { likes_given: 1, likes_received: 2, @@ -18,7 +9,8 @@ var UserActionTypes = { quotes: 9, edits: 11, messages_sent: 12, - messages_received: 13 + messages_received: 13, + pending: 14 }, InvertedActionTypes = {}; diff --git a/app/assets/javascripts/discourse/routes/user-activity-pending.js.es6 b/app/assets/javascripts/discourse/routes/user-activity-pending.js.es6 new file mode 100644 index 00000000000..5d7e9e47b75 --- /dev/null +++ b/app/assets/javascripts/discourse/routes/user-activity-pending.js.es6 @@ -0,0 +1,5 @@ +import UserActivityStreamRoute from "discourse/routes/user-activity-stream"; + +export default UserActivityStreamRoute.extend({ + userActionType: Discourse.UserAction.TYPES.pending +}); diff --git a/app/assets/javascripts/discourse/templates/user/user.hbs b/app/assets/javascripts/discourse/templates/user/user.hbs index 8fdb0ceeab7..2aaa4a90cc7 100644 --- a/app/assets/javascripts/discourse/templates/user/user.hbs +++ b/app/assets/javascripts/discourse/templates/user/user.hbs @@ -168,7 +168,7 @@ {{#if canSeeNotificationHistory}} {{#link-to 'user.notifications' tagName="li"}} {{#link-to 'user.notifications'}} - + {{fa-icon "comment" class="glyph"}} {{i18n 'user.notifications'}} ({{notification_count}}) {{/link-to}} diff --git a/app/controllers/user_actions_controller.rb b/app/controllers/user_actions_controller.rb index da2e042d481..e4d4079fec0 100644 --- a/app/controllers/user_actions_controller.rb +++ b/app/controllers/user_actions_controller.rb @@ -8,16 +8,23 @@ class UserActionsController < ApplicationController user = fetch_user_from_params - opts = { - user_id: user.id, - offset: params[:offset].to_i, - limit: per_chunk, - action_types: (params[:filter] || "").split(",").map(&:to_i), - guardian: guardian, - ignore_private_messages: params[:filter] ? false : true - } + opts = { user_id: user.id, + user: user, + offset: params[:offset].to_i, + limit: per_chunk, + action_types: (params[:filter] || "").split(",").map(&:to_i), + guardian: guardian, + ignore_private_messages: params[:filter] ? false : true } - render_serialized(UserAction.stream(opts), UserActionSerializer, root: "user_actions") + # Pending is restricted + stream = if opts[:action_types].include?(UserAction::PENDING) + guardian.ensure_can_see_notifications!(user) + UserAction.stream_queued(opts) + else + UserAction.stream(opts) + end + + render_serialized(stream, UserActionSerializer, root: "user_actions") end def show diff --git a/app/models/user_action.rb b/app/models/user_action.rb index 95f87387d5e..0e979991c0e 100644 --- a/app/models/user_action.rb +++ b/app/models/user_action.rb @@ -17,10 +17,12 @@ class UserAction < ActiveRecord::Base EDIT = 11 NEW_PRIVATE_MESSAGE = 12 GOT_PRIVATE_MESSAGE = 13 + PENDING = 14 ORDER = Hash[*[ GOT_PRIVATE_MESSAGE, NEW_PRIVATE_MESSAGE, + PENDING, NEW_TOPIC, REPLY, RESPONSE, @@ -56,15 +58,14 @@ class UserAction < ActiveRecord::Base SELECT action_type, COUNT(*) count FROM user_actions a - JOIN topics t ON t.id = a.target_topic_id + LEFT JOIN topics t ON t.id = a.target_topic_id LEFT JOIN posts p on p.id = a.target_post_id - JOIN posts p2 on p2.topic_id = a.target_topic_id and p2.post_number = 1 + LEFT JOIN posts p2 on p2.topic_id = a.target_topic_id and p2.post_number = 1 LEFT JOIN categories c ON c.id = t.category_id /*where*/ GROUP BY action_type SQL - builder.where('a.user_id = :user_id', user_id: user_id) apply_common_filters(builder, user_id, guardian) @@ -91,48 +92,82 @@ SQL stream(action_id: action_id, guardian: guardian).first end - def self.stream(opts={}) - user_id = opts[:user_id] + def self.stream_queued(opts=nil) + opts ||= {} + offset = opts[:offset] || 0 limit = opts[:limit] || 60 - action_id = opts[:action_id] + + builder = SqlBuilder.new <<-SQL + SELECT + a.id, + t.title, a.action_type, a.created_at, t.id topic_id, + u.username, u.name, u.id AS user_id, + qp.raw, + t.category_id + FROM user_actions as a + JOIN queued_posts AS qp ON qp.id = a.queued_post_id + LEFT OUTER JOIN topics t on t.id = qp.topic_id + JOIN users u on u.id = a.user_id + LEFT JOIN categories c on c.id = t.category_id + /*where*/ + /*order_by*/ + /*offset*/ + /*limit*/ + SQL + + builder + .where('a.user_id = :user_id', user_id: opts[:user_id].to_i) + .where('action_type = :pending', pending: UserAction::PENDING) + .order_by("a.created_at desc") + .offset(offset.to_i) + .limit(limit.to_i) + .map_exec(UserActionRow) + end + + def self.stream(opts=nil) + opts ||= {} + action_types = opts[:action_types] + user_id = opts[:user_id] + action_id = opts[:action_id] guardian = opts[:guardian] ignore_private_messages = opts[:ignore_private_messages] + offset = opts[:offset] || 0 + limit = opts[:limit] || 60 # The weird thing is that target_post_id can be null, so it makes everything # ever so more complex. Should we allow this, not sure. - - builder = SqlBuilder.new(" -SELECT - a.id, - t.title, a.action_type, a.created_at, t.id topic_id, - a.user_id AS target_user_id, au.name AS target_name, au.username AS target_username, - coalesce(p.post_number, 1) post_number, p.id as post_id, - p.reply_to_post_number, - pu.email, pu.username, pu.name, pu.id user_id, - pu.uploaded_avatar_id, - u.email acting_email, u.username acting_username, u.name acting_name, u.id acting_user_id, - u.uploaded_avatar_id acting_uploaded_avatar_id, - coalesce(p.cooked, p2.cooked) cooked, - CASE WHEN coalesce(p.deleted_at, p2.deleted_at, t.deleted_at) IS NULL THEN false ELSE true END deleted, - p.hidden, - p.post_type, - p.edit_reason, - t.category_id -FROM user_actions as a -JOIN topics t on t.id = a.target_topic_id -LEFT JOIN posts p on p.id = a.target_post_id -JOIN posts p2 on p2.topic_id = a.target_topic_id and p2.post_number = 1 -JOIN users u on u.id = a.acting_user_id -JOIN users pu on pu.id = COALESCE(p.user_id, t.user_id) -JOIN users au on au.id = a.user_id -LEFT JOIN categories c on c.id = t.category_id -/*where*/ -/*order_by*/ -/*offset*/ -/*limit*/ -") + builder = SqlBuilder.new <<-SQL + SELECT + a.id, + t.title, a.action_type, a.created_at, t.id topic_id, + a.user_id AS target_user_id, au.name AS target_name, au.username AS target_username, + coalesce(p.post_number, 1) post_number, p.id as post_id, + p.reply_to_post_number, + pu.username, pu.name, pu.id user_id, + pu.uploaded_avatar_id, + u.username acting_username, u.name acting_name, u.id acting_user_id, + u.uploaded_avatar_id acting_uploaded_avatar_id, + coalesce(p.cooked, p2.cooked) cooked, + CASE WHEN coalesce(p.deleted_at, p2.deleted_at, t.deleted_at) IS NULL THEN false ELSE true END deleted, + p.hidden, + p.post_type, + p.edit_reason, + t.category_id + FROM user_actions as a + JOIN topics t on t.id = a.target_topic_id + LEFT JOIN posts p on p.id = a.target_post_id + JOIN posts p2 on p2.topic_id = a.target_topic_id and p2.post_number = 1 + JOIN users u on u.id = a.acting_user_id + JOIN users pu on pu.id = COALESCE(p.user_id, t.user_id) + JOIN users au on au.id = a.user_id + LEFT JOIN categories c on c.id = t.category_id + /*where*/ + /*order_by*/ + /*offset*/ + /*limit*/ + SQL apply_common_filters(builder, user_id, guardian, ignore_private_messages) @@ -151,7 +186,15 @@ LEFT JOIN categories c on c.id = t.category_id end def self.log_action!(hash) - required_parameters = [:action_type, :user_id, :acting_user_id, :target_topic_id, :target_post_id] + required_parameters = [:action_type, :user_id, :acting_user_id] + + if hash[:action_type] == UserAction::PENDING + required_parameters << :queued_post_id + else + required_parameters << :target_post_id + required_parameters << :target_topic_id + end + require_parameters(hash, *required_parameters) transaction(requires_new: true) do @@ -269,6 +312,10 @@ SQL builder.where("t.visible") end + unless guardian.can_see_notifications?(User.where(id: user_id).first) + builder.where('a.action_type <> :pending', pending: UserAction::PENDING) + end + if !guardian.can_see_private_messages?(user_id) || ignore_private_messages builder.where("t.archetype != :archetype", archetype: Archetype::private_message) end diff --git a/app/serializers/user_action_serializer.rb b/app/serializers/user_action_serializer.rb index 188ed1b69d6..503aabe626d 100644 --- a/app/serializers/user_action_serializer.rb +++ b/app/serializers/user_action_serializer.rb @@ -29,7 +29,8 @@ class UserActionSerializer < ApplicationSerializer :acting_uploaded_avatar_id def excerpt - PrettyText.excerpt(object.cooked, 300) if object.cooked + cooked = object.cooked || PrettyText.cook(object.raw) + PrettyText.excerpt(cooked, 300) if cooked end def avatar_template @@ -40,6 +41,10 @@ class UserActionSerializer < ApplicationSerializer User.avatar_template(object.acting_username, object.acting_uploaded_avatar_id) end + def include_acting_avatar_template? + object.acting_username.present? + end + def include_name? SiteSetting.enable_names? end @@ -56,6 +61,10 @@ class UserActionSerializer < ApplicationSerializer Slug.for(object.title) end + def include_slug? + object.title.present? + end + def moderator_action object.post_type == Post.types[:moderator_action] end diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb index 350b0262018..f9ba6563b62 100644 --- a/app/serializers/user_serializer.rb +++ b/app/serializers/user_serializer.rb @@ -64,7 +64,8 @@ class UserSerializer < BasicUserSerializer :edit_history_public, :custom_fields, :user_fields, - :topic_post_count + :topic_post_count, + :pending_count has_one :invited_by, embed: :object, serializer: BasicUserSerializer has_many :custom_groups, embed: :object, serializer: BasicGroupSerializer @@ -312,4 +313,8 @@ class UserSerializer < BasicUserSerializer {} end end + + def pending_count + 0 + end end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 309cc13d91a..79eddd24ce6 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -308,6 +308,7 @@ en: "11": "Edits" "12": "Sent Items" "13": "Inbox" + "14": "Pending" categories: all: "all categories" diff --git a/config/routes.rb b/config/routes.rb index 536a0929afc..d71ecc3c5bb 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -270,6 +270,7 @@ Discourse::Application.routes.draw do get "users/:username/activity/:filter" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT} get "users/:username/badges" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT} get "users/:username/notifications" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT} + get "users/:username/pending" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT} delete "users/:username" => "users#destroy", constraints: {username: USERNAME_ROUTE_FORMAT} get "users/by-external/:external_id" => "users#show" get "users/:username/flagged-posts" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT} diff --git a/db/migrate/20150421190714_add_queued_post_id_to_user_actions.rb b/db/migrate/20150421190714_add_queued_post_id_to_user_actions.rb new file mode 100644 index 00000000000..d6987afc253 --- /dev/null +++ b/db/migrate/20150421190714_add_queued_post_id_to_user_actions.rb @@ -0,0 +1,5 @@ +class AddQueuedPostIdToUserActions < ActiveRecord::Migration + def change + add_column :user_actions, :queued_post_id, :integer, null: true + end +end diff --git a/lib/post_enqueuer.rb b/lib/post_enqueuer.rb index 78242d405d1..64a6accc501 100644 --- a/lib/post_enqueuer.rb +++ b/lib/post_enqueuer.rb @@ -23,7 +23,16 @@ class PostEnqueuer return unless send(validate_method, queued_post.create_options) end - add_errors_from(queued_post) unless queued_post.save + if queued_post.save + UserAction.log_action!(action_type: UserAction::PENDING, + user_id: @user.id, + acting_user_id: @user.id, + target_topic_id: args[:topic_id], + queued_post_id: queued_post.id) + else + add_errors_from(queued_post) + end + queued_post end diff --git a/spec/components/post_enqueuer_spec.rb b/spec/components/post_enqueuer_spec.rb index 0c9d5a1dee1..964ba891bcf 100644 --- a/spec/components/post_enqueuer_spec.rb +++ b/spec/components/post_enqueuer_spec.rb @@ -10,6 +10,9 @@ describe PostEnqueuer do let(:enqueuer) { PostEnqueuer.new(user, 'new_post') } it 'enqueues the post' do + + old_count = user.user_actions.count + qp = enqueuer.enqueue(raw: 'This should be enqueued', topic_id: topic.id, post_options: { reply_to_post_number: 1 }) @@ -18,6 +21,7 @@ describe PostEnqueuer do expect(qp).to be_present expect(qp.topic).to eq(topic) expect(qp.user).to eq(user) + expect(UserAction.where(user_id: user.id).count).to eq(old_count + 1) end end diff --git a/spec/controllers/user_actions_controller_spec.rb b/spec/controllers/user_actions_controller_spec.rb index c119ad3beaa..38be9207647 100644 --- a/spec/controllers/user_actions_controller_spec.rb +++ b/spec/controllers/user_actions_controller_spec.rb @@ -1,4 +1,5 @@ require 'spec_helper' +require_dependency 'post_enqueuer' describe UserActionsController do context 'index' do @@ -22,5 +23,35 @@ describe UserActionsController do expect(action["email"]).to eq(nil) expect(action["post_number"]).to eq(1) end + + context "queued posts" do + context "without access" do + let(:user) { Fabricate(:user) } + it "raises an exception" do + xhr :get, :index, username: user.username, filter: UserAction::PENDING + expect(response).to_not be_success + + end + end + + context "with access" do + let(:user) { log_in } + + it 'finds queued posts' do + queued_post = PostEnqueuer.new(user, 'default').enqueue(raw: 'this is the raw enqueued content') + + xhr :get, :index, username: user.username, filter: UserAction::PENDING + + expect(response.status).to eq(200) + parsed = JSON.parse(response.body) + actions = parsed["user_actions"] + expect(actions.length).to eq(1) + + action = actions.first + expect(action['username']).to eq(user.username) + expect(action['excerpt']).to be_present + end + end + end end end diff --git a/test/javascripts/acceptance/user-anonymous-test.js.es6 b/test/javascripts/acceptance/user-anonymous-test.js.es6 new file mode 100644 index 00000000000..85b606ba62c --- /dev/null +++ b/test/javascripts/acceptance/user-anonymous-test.js.es6 @@ -0,0 +1,41 @@ +import { acceptance } from "helpers/qunit-helpers"; +acceptance("User Anonymous"); + +export function hasStream() { + andThen(() => { + ok(exists('.user-main .about'), 'it has the about section'); + ok(count('.user-stream .item') > 0, 'it has stream items'); + }); +} + +function hasTopicList() { + andThen(() => { + equal(count('.user-stream .item'), 0, "has no stream displayed"); + ok(count('.topic-list tr') > 0, 'it has a topic list'); + }); +} + +test("Filters", () => { + expect(14); + + visit("/users/eviltrout"); + hasStream(); + + visit("/users/eviltrout/activity/topics"); + hasTopicList(); + + visit("/users/eviltrout/activity/posts"); + hasStream(); + + visit("/users/eviltrout/activity/replies"); + hasStream(); + + visit("/users/eviltrout/activity/likes-given"); + hasStream(); + + visit("/users/eviltrout/activity/likes-received"); + hasStream(); + + visit("/users/eviltrout/activity/edits"); + hasStream(); +}); diff --git a/test/javascripts/acceptance/user-test.js.es6 b/test/javascripts/acceptance/user-test.js.es6 index 9d38301ff1b..e642d2aad47 100644 --- a/test/javascripts/acceptance/user-test.js.es6 +++ b/test/javascripts/acceptance/user-test.js.es6 @@ -1,41 +1,9 @@ import { acceptance } from "helpers/qunit-helpers"; -acceptance("User"); +import { hasStream } from 'acceptance/user-anonymous-test'; -function hasStream() { - andThen(() => { - ok(exists('.user-main .about'), 'it has the about section'); - ok(count('.user-stream .item') > 0, 'it has stream items'); - }); -} +acceptance("User", {loggedIn: true}); -function hasTopicList() { - andThen(() => { - equal(count('.user-stream .item'), 0, "has no stream displayed"); - ok(count('.topic-list tr') > 0, 'it has a topic list'); - }); -} - -test("Filters", () => { - expect(14); - - visit("/users/eviltrout"); - hasStream(); - - visit("/users/eviltrout/activity/topics"); - hasTopicList(); - - visit("/users/eviltrout/activity/posts"); - hasStream(); - - visit("/users/eviltrout/activity/replies"); - hasStream(); - - visit("/users/eviltrout/activity/likes-given"); - hasStream(); - - visit("/users/eviltrout/activity/likes-received"); - hasStream(); - - visit("/users/eviltrout/activity/edits"); +test("Pending", () => { + visit("/users/eviltrout/activity/pending"); hasStream(); }); diff --git a/test/javascripts/helpers/create-pretender.js.es6 b/test/javascripts/helpers/create-pretender.js.es6 index b29a618e8c8..563e1ab137f 100644 --- a/test/javascripts/helpers/create-pretender.js.es6 +++ b/test/javascripts/helpers/create-pretender.js.es6 @@ -101,6 +101,8 @@ export default function() { this.delete('/draft.json', success); + this.get('/users/:username/staff-info.json', () => response({})); + this.get('/draft.json', function() { return response({}); });