mirror of
https://github.com/discourse/discourse.git
synced 2025-01-31 15:17:14 +08:00
Post Queue model to enqueue creation of posts
This commit is contained in:
parent
5111fdc926
commit
8ba6a45cd7
77
app/models/queued_post.rb
Normal file
77
app/models/queued_post.rb
Normal file
|
@ -0,0 +1,77 @@
|
|||
class QueuedPost < ActiveRecord::Base
|
||||
|
||||
class InvalidStateTransition < StandardError; end;
|
||||
|
||||
serialize :post_options, JSON
|
||||
|
||||
belongs_to :user
|
||||
belongs_to :topic
|
||||
belongs_to :approved_by, class_name: "User"
|
||||
belongs_to :rejected_by, class_name: "User"
|
||||
|
||||
def self.attributes_by_queue
|
||||
@attributes_by_queue ||= {
|
||||
base: [:archetype,
|
||||
:via_email,
|
||||
:raw_email,
|
||||
:auto_track,
|
||||
:custom_fields,
|
||||
:cooking_options,
|
||||
:cook_method,
|
||||
:image_sizes],
|
||||
new_post: [:reply_to_post_number],
|
||||
new_topic: [:title, :category, :meta_data, :archetype],
|
||||
}
|
||||
end
|
||||
|
||||
def self.states
|
||||
@states ||= Enum.new(:new, :approved, :rejected)
|
||||
end
|
||||
|
||||
def reject!(rejected_by)
|
||||
change_to!(:rejected, rejected_by)
|
||||
end
|
||||
|
||||
def create_options
|
||||
opts = {raw: raw}
|
||||
post_attributes.each {|a| opts[a] = post_options[a.to_s] }
|
||||
|
||||
opts[:topic_id] = topic_id if topic_id
|
||||
opts
|
||||
end
|
||||
|
||||
def approve!(approved_by)
|
||||
created_post = nil
|
||||
QueuedPost.transaction do
|
||||
change_to!(:approved, approved_by)
|
||||
|
||||
creator = PostCreator.new(user, create_options)
|
||||
created_post = creator.create
|
||||
end
|
||||
created_post
|
||||
end
|
||||
|
||||
private
|
||||
def post_attributes
|
||||
[QueuedPost.attributes_by_queue[:base], QueuedPost.attributes_by_queue[queue.to_sym]].flatten.compact
|
||||
end
|
||||
|
||||
def change_to!(state, changed_by)
|
||||
state_val = QueuedPost.states[state]
|
||||
|
||||
updates = { state: state_val,
|
||||
"#{state}_by_id" => changed_by.id,
|
||||
"#{state}_at" => Time.now }
|
||||
|
||||
# We use an update with `row_count` trick here to avoid stampeding requests to
|
||||
# update the same row simultaneously. Only one state change should go through and
|
||||
# we can use the DB to enforce this
|
||||
row_count = QueuedPost.where('id = ? AND state <> ?', id, state_val).update_all(updates)
|
||||
raise InvalidStateTransition.new if row_count == 0
|
||||
|
||||
# Update the record in memory too, and clear the dirty flag
|
||||
updates.each {|k, v| send("#{k}=", v) }
|
||||
changes_applied
|
||||
end
|
||||
|
||||
end
|
20
db/migrate/20150325160959_create_queued_posts.rb
Normal file
20
db/migrate/20150325160959_create_queued_posts.rb
Normal file
|
@ -0,0 +1,20 @@
|
|||
class CreateQueuedPosts < ActiveRecord::Migration
|
||||
def change
|
||||
create_table :queued_posts do |t|
|
||||
t.string :queue, null: false
|
||||
t.integer :state, null: false
|
||||
t.integer :user_id, null: false
|
||||
t.text :raw, null: false
|
||||
t.text :post_options, null: false
|
||||
t.integer :topic_id
|
||||
t.integer :approved_by_id
|
||||
t.timestamp :approved_at
|
||||
t.integer :rejected_by_id
|
||||
t.timestamp :rejected_at
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :queued_posts, [:queue, :state, :created_at], name: 'by_queue_status'
|
||||
add_index :queued_posts, [:topic, :queue, :state, :created_at], name: 'by_queue_status_topic'
|
||||
end
|
||||
end
|
124
spec/models/queued_post_spec.rb
Normal file
124
spec/models/queued_post_spec.rb
Normal file
|
@ -0,0 +1,124 @@
|
|||
require 'spec_helper'
|
||||
require_dependency 'queued_post'
|
||||
|
||||
describe QueuedPost do
|
||||
|
||||
context "creating a post" do
|
||||
let(:topic) { Fabricate(:topic) }
|
||||
let(:user) { Fabricate(:user) }
|
||||
let(:admin) { Fabricate(:admin) }
|
||||
let(:qp) { QueuedPost.create(queue: 'new_post',
|
||||
state: QueuedPost.states[:new],
|
||||
user_id: user.id,
|
||||
topic_id: topic.id,
|
||||
raw: 'This post should be queued up',
|
||||
post_options: {
|
||||
reply_to_post_number: 1,
|
||||
via_email: true,
|
||||
raw_email: 'store_me',
|
||||
auto_track: true,
|
||||
custom_fields: { hello: 'world' },
|
||||
cooking_options: { cat: 'hat' },
|
||||
cook_method: 'regular',
|
||||
not_create_option: true,
|
||||
image_sizes: {"http://foo.bar/image.png" => {"width" => 0, "height" => 222}}
|
||||
}) }
|
||||
|
||||
it "returns the appropriate options for posting" do
|
||||
create_options = qp.create_options
|
||||
|
||||
expect(create_options[:topic_id]).to eq(topic.id)
|
||||
expect(create_options[:raw]).to eq('This post should be queued up')
|
||||
expect(create_options[:reply_to_post_number]).to eq(1)
|
||||
expect(create_options[:via_email]).to eq(true)
|
||||
expect(create_options[:raw_email]).to eq('store_me')
|
||||
expect(create_options[:auto_track]).to eq(true)
|
||||
expect(create_options[:custom_fields]).to eq('hello' => 'world')
|
||||
expect(create_options[:cooking_options]).to eq('cat' => 'hat')
|
||||
expect(create_options[:cook_method]).to eq('regular')
|
||||
expect(create_options[:not_create_option]).to eq(nil)
|
||||
expect(create_options[:image_sizes]).to eq("http://foo.bar/image.png" => {"width" => 0, "height" => 222})
|
||||
end
|
||||
|
||||
it "follows the correct workflow for approval" do
|
||||
post = qp.approve!(admin)
|
||||
|
||||
# Creates the post with the attributes
|
||||
expect(post).to be_present
|
||||
expect(post).to be_valid
|
||||
expect(post.topic).to eq(topic)
|
||||
|
||||
# Updates the QP record
|
||||
expect(qp.approved_by).to eq(admin)
|
||||
expect(qp.state).to eq(QueuedPost.states[:approved])
|
||||
expect(qp.approved_at).to be_present
|
||||
|
||||
# We can't approve twice
|
||||
expect(-> { qp.approve!(admin) }).to raise_error(QueuedPost::InvalidStateTransition)
|
||||
|
||||
end
|
||||
|
||||
it "follows the correct workflow for rejection" do
|
||||
qp.reject!(admin)
|
||||
|
||||
# Updates the QP record
|
||||
expect(qp.rejected_by).to eq(admin)
|
||||
expect(qp.state).to eq(QueuedPost.states[:rejected])
|
||||
expect(qp.rejected_at).to be_present
|
||||
|
||||
# We can't reject twice
|
||||
expect(-> { qp.reject!(admin) }).to raise_error(QueuedPost::InvalidStateTransition)
|
||||
end
|
||||
end
|
||||
|
||||
context "creating a topic" do
|
||||
let(:user) { Fabricate(:user) }
|
||||
let(:admin) { Fabricate(:admin) }
|
||||
let!(:category) { Fabricate(:category) }
|
||||
|
||||
let(:qp) { QueuedPost.create(queue: 'new_topic',
|
||||
state: QueuedPost.states[:new],
|
||||
user_id: user.id,
|
||||
raw: 'This post should be queued up',
|
||||
post_options: {
|
||||
title: 'This is the topic title to queue up',
|
||||
archetype: 'regular',
|
||||
category: category.id,
|
||||
meta_data: {evil: 'trout'}
|
||||
}) }
|
||||
|
||||
|
||||
it "returns the appropriate options for creating a topic" do
|
||||
create_options = qp.create_options
|
||||
|
||||
expect(create_options[:category]).to eq(category.id)
|
||||
expect(create_options[:archetype]).to eq('regular')
|
||||
expect(create_options[:meta_data]).to eq('evil' => 'trout')
|
||||
end
|
||||
|
||||
it "creates the post and topic" do
|
||||
topic_count, post_count = Topic.count, Post.count
|
||||
post = qp.approve!(admin)
|
||||
|
||||
expect(Topic.count).to eq(topic_count + 1)
|
||||
expect(Post.count).to eq(post_count + 1)
|
||||
|
||||
expect(post).to be_present
|
||||
expect(post).to be_valid
|
||||
|
||||
topic = post.topic
|
||||
expect(topic).to be_present
|
||||
expect(topic.category).to eq(category)
|
||||
end
|
||||
|
||||
it "doesn't create the post and topic" do
|
||||
topic_count, post_count = Topic.count, Post.count
|
||||
|
||||
qp.reject!(admin)
|
||||
|
||||
expect(Topic.count).to eq(topic_count)
|
||||
expect(Post.count).to eq(post_count)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
Loading…
Reference in New Issue
Block a user