Users can see their pending posts
This commit is contained in:
parent
26693c16ac
commit
5bf8c31af4
|
@ -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() {
|
||||
|
|
|
@ -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 = {};
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
import UserActivityStreamRoute from "discourse/routes/user-activity-stream";
|
||||
|
||||
export default UserActivityStreamRoute.extend({
|
||||
userActionType: Discourse.UserAction.TYPES.pending
|
||||
});
|
|
@ -168,7 +168,7 @@
|
|||
{{#if canSeeNotificationHistory}}
|
||||
{{#link-to 'user.notifications' tagName="li"}}
|
||||
{{#link-to 'user.notifications'}}
|
||||
<i class='glyph fa fa-comment'></i>
|
||||
{{fa-icon "comment" class="glyph"}}
|
||||
{{i18n 'user.notifications'}}
|
||||
<span class='count'>({{notification_count}})</span>
|
||||
{{/link-to}}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -308,6 +308,7 @@ en:
|
|||
"11": "Edits"
|
||||
"12": "Sent Items"
|
||||
"13": "Inbox"
|
||||
"14": "Pending"
|
||||
|
||||
categories:
|
||||
all: "all categories"
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
class AddQueuedPostIdToUserActions < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :user_actions, :queued_post_id, :integer, null: true
|
||||
end
|
||||
end
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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({});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue