diff --git a/app/assets/javascripts/discourse/controllers/queued-posts.js.es6 b/app/assets/javascripts/discourse/controllers/queued-posts.js.es6 new file mode 100644 index 00000000000..a597c2cb40b --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/queued-posts.js.es6 @@ -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); + }); + } + } +}); diff --git a/app/assets/javascripts/discourse/helpers/cook-text.js.es6 b/app/assets/javascripts/discourse/helpers/cook-text.js.es6 new file mode 100644 index 00000000000..c7acadce1b1 --- /dev/null +++ b/app/assets/javascripts/discourse/helpers/cook-text.js.es6 @@ -0,0 +1,6 @@ +import registerUnbound from 'discourse/helpers/register-unbound'; + +registerUnbound('cook-text', function(text) { + return new Handlebars.SafeString(Discourse.Markdown.cook(text)); +}); + diff --git a/app/assets/javascripts/discourse/routes/app-route-map.js.es6 b/app/assets/javascripts/discourse/routes/app-route-map.js.es6 index b866d99a278..922183aebea 100644 --- a/app/assets/javascripts/discourse/routes/app-route-map.js.es6 +++ b/app/assets/javascripts/discourse/routes/app-route-map.js.es6 @@ -93,4 +93,6 @@ export default function() { this.resource('badges', function() { this.route('show', {path: '/:id/:slug'}); }); + + this.resource('queued-posts', { path: '/queued-posts' }); } diff --git a/app/assets/javascripts/discourse/routes/queued-posts.js.es6 b/app/assets/javascripts/discourse/routes/queued-posts.js.es6 new file mode 100644 index 00000000000..58b28dd8fa8 --- /dev/null +++ b/app/assets/javascripts/discourse/routes/queued-posts.js.es6 @@ -0,0 +1,8 @@ +import DiscourseRoute from 'discourse/routes/discourse'; + +export default DiscourseRoute.extend({ + model() { + return this.store.find('queuedPost', {status: 'new'}); + } +}); + diff --git a/app/assets/javascripts/discourse/templates/queued-posts.hbs b/app/assets/javascripts/discourse/templates/queued-posts.hbs new file mode 100644 index 00000000000..2119de912b0 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/queued-posts.hbs @@ -0,0 +1,30 @@ +
+
+ {{#each post in model}} +
+ {{#if post.title}} +

{{post.title}}

+ {{/if}} +
+ {{avatar post.user imageSize="large"}} +
+
+
+ {{post.user.username}} +
+
+ + {{{cook-text post.raw}}} + +
+ {{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"}} +
+
+
+
+ {{else}} +

{{i18n "queue.none"}}

+ {{/each}} +
+
diff --git a/app/assets/javascripts/discourse/templates/site-map.hbs b/app/assets/javascripts/discourse/templates/site-map.hbs index 0bbe1dd96d6..46e9080dfd9 100644 --- a/app/assets/javascripts/discourse/templates/site-map.hbs +++ b/app/assets/javascripts/discourse/templates/site-map.hbs @@ -28,12 +28,12 @@ {{#if currentUser.staff}}
  • - + {{#link-to 'queued-posts'}} {{i18n "queue.title"}} {{#if currentUser.post_queue_new_count}} {{currentUser.post_queue_new_count}} {{/if}} - + {{/link-to}}
  • {{/if}} diff --git a/app/assets/stylesheets/desktop.scss b/app/assets/stylesheets/desktop.scss index abeab149278..426146d3bea 100644 --- a/app/assets/stylesheets/desktop.scss +++ b/app/assets/stylesheets/desktop.scss @@ -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. */ diff --git a/app/assets/stylesheets/desktop/queued-posts.scss b/app/assets/stylesheets/desktop/queued-posts.scss new file mode 100644 index 00000000000..50955c9970f --- /dev/null +++ b/app/assets/stylesheets/desktop/queued-posts.scss @@ -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%); + } +} + diff --git a/app/controllers/admin/admin_controller.rb b/app/controllers/admin/admin_controller.rb index 77d12479cf2..30785689e1d 100644 --- a/app/controllers/admin/admin_controller.rb +++ b/app/controllers/admin/admin_controller.rb @@ -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 diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 3b76392c287..7ee38765d2a 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -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?) diff --git a/app/controllers/queued_posts_controller.rb b/app/controllers/queued_posts_controller.rb new file mode 100644 index 00000000000..e7dd20d79da --- /dev/null +++ b/app/controllers/queued_posts_controller.rb @@ -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 diff --git a/app/models/queued_post.rb b/app/models/queued_post.rb index 03d5e352a68..8dce4ffeefa 100644 --- a/app/models/queued_post.rb +++ b/app/models/queued_post.rb @@ -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 diff --git a/app/serializers/queued_post_serializer.rb b/app/serializers/queued_post_serializer.rb new file mode 100644 index 00000000000..ed9b9a70036 --- /dev/null +++ b/app/serializers/queued_post_serializer.rb @@ -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 diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index cbfa19f3a5d..d27df473215 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -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" diff --git a/config/routes.rb b/config/routes.rb index 731838d2e70..17a9c307ffb 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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" diff --git a/lib/new_post_manager.rb b/lib/new_post_manager.rb index f892dcbb6b0..d649cdf0afc 100644 --- a/lib/new_post_manager.rb +++ b/lib/new_post_manager.rb @@ -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 diff --git a/lib/new_post_result.rb b/lib/new_post_result.rb index 1e8bdffb04d..a14f0458a54 100644 --- a/lib/new_post_result.rb +++ b/lib/new_post_result.rb @@ -5,6 +5,7 @@ class NewPostResult attr_reader :action attr_accessor :post + attr_accessor :queued_post def initialize(action, success=false) @action = action diff --git a/spec/components/new_post_manager_spec.rb b/spec/components/new_post_manager_spec.rb index 78bc1250603..48d81bd0f4b 100644 --- a/spec/components/new_post_manager_spec.rb +++ b/spec/components/new_post_manager_spec.rb @@ -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 diff --git a/spec/controllers/queued_posts_controller_spec.rb b/spec/controllers/queued_posts_controller_spec.rb new file mode 100644 index 00000000000..99cbd383444 --- /dev/null +++ b/spec/controllers/queued_posts_controller_spec.rb @@ -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 + diff --git a/test/javascripts/acceptance/queued-posts-test.js.es6 b/test/javascripts/acceptance/queued-posts-test.js.es6 new file mode 100644 index 00000000000..33b301790f3 --- /dev/null +++ b/test/javascripts/acceptance/queued-posts-test.js.es6 @@ -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'); + }); +}); diff --git a/test/javascripts/helpers/create-pretender.js.es6 b/test/javascripts/helpers/create-pretender.js.es6 index d07876fe22c..c71f168c46d 100644 --- a/test/javascripts/helpers/create-pretender.js.es6 +++ b/test/javascripts/helpers/create-pretender.js.es6 @@ -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);