Users can see their pending posts

This commit is contained in:
Robin Ward 2015-04-21 14:36:46 -04:00
parent 26693c16ac
commit 5bf8c31af4
17 changed files with 225 additions and 101 deletions

View File

@ -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() {

View File

@ -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 = {};

View File

@ -0,0 +1,5 @@
import UserActivityStreamRoute from "discourse/routes/user-activity-stream";
export default UserActivityStreamRoute.extend({
userActionType: Discourse.UserAction.TYPES.pending
});

View File

@ -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}}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -308,6 +308,7 @@ en:
"11": "Edits"
"12": "Sent Items"
"13": "Inbox"
"14": "Pending"
categories:
all: "all categories"

View File

@ -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}

View File

@ -0,0 +1,5 @@
class AddQueuedPostIdToUserActions < ActiveRecord::Migration
def change
add_column :user_actions, :queued_post_id, :integer, null: true
end
end

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

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

View File

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