mirror of
https://github.com/discourse/discourse.git
synced 2025-03-21 06:55:41 +08:00
FEATURE: show the user's flagged/deleted posts
This commit is contained in:
parent
71c67c43a1
commit
7dcf2a2c4f
48
app/assets/javascripts/discourse/models/admin_post.js
Normal file
48
app/assets/javascripts/discourse/models/admin_post.js
Normal file
@ -0,0 +1,48 @@
|
||||
/**
|
||||
A data model for flagged/deleted posts.
|
||||
|
||||
@class AdminPost
|
||||
@extends Discourse.Post
|
||||
@namespace Discourse
|
||||
@module Discourse
|
||||
**/
|
||||
Discourse.AdminPost = Discourse.Post.extend({
|
||||
|
||||
_attachCategory: function () {
|
||||
var categoryId = this.get("category_id");
|
||||
if (categoryId) {
|
||||
this.set("category", Discourse.Category.findById(categoryId));
|
||||
}
|
||||
}.on("init"),
|
||||
|
||||
presentName: Em.computed.any('name', 'username'),
|
||||
|
||||
sameUser: function() {
|
||||
return this.get("username") === Discourse.User.currentProp("username");
|
||||
}.property("username"),
|
||||
|
||||
descriptionKey: function () {
|
||||
if (this.get("reply_to_post_number")) {
|
||||
return this.get("sameUser") ? "you_replied_to_post" : "user_replied_to_post";
|
||||
} else {
|
||||
return this.get("sameUser") ? "you_replied_to_topic" : "user_replied_to_topic";
|
||||
}
|
||||
}.property("reply_to_post_number", "sameUser"),
|
||||
|
||||
descriptionHtml: function () {
|
||||
var descriptionKey = this.get("descriptionKey");
|
||||
if (!descriptionKey) { return; }
|
||||
|
||||
var description = I18n.t("user_action." + descriptionKey, {
|
||||
userUrl: this.get("usernameUrl"),
|
||||
user: Handlebars.Utils.escapeExpression(this.get("presentName")),
|
||||
postUrl: this.get("url"),
|
||||
post_number: "#" + this.get("reply_to_post_number"),
|
||||
topicUrl: this.get("url"),
|
||||
});
|
||||
|
||||
return new Handlebars.SafeString(description);
|
||||
|
||||
}.property("descriptionKey")
|
||||
|
||||
});
|
@ -22,6 +22,16 @@ Discourse.User = Discourse.Model.extend({
|
||||
return Discourse.UserStream.create({ user: this });
|
||||
}.property(),
|
||||
|
||||
/**
|
||||
The user's posts stream
|
||||
|
||||
@property postsStream
|
||||
@type {Discourse.UserPostsStream}
|
||||
**/
|
||||
postsStream: function() {
|
||||
return Discourse.UserPostsStream.create({ user: this });
|
||||
}.property(),
|
||||
|
||||
/**
|
||||
Is this user a member of staff?
|
||||
|
||||
|
53
app/assets/javascripts/discourse/models/user_posts_stream.js
Normal file
53
app/assets/javascripts/discourse/models/user_posts_stream.js
Normal file
@ -0,0 +1,53 @@
|
||||
/**
|
||||
Represents a user's stream
|
||||
|
||||
@class UserPostsStream
|
||||
@extends Discourse.Model
|
||||
@namespace Discourse
|
||||
@module Discourse
|
||||
**/
|
||||
Discourse.UserPostsStream = Discourse.Model.extend({
|
||||
loaded: false,
|
||||
|
||||
_initialize: function () {
|
||||
this.setProperties({
|
||||
itemsLoaded: 0,
|
||||
content: []
|
||||
});
|
||||
}.on("init"),
|
||||
|
||||
url: Discourse.computed.url("user.username_lower", "filter", "itemsLoaded", "/posts/%@/%@?offset=%@"),
|
||||
|
||||
filterBy: function (filter) {
|
||||
if (this.get("loaded") && this.get("filter") === filter) { return Ember.RSVP.resolve(); }
|
||||
|
||||
this.setProperties({
|
||||
filter: filter,
|
||||
itemsLoaded: 0,
|
||||
content: []
|
||||
});
|
||||
|
||||
return this.findItems();
|
||||
},
|
||||
|
||||
findItems: function () {
|
||||
var self = this;
|
||||
if (this.get("loading")) { return Ember.RSVP.reject(); }
|
||||
|
||||
this.set("loading", true);
|
||||
|
||||
return Discourse.ajax(this.get("url"), { cache: false }).then(function (result) {
|
||||
if (result) {
|
||||
var posts = result.map(function (post) { return Discourse.AdminPost.create(post); });
|
||||
self.get("content").pushObjects(posts);
|
||||
self.setProperties({
|
||||
loaded: true,
|
||||
itemsLoaded: self.get("itemsLoaded") + posts.length
|
||||
});
|
||||
}
|
||||
}).finally(function () {
|
||||
self.set("loading", false);
|
||||
});
|
||||
}
|
||||
|
||||
});
|
@ -9,9 +9,12 @@
|
||||
Discourse.UserStream = Discourse.Model.extend({
|
||||
loaded: false,
|
||||
|
||||
init: function() {
|
||||
this.setProperties({ itemsLoaded: 0, content: [] });
|
||||
},
|
||||
_initialize: function() {
|
||||
this.setProperties({
|
||||
itemsLoaded: 0,
|
||||
content: []
|
||||
});
|
||||
}.on("init"),
|
||||
|
||||
filterParam: function() {
|
||||
var filter = this.get('filter');
|
||||
@ -33,6 +36,7 @@ Discourse.UserStream = Discourse.Model.extend({
|
||||
itemsLoaded: 0,
|
||||
content: []
|
||||
});
|
||||
|
||||
return this.findItems();
|
||||
},
|
||||
|
||||
|
@ -73,6 +73,8 @@ Discourse.Route.buildRoutes(function() {
|
||||
});
|
||||
|
||||
this.route('badges');
|
||||
this.route('flaggedPosts', { path: '/flagged-posts' });
|
||||
this.route('deletedPosts', { path: '/deleted-posts' });
|
||||
|
||||
this.resource('userPrivateMessages', { path: '/private-messages' }, function() {
|
||||
this.route('mine');
|
||||
|
@ -0,0 +1,23 @@
|
||||
function createAdminPostRoute (filter) {
|
||||
return Discourse.Route.extend({
|
||||
model: function () {
|
||||
return this.modelFor("user").get("postsStream");
|
||||
},
|
||||
|
||||
afterModel: function () {
|
||||
return this.modelFor("user").get("postsStream").filterBy(filter);
|
||||
},
|
||||
|
||||
setupController: function (controller, model) {
|
||||
controller.set("model", model);
|
||||
this.controllerFor("user").set("indexStream", true);
|
||||
},
|
||||
|
||||
renderTemplate: function() {
|
||||
this.render("user/posts", { into: "user", outlet: "userOutlet" });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Discourse.UserDeletedPostsRoute = createAdminPostRoute("deleted");
|
||||
Discourse.UserFlaggedPostsRoute = createAdminPostRoute("flagged");
|
@ -0,0 +1,32 @@
|
||||
{{#each model.content}}
|
||||
<div {{bind-attr class=":item hidden deleted moderator_action"}}>
|
||||
<div class="clearfix info">
|
||||
<a href="{{unbound usernameUrl}}" class="avatar-link">
|
||||
<div class="avatar-wrapper">
|
||||
{{avatar this imageSize="large" extraClasses="actor" ignoreTitle="true"}}
|
||||
</div>
|
||||
</a>
|
||||
<span class="time">
|
||||
{{date path="created_at" leaveAgo="true"}}
|
||||
</span>
|
||||
<span class="title">
|
||||
<a href="{{unbound url}}">{{unbound topic_title}}</a>
|
||||
{{category-link category}}
|
||||
</span>
|
||||
<span class="type">
|
||||
{{descriptionHtml}}
|
||||
</span>
|
||||
{{#if deleted}}
|
||||
<span class="time">
|
||||
{{i18n post.deleted_by}} {{avatar deleted_by imageSize="tiny" extraClasses="actor" ignoreTitle="true"}} {{date path="deleted_at" leaveAgo="true"}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<p class="excerpt">
|
||||
{{{excerpt}}}
|
||||
</p>
|
||||
</div>
|
||||
{{/each}}
|
||||
{{#if loading}}
|
||||
<div class='spinner'>{{i18n loading}}</div>
|
||||
{{/if}}
|
@ -27,3 +27,6 @@
|
||||
{{/groupedEach}}
|
||||
</div>
|
||||
{{/groupedEach}}
|
||||
{{#if loading}}
|
||||
<div class='spinner'>{{i18n loading}}</div>
|
||||
{{/if}}
|
||||
|
@ -69,10 +69,17 @@
|
||||
<div><span class="pill helpful-flags">{{number_of_flags_given}}</span> {{i18n user.staff_counters.flags_given}}</div>
|
||||
{{/if}}
|
||||
{{#if number_of_flagged_posts}}
|
||||
<div><span class="pill flagged-posts">{{number_of_flagged_posts}}</span> {{i18n user.staff_counters.flagged_posts}}</div>
|
||||
<div>
|
||||
{{#link-to 'user.flaggedPosts' this}}
|
||||
<span class="pill flagged-posts">{{number_of_flagged_posts}}</span> {{i18n user.staff_counters.flagged_posts}}</div>
|
||||
{{/link-to}}
|
||||
{{/if}}
|
||||
{{#if number_of_deleted_posts}}
|
||||
<div><span class="pill deleted-posts">{{number_of_deleted_posts}}</span> {{i18n user.staff_counters.deleted_posts}}</div>
|
||||
<div>
|
||||
{{#link-to 'user.deletedPosts' this}}
|
||||
<span class="pill deleted-posts">{{number_of_deleted_posts}}</span> {{i18n user.staff_counters.deleted_posts}}
|
||||
{{/link-to}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if number_of_suspensions}}
|
||||
<div><span class="pill suspensions">{{number_of_suspensions}}</span> {{i18n user.staff_counters.suspensions}}</div>
|
||||
|
@ -0,0 +1,27 @@
|
||||
/**
|
||||
This view handles rendering of a user's posts
|
||||
|
||||
@class UserPostsView
|
||||
@extends Discourse.View
|
||||
@namespace Discourse
|
||||
@uses Discourse.LoadMore
|
||||
@module Discourse
|
||||
**/
|
||||
Discourse.UserPostsView = Discourse.View.extend(Discourse.LoadMore, {
|
||||
loading: false,
|
||||
eyelineSelector: ".user-stream .item",
|
||||
classNames: ["user-stream"],
|
||||
|
||||
actions: {
|
||||
loadMore: function() {
|
||||
var self = this;
|
||||
if (this.get("loading")) { return; }
|
||||
|
||||
var postsStream = this.get("controller.model");
|
||||
postsStream.findItems().then(function () {
|
||||
self.set("loading", false);
|
||||
self.get("eyeline").flushRest();
|
||||
}).catch(function () { });
|
||||
}
|
||||
}
|
||||
});
|
@ -364,6 +364,9 @@
|
||||
> div {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
a.active {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.pill {
|
||||
|
@ -244,6 +244,37 @@ class PostsController < ApplicationController
|
||||
render nothing: true
|
||||
end
|
||||
|
||||
def flagged_posts
|
||||
params.permit(:offset, :limit)
|
||||
guardian.ensure_can_see_flagged_posts!
|
||||
|
||||
user = fetch_user_from_params
|
||||
offset = [params[:offset].to_i, 0].max
|
||||
limit = [(params[:limit] || 60).to_i, 100].min
|
||||
|
||||
posts = user_posts(user.id, offset, limit)
|
||||
.where(id: PostAction.with_deleted
|
||||
.where(post_action_type_id: PostActionType.notify_flag_type_ids)
|
||||
.select(:post_id))
|
||||
|
||||
render_serialized(posts, AdminPostSerializer)
|
||||
end
|
||||
|
||||
def deleted_posts
|
||||
params.permit(:offset, :limit)
|
||||
guardian.ensure_can_see_deleted_posts!
|
||||
|
||||
user = fetch_user_from_params
|
||||
offset = [params[:offset].to_i, 0].max
|
||||
limit = [(params[:limit] || 60).to_i, 100].min
|
||||
|
||||
posts = user_posts(user.id, offset, limit)
|
||||
.where(user_deleted: false)
|
||||
.where.not(deleted_by_id: user.id)
|
||||
|
||||
render_serialized(posts, AdminPostSerializer)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def find_post_revision_from_params
|
||||
@ -272,6 +303,15 @@ class PostsController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def user_posts(user_id, offset=0, limit=60)
|
||||
Post.includes(:user, :topic, :deleted_by, :user_actions)
|
||||
.with_deleted
|
||||
.where(user_id: user_id)
|
||||
.order(created_at: :desc)
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
end
|
||||
|
||||
def params_key(params)
|
||||
"post##" << Digest::SHA1.hexdigest(params
|
||||
.to_a
|
||||
|
@ -23,6 +23,7 @@ class Post < ActiveRecord::Base
|
||||
|
||||
belongs_to :user
|
||||
belongs_to :topic, counter_cache: :posts_count
|
||||
|
||||
belongs_to :reply_to_user, class_name: "User"
|
||||
|
||||
has_many :post_replies
|
||||
@ -40,6 +41,8 @@ class Post < ActiveRecord::Base
|
||||
has_many :post_revisions
|
||||
has_many :revisions, foreign_key: :post_id, class_name: 'PostRevision'
|
||||
|
||||
has_many :user_actions, foreign_key: :target_post_id
|
||||
|
||||
validates_with ::Validators::PostValidator
|
||||
|
||||
# We can pass several creating options to a post via attributes
|
||||
|
76
app/serializers/admin_post_serializer.rb
Normal file
76
app/serializers/admin_post_serializer.rb
Normal file
@ -0,0 +1,76 @@
|
||||
class AdminPostSerializer < ApplicationSerializer
|
||||
|
||||
attributes :id,
|
||||
:created_at,
|
||||
:post_number,
|
||||
:name, :username, :avatar_template, :uploaded_avatar_id,
|
||||
:topic_id, :topic_slug, :topic_title,
|
||||
:category_id,
|
||||
:excerpt,
|
||||
:hidden,
|
||||
:moderator_action,
|
||||
:deleted_at, :deleted_by,
|
||||
:reply_to_post_number,
|
||||
:action_type
|
||||
|
||||
def name
|
||||
object.user.name
|
||||
end
|
||||
|
||||
def include_name?
|
||||
SiteSetting.enable_names?
|
||||
end
|
||||
|
||||
def username
|
||||
object.user.username
|
||||
end
|
||||
|
||||
def avatar_template
|
||||
object.user.avatar_template
|
||||
end
|
||||
|
||||
def uploaded_avatar_id
|
||||
object.user.uploaded_avatar_id
|
||||
end
|
||||
|
||||
def topic_slug
|
||||
topic.slug
|
||||
end
|
||||
|
||||
def topic_title
|
||||
topic.title
|
||||
end
|
||||
|
||||
def category_id
|
||||
topic.category_id
|
||||
end
|
||||
|
||||
def moderator_action
|
||||
object.post_type == Post.types[:moderator_action]
|
||||
end
|
||||
|
||||
def deleted_by
|
||||
BasicUserSerializer.new(object.deleted_by, root: false).as_json
|
||||
end
|
||||
|
||||
def include_deleted_by?
|
||||
object.trashed?
|
||||
end
|
||||
|
||||
def action_type
|
||||
object.user_actions.select { |ua| ua.user_id = object.user_id }
|
||||
.select { |ua| [UserAction::REPLY, UserAction::RESPONSE].include? ua.action_type }
|
||||
.first.try(:action_type)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# we need this to handle deleted topics which aren't loaded via the .includes(:topic)
|
||||
# because Rails 4 "unscoped" support is bugged (cf. https://github.com/rails/rails/issues/13775)
|
||||
def topic
|
||||
return @topic if @topic
|
||||
@topic = object.topic || Topic.with_deleted.find(object.topic_id)
|
||||
@topic
|
||||
end
|
||||
|
||||
end
|
@ -218,6 +218,8 @@ Discourse::Application.routes.draw do
|
||||
get "users/:username/badges" => "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}
|
||||
get "users/:username/deleted-posts" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT}
|
||||
|
||||
post "user_avatar/:username/refresh_gravatar" => "user_avatars#refresh_gravatar"
|
||||
get "letter_avatar/:username/:size/:version.png" => "user_avatars#show_letter",
|
||||
@ -231,6 +233,8 @@ Discourse::Application.routes.draw do
|
||||
|
||||
get "posts/by_number/:topic_id/:post_number" => "posts#by_number"
|
||||
get "posts/:id/reply-history" => "posts#reply_history"
|
||||
get "posts/:username/deleted" => "posts#deleted_posts", constraints: {username: USERNAME_ROUTE_FORMAT}
|
||||
get "posts/:username/flagged" => "posts#flagged_posts", constraints: {username: USERNAME_ROUTE_FORMAT}
|
||||
|
||||
resources :groups do
|
||||
get 'members'
|
||||
|
@ -156,4 +156,12 @@ module PostGuardian
|
||||
def can_wiki?
|
||||
is_staff? || @user.has_trust_level?(:elder)
|
||||
end
|
||||
|
||||
def can_see_flagged_posts?
|
||||
is_staff?
|
||||
end
|
||||
|
||||
def can_see_deleted_posts?
|
||||
is_staff?
|
||||
end
|
||||
end
|
||||
|
@ -606,4 +606,51 @@ describe PostsController do
|
||||
::JSON.parse(response.body)['cooked'].should == "full content"
|
||||
end
|
||||
end
|
||||
|
||||
describe "flagged posts" do
|
||||
|
||||
include_examples "action requires login", :get, :flagged_posts, username: "system"
|
||||
|
||||
describe "when logged in" do
|
||||
before { log_in }
|
||||
|
||||
it "raises an error if the user doesn't have permission to see the flagged posts" do
|
||||
Guardian.any_instance.expects(:can_see_flagged_posts?).returns(false)
|
||||
xhr :get, :flagged_posts, username: "system"
|
||||
response.should be_forbidden
|
||||
end
|
||||
|
||||
it "can see the flagged posts when authorized" do
|
||||
Guardian.any_instance.expects(:can_see_flagged_posts?).returns(true)
|
||||
xhr :get, :flagged_posts, username: "system"
|
||||
response.should be_success
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe "deleted posts" do
|
||||
|
||||
include_examples "action requires login", :get, :deleted_posts, username: "system"
|
||||
|
||||
describe "when logged in" do
|
||||
before { log_in }
|
||||
|
||||
it "raises an error if the user doesn't have permission to see the deleted posts" do
|
||||
Guardian.any_instance.expects(:can_see_deleted_posts?).returns(false)
|
||||
xhr :get, :deleted_posts, username: "system"
|
||||
response.should be_forbidden
|
||||
end
|
||||
|
||||
it "can see the deleted posts when authorized" do
|
||||
Guardian.any_instance.expects(:can_see_deleted_posts?).returns(true)
|
||||
xhr :get, :deleted_posts, username: "system"
|
||||
response.should be_success
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
Loading…
x
Reference in New Issue
Block a user