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({});
});