discourse/app/models/draft.rb
Daniel Waterworth 55a1394342 DEV: pluck_first
Doing .pluck(:column).first is a very common pattern in Discourse and in
most cases, a limit cause isn't being added. Instead of adding a limit
clause to all these callsites, this commit adds two new methods to
ActiveRecord::Relation:

pluck_first, equivalent to limit(1).pluck(*columns).first

and pluck_first! which, like other finder methods, raises an exception
when no record is found
2019-10-21 12:08:20 +01:00

208 lines
5.7 KiB
Ruby

# frozen_string_literal: true
class Draft < ActiveRecord::Base
NEW_TOPIC ||= 'new_topic'
NEW_PRIVATE_MESSAGE ||= 'new_private_message'
EXISTING_TOPIC ||= 'topic_'
def self.set(user, key, sequence, data)
if SiteSetting.backup_drafts_to_pm_length > 0 && SiteSetting.backup_drafts_to_pm_length < data.length
backup_draft(user, key, sequence, data)
end
if d = find_draft(user, key)
return if d.sequence > sequence
DB.exec(<<~SQL, id: d.id, sequence: sequence, data: data)
UPDATE drafts
SET sequence = :sequence
, data = :data
, revisions = revisions + 1
WHERE id = :id
SQL
else
Draft.create!(user_id: user.id, draft_key: key, data: data, sequence: sequence)
end
true
end
def self.get(user, key, sequence)
d = find_draft(user, key)
d.data if d && d.sequence == sequence
end
def self.clear(user, key, sequence)
d = find_draft(user, key)
d.destroy if d && d.sequence <= sequence
end
def self.find_draft(user, key)
if user.is_a?(User)
find_by(user_id: user.id, draft_key: key)
else
find_by(user_id: user, draft_key: key)
end
end
def self.stream(opts = nil)
opts ||= {}
user_id = opts[:user].id
offset = (opts[:offset] || 0).to_i
limit = (opts[:limit] || 30).to_i
# JOIN of topics table based on manipulating draft_key seems imperfect
builder = DB.build <<~SQL
SELECT
d.*, t.title, t.id topic_id, t.archetype,
t.category_id, t.closed topic_closed, t.archived topic_archived,
pu.username, pu.name, pu.id user_id, pu.uploaded_avatar_id, pu.username_lower,
du.username draft_username, NULL as raw, NULL as cooked, NULL as post_number
FROM drafts d
LEFT JOIN LATERAL json_extract_path_text (d.data::json, 'postId') postId ON TRUE
LEFT JOIN posts p ON postId :: BIGINT = p.id
LEFT JOIN topics t ON
CASE
WHEN d.draft_key LIKE '%' || '#{EXISTING_TOPIC}' || '%'
THEN CAST(replace(d.draft_key, '#{EXISTING_TOPIC}', '') AS INT)
ELSE 0
END = t.id
JOIN users pu on pu.id = COALESCE(p.user_id, t.user_id, d.user_id)
JOIN users du on du.id = #{user_id}
/*where*/
/*order_by*/
/*offset*/
/*limit*/
SQL
builder
.where('d.user_id = :user_id', user_id: user_id.to_i)
.order_by('d.updated_at desc')
.offset(offset)
.limit(limit)
.query
end
def self.cleanup!
DB.exec(<<~SQL)
DELETE FROM drafts
WHERE sequence < (
SELECT MAX(s.sequence)
FROM draft_sequences s
WHERE s.draft_key = drafts.draft_key
AND s.user_id = drafts.user_id
)
SQL
# remove old drafts
delete_drafts_older_than_n_days = SiteSetting.delete_drafts_older_than_n_days.days.ago
Draft.where("updated_at < ?", delete_drafts_older_than_n_days).destroy_all
end
def self.backup_draft(user, key, sequence, data)
reply = JSON.parse(data)["reply"] || ""
return if reply.length < SiteSetting.backup_drafts_to_pm_length
post_id = BackupDraftPost.where(user_id: user.id, key: key).pluck_first(:post_id)
post = Post.where(id: post_id).first if post_id
if post_id && !post
BackupDraftPost.where(user_id: user.id, key: key).delete_all
end
indented_reply = reply.split("\n").map! do |l|
" #{l}"
end
draft_body = <<~MD
#{indented_reply.join("\n")}
```text
seq: #{sequence}
key: #{key}
```
MD
return if post && post.raw == draft_body
if !post
topic = ensure_draft_topic!(user)
Post.transaction do
post = PostCreator.new(
user,
raw: draft_body,
skip_jobs: true,
skip_validations: true,
topic_id: topic.id,
).create
BackupDraftPost.create!(user_id: user.id, key: key, post_id: post.id)
end
elsif post.last_version_at > 5.minutes.ago
# bypass all validations here to maximize speed
post.update_columns(
raw: draft_body,
cooked: PrettyText.cook(draft_body),
updated_at: Time.zone.now
)
else
revisor = PostRevisor.new(post, post.topic)
revisor.revise!(user, { raw: draft_body },
bypass_bump: true,
skip_validations: true,
skip_staff_log: true,
bypass_rate_limiter: true
)
end
rescue => e
Discourse.warn_exception(e, message: "Failed to backup draft")
end
def self.ensure_draft_topic!(user)
topic_id = BackupDraftTopic.where(user_id: user.id).pluck_first(:topic_id)
topic = Topic.find_by(id: topic_id) if topic_id
if topic_id && !topic
BackupDraftTopic.where(user_id: user.id).delete_all
end
if !topic
Topic.transaction do
creator = PostCreator.new(
user,
title: I18n.t("draft_backup.pm_title"),
archetype: Archetype.private_message,
raw: I18n.t("draft_backup.pm_body"),
skip_jobs: true,
skip_validations: true,
target_usernames: user.username
)
topic = creator.create.topic
BackupDraftTopic.create!(topic_id: topic.id, user_id: user.id)
end
end
topic
end
end
# == Schema Information
#
# Table name: drafts
#
# id :integer not null, primary key
# user_id :integer not null
# draft_key :string not null
# data :text not null
# created_at :datetime not null
# updated_at :datetime not null
# sequence :integer default(0), not null
# revisions :integer default(1), not null
#
# Indexes
#
# index_drafts_on_user_id_and_draft_key (user_id,draft_key)
#