mirror of
https://github.com/discourse/discourse.git
synced 2025-03-09 14:34:35 +00:00
Interface for reviewing queued posts
This commit is contained in:
parent
f1ede42569
commit
96d2c5069b
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,6 @@
|
|||||||
|
import registerUnbound from 'discourse/helpers/register-unbound';
|
||||||
|
|
||||||
|
registerUnbound('cook-text', function(text) {
|
||||||
|
return new Handlebars.SafeString(Discourse.Markdown.cook(text));
|
||||||
|
});
|
||||||
|
|
@ -93,4 +93,6 @@ export default function() {
|
|||||||
this.resource('badges', function() {
|
this.resource('badges', function() {
|
||||||
this.route('show', {path: '/:id/:slug'});
|
this.route('show', {path: '/:id/:slug'});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.resource('queued-posts', { path: '/queued-posts' });
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
import DiscourseRoute from 'discourse/routes/discourse';
|
||||||
|
|
||||||
|
export default DiscourseRoute.extend({
|
||||||
|
model() {
|
||||||
|
return this.store.find('queuedPost', {status: 'new'});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
30
app/assets/javascripts/discourse/templates/queued-posts.hbs
Normal file
30
app/assets/javascripts/discourse/templates/queued-posts.hbs
Normal 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>
|
@ -28,12 +28,12 @@
|
|||||||
|
|
||||||
{{#if currentUser.staff}}
|
{{#if currentUser.staff}}
|
||||||
<li>
|
<li>
|
||||||
<a href="/queued-posts">
|
{{#link-to 'queued-posts'}}
|
||||||
{{i18n "queue.title"}}
|
{{i18n "queue.title"}}
|
||||||
{{#if currentUser.post_queue_new_count}}
|
{{#if currentUser.post_queue_new_count}}
|
||||||
<span class='badge-notification flagged-posts'>{{currentUser.post_queue_new_count}}</span>
|
<span class='badge-notification flagged-posts'>{{currentUser.post_queue_new_count}}</span>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</a>
|
{{/link-to}}
|
||||||
</li>
|
</li>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
@import "desktop/upload";
|
@import "desktop/upload";
|
||||||
@import "desktop/user";
|
@import "desktop/user";
|
||||||
@import "desktop/history";
|
@import "desktop/history";
|
||||||
|
@import "desktop/queued-posts";
|
||||||
|
|
||||||
/* These files doesn't actually exist, they are injected by DiscourseSassImporter. */
|
/* These files doesn't actually exist, they are injected by DiscourseSassImporter. */
|
||||||
|
|
||||||
|
20
app/assets/stylesheets/desktop/queued-posts.scss
Normal file
20
app/assets/stylesheets/desktop/queued-posts.scss
Normal 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%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,11 +7,4 @@ class Admin::AdminController < ApplicationController
|
|||||||
render nothing: true
|
render nothing: true
|
||||||
end
|
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
|
end
|
||||||
|
@ -371,6 +371,10 @@ class ApplicationController < ActionController::Base
|
|||||||
raise Discourse::NotLoggedIn.new unless current_user.present?
|
raise Discourse::NotLoggedIn.new unless current_user.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def ensure_staff
|
||||||
|
raise Discourse::InvalidAccess.new unless current_user && current_user.staff?
|
||||||
|
end
|
||||||
|
|
||||||
def redirect_to_login_if_required
|
def redirect_to_login_if_required
|
||||||
return if current_user || (request.format.json? && api_key_valid?)
|
return if current_user || (request.format.json? && api_key_valid?)
|
||||||
|
|
||||||
|
20
app/controllers/queued_posts_controller.rb
Normal file
20
app/controllers/queued_posts_controller.rb
Normal 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
|
@ -30,7 +30,7 @@ class QueuedPost < ActiveRecord::Base
|
|||||||
where(state: states[:new]).count
|
where(state: states[:new]).count
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.publish_new!
|
def self.broadcast_new!
|
||||||
msg = { post_queue_new_count: QueuedPost.new_count }
|
msg = { post_queue_new_count: QueuedPost.new_count }
|
||||||
MessageBus.publish('/queue_counts', msg, user_ids: User.staff.pluck(:id))
|
MessageBus.publish('/queue_counts', msg, user_ids: User.staff.pluck(:id))
|
||||||
end
|
end
|
||||||
@ -60,10 +60,14 @@ class QueuedPost < ActiveRecord::Base
|
|||||||
created_post
|
created_post
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.all_attributes_for(queue)
|
||||||
|
[QueuedPost.attributes_by_queue[:base], QueuedPost.attributes_by_queue[queue.to_sym]].flatten.compact
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def post_attributes
|
def post_attributes
|
||||||
[QueuedPost.attributes_by_queue[:base], QueuedPost.attributes_by_queue[queue.to_sym]].flatten.compact
|
QueuedPost.all_attributes_for(queue)
|
||||||
end
|
end
|
||||||
|
|
||||||
def change_to!(state, changed_by)
|
def change_to!(state, changed_by)
|
||||||
@ -83,7 +87,7 @@ class QueuedPost < ActiveRecord::Base
|
|||||||
updates.each {|k, v| send("#{k}=", v) }
|
updates.each {|k, v| send("#{k}=", v) }
|
||||||
changes_applied
|
changes_applied
|
||||||
|
|
||||||
QueuedPost.publish_new!
|
QueuedPost.broadcast_new!
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
14
app/serializers/queued_post_serializer.rb
Normal file
14
app/serializers/queued_post_serializer.rb
Normal 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
|
@ -226,7 +226,10 @@ en:
|
|||||||
placeholder: "type the topic title here"
|
placeholder: "type the topic title here"
|
||||||
|
|
||||||
queue:
|
queue:
|
||||||
|
approve: 'Approve Post'
|
||||||
|
reject: 'Reject Post'
|
||||||
title: "Needs Approval"
|
title: "Needs Approval"
|
||||||
|
none: "There are no posts to review."
|
||||||
|
|
||||||
approval:
|
approval:
|
||||||
title: "Post Needs Approval"
|
title: "Post Needs Approval"
|
||||||
|
@ -454,6 +454,9 @@ Discourse::Application.routes.draw do
|
|||||||
get "/posts/:id/raw-email" => "posts#raw_email"
|
get "/posts/:id/raw-email" => "posts#raw_email"
|
||||||
get "raw/:topic_id(/:post_number)" => "posts#markdown_num"
|
get "raw/:topic_id(/:post_number)" => "posts#markdown_num"
|
||||||
|
|
||||||
|
resources :queued_posts, constraints: StaffConstraint.new
|
||||||
|
get 'queued-posts' => 'queued_posts#index'
|
||||||
|
|
||||||
resources :invites do
|
resources :invites do
|
||||||
collection do
|
collection do
|
||||||
get "upload" => "invites#check_csv_chunk"
|
get "upload" => "invites#check_csv_chunk"
|
||||||
|
@ -21,7 +21,7 @@ class NewPostManager
|
|||||||
|
|
||||||
def initialize(user, args)
|
def initialize(user, args)
|
||||||
@user = user
|
@user = user
|
||||||
@args = args
|
@args = args.delete_if {|_, v| v.nil?}
|
||||||
end
|
end
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
@ -41,10 +41,16 @@ class NewPostManager
|
|||||||
def enqueue(queue)
|
def enqueue(queue)
|
||||||
result = NewPostResult.new(:enqueued)
|
result = NewPostResult.new(:enqueued)
|
||||||
enqueuer = PostEnqueuer.new(@user, queue)
|
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.check_errors_from(enqueuer)
|
||||||
result
|
result
|
||||||
end
|
end
|
||||||
|
@ -5,6 +5,7 @@ class NewPostResult
|
|||||||
|
|
||||||
attr_reader :action
|
attr_reader :action
|
||||||
attr_accessor :post
|
attr_accessor :post
|
||||||
|
attr_accessor :queued_post
|
||||||
|
|
||||||
def initialize(action, success=false)
|
def initialize(action, success=false)
|
||||||
@action = action
|
@action = action
|
||||||
|
@ -32,7 +32,7 @@ describe NewPostManager do
|
|||||||
result
|
result
|
||||||
end
|
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(&@counter_handler)
|
||||||
NewPostManager.add_handler(&@queue_handler)
|
NewPostManager.add_handler(&@queue_handler)
|
||||||
@ -56,10 +56,14 @@ describe NewPostManager do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it "calls custom enqueuing handlers" do
|
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
|
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.action).to eq(:enqueued)
|
||||||
expect(result).to be_success
|
expect(result).to be_success
|
||||||
expect(result.post).to be_blank
|
expect(result.post).to be_blank
|
||||||
|
28
spec/controllers/queued_posts_controller_spec.rb
Normal file
28
spec/controllers/queued_posts_controller_spec.rb
Normal 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
|
||||||
|
|
29
test/javascripts/acceptance/queued-posts-test.js.es6
Normal file
29
test/javascripts/acceptance/queued-posts-test.js.es6
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
@ -94,6 +94,16 @@ export default function() {
|
|||||||
return response({});
|
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) {
|
this.post('/session', function(request) {
|
||||||
const data = parsePostData(request.requestBody);
|
const data = parsePostData(request.requestBody);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user