Interface for reviewing queued posts

This commit is contained in:
Robin Ward 2015-04-10 17:00:50 -04:00
parent f1ede42569
commit 96d2c5069b
21 changed files with 219 additions and 17 deletions

View File

@ -0,0 +1,16 @@
export default Ember.Controller.extend({
actions: {
approve(post) {
post.update({ state: 'approved' }).then(() => {
this.get('model').removeObject(post);
});
},
reject(post) {
post.update({ state: 'rejected' }).then(() => {
this.get('model').removeObject(post);
});
}
}
});

View File

@ -0,0 +1,6 @@
import registerUnbound from 'discourse/helpers/register-unbound';
registerUnbound('cook-text', function(text) {
return new Handlebars.SafeString(Discourse.Markdown.cook(text));
});

View File

@ -93,4 +93,6 @@ export default function() {
this.resource('badges', function() {
this.route('show', {path: '/:id/:slug'});
});
this.resource('queued-posts', { path: '/queued-posts' });
}

View File

@ -0,0 +1,8 @@
import DiscourseRoute from 'discourse/routes/discourse';
export default DiscourseRoute.extend({
model() {
return this.store.find('queuedPost', {status: 'new'});
}
});

View File

@ -0,0 +1,30 @@
<div class='container'>
<div class='queued-posts'>
{{#each post in model}}
<div class='queued-post'>
{{#if post.title}}
<h4 class='title'>{{post.title}}</h4>
{{/if}}
<div class='poster'>
{{avatar post.user imageSize="large"}}
</div>
<div class='cooked'>
<div class='names'>
<span class='username'>{{post.user.username}}</span>
</div>
<div class='clearfix'></div>
{{{cook-text post.raw}}}
<div class='queue-controls'>
{{d-button action="approve" actionParam=post label="queue.approve" icon="check" class="btn-primary approve"}}
{{d-button action="reject" actionParam=post label="queue.reject" icon="times" class="btn-warning reject"}}
</div>
</div>
<div class='clearfix'></div>
</div>
{{else}}
<p>{{i18n "queue.none"}}</p>
{{/each}}
</div>
</div>

View File

@ -28,12 +28,12 @@
{{#if currentUser.staff}}
<li>
<a href="/queued-posts">
{{#link-to 'queued-posts'}}
{{i18n "queue.title"}}
{{#if currentUser.post_queue_new_count}}
<span class='badge-notification flagged-posts'>{{currentUser.post_queue_new_count}}</span>
{{/if}}
</a>
{{/link-to}}
</li>
{{/if}}

View File

@ -16,6 +16,7 @@
@import "desktop/upload";
@import "desktop/user";
@import "desktop/history";
@import "desktop/queued-posts";
/* These files doesn't actually exist, they are injected by DiscourseSassImporter. */

View File

@ -0,0 +1,20 @@
.queued-posts {
.queued-post {
padding: 1em 0;
.poster {
width: 70px;
float: left;
}
.cooked {
width: $topic-body-width;
float: left;
}
h4.title {
margin-bottom: 1em;
}
border-bottom: 1px solid darken(scale-color-diff(), 10%);
}
}

View File

@ -7,11 +7,4 @@ class Admin::AdminController < ApplicationController
render nothing: true
end
protected
# this is not really necessary cause the routes are secure
def ensure_staff
raise Discourse::InvalidAccess.new unless current_user.staff?
end
end

View File

@ -371,6 +371,10 @@ class ApplicationController < ActionController::Base
raise Discourse::NotLoggedIn.new unless current_user.present?
end
def ensure_staff
raise Discourse::InvalidAccess.new unless current_user && current_user.staff?
end
def redirect_to_login_if_required
return if current_user || (request.format.json? && api_key_valid?)

View File

@ -0,0 +1,20 @@
require_dependency 'queued_post_serializer'
class QueuedPostsController < ApplicationController
before_filter :ensure_staff
def index
state = QueuedPost.states[(params[:state] || 'new').to_sym]
state ||= QueuedPost.states[:new]
@queued_posts = QueuedPost.where(state: state)
render_serialized(@queued_posts, QueuedPostSerializer, root: :queued_posts)
end
def update
qp = QueuedPost.where(id: params[:id]).first
render_serialized(qp, QueuedPostSerializer, root: :queued_posts)
end
end

View File

@ -30,7 +30,7 @@ class QueuedPost < ActiveRecord::Base
where(state: states[:new]).count
end
def self.publish_new!
def self.broadcast_new!
msg = { post_queue_new_count: QueuedPost.new_count }
MessageBus.publish('/queue_counts', msg, user_ids: User.staff.pluck(:id))
end
@ -60,10 +60,14 @@ class QueuedPost < ActiveRecord::Base
created_post
end
def self.all_attributes_for(queue)
[QueuedPost.attributes_by_queue[:base], QueuedPost.attributes_by_queue[queue.to_sym]].flatten.compact
end
private
def post_attributes
[QueuedPost.attributes_by_queue[:base], QueuedPost.attributes_by_queue[queue.to_sym]].flatten.compact
QueuedPost.all_attributes_for(queue)
end
def change_to!(state, changed_by)
@ -83,7 +87,7 @@ class QueuedPost < ActiveRecord::Base
updates.each {|k, v| send("#{k}=", v) }
changes_applied
QueuedPost.publish_new!
QueuedPost.broadcast_new!
end
end

View File

@ -0,0 +1,14 @@
class QueuedPostSerializer < ApplicationSerializer
attributes :id,
:queue,
:user_id,
:state,
:topic_id,
:approved_by_id,
:rejected_by_id,
:raw,
:post_options,
:created_at
has_one :user, serializer: BasicUserSerializer, embed: :object
end

View File

@ -226,7 +226,10 @@ en:
placeholder: "type the topic title here"
queue:
approve: 'Approve Post'
reject: 'Reject Post'
title: "Needs Approval"
none: "There are no posts to review."
approval:
title: "Post Needs Approval"

View File

@ -454,6 +454,9 @@ Discourse::Application.routes.draw do
get "/posts/:id/raw-email" => "posts#raw_email"
get "raw/:topic_id(/:post_number)" => "posts#markdown_num"
resources :queued_posts, constraints: StaffConstraint.new
get 'queued-posts' => 'queued_posts#index'
resources :invites do
collection do
get "upload" => "invites#check_csv_chunk"

View File

@ -21,7 +21,7 @@ class NewPostManager
def initialize(user, args)
@user = user
@args = args
@args = args.delete_if {|_, v| v.nil?}
end
def perform
@ -41,10 +41,16 @@ class NewPostManager
def enqueue(queue)
result = NewPostResult.new(:enqueued)
enqueuer = PostEnqueuer.new(@user, queue)
post = enqueuer.enqueue(@args)
QueuedPost.publish_new! if post && post.errors.empty?
queued_args = {post_options: @args.dup}
queued_args[:raw] = queued_args[:post_options].delete(:raw)
queued_args[:topic_id] = queued_args[:post_options].delete(:topic_id)
post = enqueuer.enqueue(queued_args)
QueuedPost.broadcast_new! if post && post.errors.empty?
result.queued_post = post
result.check_errors_from(enqueuer)
result
end

View File

@ -5,6 +5,7 @@ class NewPostResult
attr_reader :action
attr_accessor :post
attr_accessor :queued_post
def initialize(action, success=false)
@action = action

View File

@ -32,7 +32,7 @@ describe NewPostManager do
result
end
@queue_handler = -> (manager) { manager.args[:raw] =~ /queue me/ ? manager.enqueue('test') : nil }
@queue_handler = -> (manager) { manager.args[:raw] =~ /queue me/ ? manager.enqueue('new_topic') : nil }
NewPostManager.add_handler(&@counter_handler)
NewPostManager.add_handler(&@queue_handler)
@ -56,10 +56,14 @@ describe NewPostManager do
end
it "calls custom enqueuing handlers" do
manager = NewPostManager.new(topic.user, raw: 'to the handler I say enqueue me!', topic_id: topic.id)
manager = NewPostManager.new(topic.user, raw: 'to the handler I say enqueue me!', title: 'this is the title of the queued post')
result = manager.perform
enqueued = result.queued_post
expect(enqueued).to be_present
expect(enqueued.post_options['title']).to eq('this is the title of the queued post')
expect(result.action).to eq(:enqueued)
expect(result).to be_success
expect(result.post).to be_blank

View File

@ -0,0 +1,28 @@
require 'spec_helper'
describe QueuedPostsController do
context 'without authentication' do
it 'fails' do
xhr :get, :index
expect(response).not_to be_success
end
end
context 'as a regular user' do
let!(:user) { log_in(:user) }
it 'fails' do
xhr :get, :index
expect(response).not_to be_success
end
end
context 'as an admin' do
let!(:user) { log_in(:moderator) }
it 'returns the queued posts' do
xhr :get, :index
expect(response).to be_success
end
end
end

View File

@ -0,0 +1,29 @@
import { acceptance } from "helpers/qunit-helpers";
acceptance("Queued Posts", { loggedIn: true });
test("approve a post", () => {
visit("/queued-posts");
andThen(() => {
ok(exists('.queued-post'), 'it has posts listed');
});
click('.queued-post:eq(0) button.approve');
andThen(() => {
ok(!exists('.queued-post'), 'it removes the post');
});
});
test("reject a post", () => {
visit("/queued-posts");
andThen(() => {
ok(exists('.queued-post'), 'it has posts listed');
});
click('.queued-post:eq(0) button.reject');
andThen(() => {
ok(!exists('.queued-post'), 'it removes the post');
});
});

View File

@ -94,6 +94,16 @@ export default function() {
return response({});
});
this.put('/queued_posts/:queued_post_id', function(request) {
return response({ queued_post: {id: request.params.queued_post_id } });
});
this.get('/queued_posts', function() {
return response({
queued_posts: [{id: 1}]
});
});
this.post('/session', function(request) {
const data = parsePostData(request.requestBody);