add new model for post replacements, add undo functionality
This commit is contained in:
@@ -7,8 +7,6 @@ class Post < ActiveRecord::Base
|
|||||||
class RevertError < Exception ; end
|
class RevertError < Exception ; end
|
||||||
class SearchError < Exception ; end
|
class SearchError < Exception ; end
|
||||||
|
|
||||||
DELETION_GRACE_PERIOD = 30.days
|
|
||||||
|
|
||||||
before_validation :initialize_uploader, :on => :create
|
before_validation :initialize_uploader, :on => :create
|
||||||
before_validation :merge_old_changes
|
before_validation :merge_old_changes
|
||||||
before_validation :normalize_tags
|
before_validation :normalize_tags
|
||||||
@@ -51,6 +49,7 @@ class Post < ActiveRecord::Base
|
|||||||
has_many :approvals, :class_name => "PostApproval", :dependent => :destroy
|
has_many :approvals, :class_name => "PostApproval", :dependent => :destroy
|
||||||
has_many :disapprovals, :class_name => "PostDisapproval", :dependent => :destroy
|
has_many :disapprovals, :class_name => "PostDisapproval", :dependent => :destroy
|
||||||
has_many :favorites, :dependent => :destroy
|
has_many :favorites, :dependent => :destroy
|
||||||
|
has_many :replacements, class_name: "PostReplacement"
|
||||||
|
|
||||||
if PostArchive.enabled?
|
if PostArchive.enabled?
|
||||||
has_many :versions, lambda {order("post_versions.updated_at ASC")}, :class_name => "PostArchive", :dependent => :destroy
|
has_many :versions, lambda {order("post_versions.updated_at ASC")}, :class_name => "PostArchive", :dependent => :destroy
|
||||||
@@ -1430,50 +1429,9 @@ class Post < ActiveRecord::Base
|
|||||||
ModAction.log("undeleted post ##{id}")
|
ModAction.log("undeleted post ##{id}")
|
||||||
end
|
end
|
||||||
|
|
||||||
def replace!(url, replacer = CurrentUser.user)
|
def replace!(url)
|
||||||
# TODO for posts with notes we need to rescale the notes if the dimensions change.
|
replacement = replacements.create(replacement_url: url)
|
||||||
if notes.size > 0
|
replacement.process!
|
||||||
raise NotImplementedError.new("Replacing images with notes not yet supported.")
|
|
||||||
end
|
|
||||||
|
|
||||||
# TODO for ugoiras we need to replace the frame data.
|
|
||||||
if is_ugoira?
|
|
||||||
raise NotImplementedError.new("Replacing ugoira images not yet supported.")
|
|
||||||
end
|
|
||||||
|
|
||||||
# TODO images hosted on s3 need to be deleted from s3 instead of the local filesystem.
|
|
||||||
if Danbooru.config.use_s3_proxy?(self)
|
|
||||||
raise NotImplementedError.new("Replacing S3 hosted images not yet supported.")
|
|
||||||
end
|
|
||||||
|
|
||||||
transaction do
|
|
||||||
upload = Upload.create!(source: url, rating: self.rating, tag_string: self.tag_string)
|
|
||||||
upload.process_upload
|
|
||||||
upload.update(status: "completed", post_id: id)
|
|
||||||
|
|
||||||
# queue the deletion *before* updating the post so that we use the old
|
|
||||||
# md5/file_ext to delete the old files. if saving the post fails,
|
|
||||||
# this is rolled back so the job won't run.
|
|
||||||
Post.delay(queue: "default", run_at: Time.now + DELETION_GRACE_PERIOD).delete_files(id, file_path, large_file_path, preview_file_path)
|
|
||||||
|
|
||||||
self.md5 = upload.md5
|
|
||||||
self.file_ext = upload.file_ext
|
|
||||||
self.image_width = upload.image_width
|
|
||||||
self.image_height = upload.image_height
|
|
||||||
self.file_size = upload.file_size
|
|
||||||
self.source = upload.source
|
|
||||||
self.tag_string = upload.tag_string
|
|
||||||
|
|
||||||
comments.create!({creator: User.system, body: presenter.comment_replacement_message(replacer), do_not_bump_post: true}, without_protection: true)
|
|
||||||
ModAction.log(presenter.modaction_replacement_message)
|
|
||||||
|
|
||||||
save!
|
|
||||||
end
|
|
||||||
|
|
||||||
# point of no return: these things can't be rolled back, so we do them
|
|
||||||
# only after the transaction successfully commits.
|
|
||||||
distribute_files
|
|
||||||
update_iqdb_async
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
64
app/models/post_replacement.rb
Normal file
64
app/models/post_replacement.rb
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
class PostReplacement < ActiveRecord::Base
|
||||||
|
DELETION_GRACE_PERIOD = 30.days
|
||||||
|
|
||||||
|
belongs_to :post
|
||||||
|
belongs_to :creator, class_name: "User"
|
||||||
|
before_validation :initialize_fields
|
||||||
|
attr_accessible :replacement_url
|
||||||
|
|
||||||
|
def initialize_fields
|
||||||
|
self.creator = CurrentUser.user
|
||||||
|
self.original_url = post.source
|
||||||
|
end
|
||||||
|
|
||||||
|
def undo!
|
||||||
|
undo_replacement = post.replacements.create(replacement_url: original_url)
|
||||||
|
undo_replacement.process!
|
||||||
|
end
|
||||||
|
|
||||||
|
def process!
|
||||||
|
# TODO for posts with notes we need to rescale the notes if the dimensions change.
|
||||||
|
if post.notes.any?
|
||||||
|
raise NotImplementedError.new("Replacing images with notes not yet supported.")
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO for ugoiras we need to replace the frame data.
|
||||||
|
if post.is_ugoira?
|
||||||
|
raise NotImplementedError.new("Replacing ugoira images not yet supported.")
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO images hosted on s3 need to be deleted from s3 instead of the local filesystem.
|
||||||
|
if Danbooru.config.use_s3_proxy?(post)
|
||||||
|
raise NotImplementedError.new("Replacing S3 hosted images not yet supported.")
|
||||||
|
end
|
||||||
|
|
||||||
|
transaction do
|
||||||
|
upload = Upload.create!(source: replacement_url, rating: post.rating, tag_string: post.tag_string)
|
||||||
|
upload.process_upload
|
||||||
|
upload.update(status: "completed", post_id: post.id)
|
||||||
|
|
||||||
|
# queue the deletion *before* updating the post so that we use the old
|
||||||
|
# md5/file_ext to delete the old files. if saving the post fails,
|
||||||
|
# this is rolled back so the job won't run.
|
||||||
|
Post.delay(queue: "default", run_at: Time.now + DELETION_GRACE_PERIOD).delete_files(post.id, post.file_path, post.large_file_path, post.preview_file_path)
|
||||||
|
|
||||||
|
post.md5 = upload.md5
|
||||||
|
post.file_ext = upload.file_ext
|
||||||
|
post.image_width = upload.image_width
|
||||||
|
post.image_height = upload.image_height
|
||||||
|
post.file_size = upload.file_size
|
||||||
|
post.source = upload.source
|
||||||
|
post.tag_string = upload.tag_string
|
||||||
|
|
||||||
|
post.comments.create!({creator: User.system, body: post.presenter.comment_replacement_message(creator), do_not_bump_post: true}, without_protection: true)
|
||||||
|
ModAction.log(post.presenter.modaction_replacement_message)
|
||||||
|
|
||||||
|
post.save!
|
||||||
|
end
|
||||||
|
|
||||||
|
# point of no return: these things can't be rolled back, so we do them
|
||||||
|
# only after the transaction successfully commits.
|
||||||
|
post.distribute_files
|
||||||
|
post.update_iqdb_async
|
||||||
|
end
|
||||||
|
end
|
||||||
14
db/migrate/20170512221200_create_post_replacements.rb
Normal file
14
db/migrate/20170512221200_create_post_replacements.rb
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
class CreatePostReplacements < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
create_table :post_replacements do |t|
|
||||||
|
t.integer :post_id, null: false
|
||||||
|
t.integer :creator_id, null: false
|
||||||
|
t.text :original_url, null: false
|
||||||
|
t.text :replacement_url, null: false
|
||||||
|
t.timestamps null: false
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :post_replacements, :post_id
|
||||||
|
add_index :post_replacements, :creator_id
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -2686,6 +2686,40 @@ CREATE SEQUENCE post_flags_id_seq
|
|||||||
ALTER SEQUENCE post_flags_id_seq OWNED BY post_flags.id;
|
ALTER SEQUENCE post_flags_id_seq OWNED BY post_flags.id;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: post_replacements; Type: TABLE; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE post_replacements (
|
||||||
|
id integer NOT NULL,
|
||||||
|
post_id integer NOT NULL,
|
||||||
|
creator_id integer NOT NULL,
|
||||||
|
original_url text NOT NULL,
|
||||||
|
replacement_url text NOT NULL,
|
||||||
|
created_at timestamp without time zone NOT NULL,
|
||||||
|
updated_at timestamp without time zone NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: post_replacements_id_seq; Type: SEQUENCE; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SEQUENCE post_replacements_id_seq
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MINVALUE
|
||||||
|
NO MAXVALUE
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: post_replacements_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER SEQUENCE post_replacements_id_seq OWNED BY post_replacements.id;
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: post_updates; Type: TABLE; Schema: public; Owner: -
|
-- Name: post_updates; Type: TABLE; Schema: public; Owner: -
|
||||||
--
|
--
|
||||||
@@ -4265,6 +4299,13 @@ ALTER TABLE ONLY post_disapprovals ALTER COLUMN id SET DEFAULT nextval('post_dis
|
|||||||
ALTER TABLE ONLY post_flags ALTER COLUMN id SET DEFAULT nextval('post_flags_id_seq'::regclass);
|
ALTER TABLE ONLY post_flags ALTER COLUMN id SET DEFAULT nextval('post_flags_id_seq'::regclass);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: id; Type: DEFAULT; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY post_replacements ALTER COLUMN id SET DEFAULT nextval('post_replacements_id_seq'::regclass);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: id; Type: DEFAULT; Schema: public; Owner: -
|
-- Name: id; Type: DEFAULT; Schema: public; Owner: -
|
||||||
--
|
--
|
||||||
@@ -4658,6 +4699,14 @@ ALTER TABLE ONLY post_flags
|
|||||||
ADD CONSTRAINT post_flags_pkey PRIMARY KEY (id);
|
ADD CONSTRAINT post_flags_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: post_replacements_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY post_replacements
|
||||||
|
ADD CONSTRAINT post_replacements_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: post_votes_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
-- Name: post_votes_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||||
--
|
--
|
||||||
@@ -6780,6 +6829,20 @@ CREATE INDEX index_post_flags_on_post_id ON post_flags USING btree (post_id);
|
|||||||
CREATE INDEX index_post_flags_on_reason_tsvector ON post_flags USING gin (to_tsvector('english'::regconfig, reason));
|
CREATE INDEX index_post_flags_on_reason_tsvector ON post_flags USING gin (to_tsvector('english'::regconfig, reason));
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: index_post_replacements_on_creator_id; Type: INDEX; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE INDEX index_post_replacements_on_creator_id ON post_replacements USING btree (creator_id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: index_post_replacements_on_post_id; Type: INDEX; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE INDEX index_post_replacements_on_post_id ON post_replacements USING btree (post_id);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: index_post_votes_on_post_id; Type: INDEX; Schema: public; Owner: -
|
-- Name: index_post_votes_on_post_id; Type: INDEX; Schema: public; Owner: -
|
||||||
--
|
--
|
||||||
@@ -7471,3 +7534,5 @@ INSERT INTO schema_migrations (version) VALUES ('20170416224142');
|
|||||||
|
|
||||||
INSERT INTO schema_migrations (version) VALUES ('20170428220448');
|
INSERT INTO schema_migrations (version) VALUES ('20170428220448');
|
||||||
|
|
||||||
|
INSERT INTO schema_migrations (version) VALUES ('20170512221200');
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
require 'test_helper'
|
require 'test_helper'
|
||||||
require 'helpers/pool_archive_test_helper'
|
require 'helpers/pool_archive_test_helper'
|
||||||
require 'helpers/saved_search_test_helper'
|
require 'helpers/saved_search_test_helper'
|
||||||
|
require 'helpers/iqdb_test_helper'
|
||||||
|
|
||||||
class PostTest < ActiveSupport::TestCase
|
class PostTest < ActiveSupport::TestCase
|
||||||
include PoolArchiveTestHelper
|
include PoolArchiveTestHelper
|
||||||
include SavedSearchTestHelper
|
include SavedSearchTestHelper
|
||||||
|
include IqdbTestHelper
|
||||||
|
|
||||||
def assert_tag_match(posts, query)
|
def assert_tag_match(posts, query)
|
||||||
assert_equal(posts.map(&:id), Post.tag_match(query).pluck(:id))
|
assert_equal(posts.map(&:id), Post.tag_match(query).pluck(:id))
|
||||||
@@ -1588,6 +1590,7 @@ class PostTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
context "Replacing: " do
|
context "Replacing: " do
|
||||||
setup do
|
setup do
|
||||||
|
mock_iqdb_service!
|
||||||
Delayed::Worker.delay_jobs = true # don't delete the old images right away
|
Delayed::Worker.delay_jobs = true # don't delete the old images right away
|
||||||
Danbooru.config.stubs(:use_s3_proxy?).returns(false) # don't fail on post ids < 10000
|
Danbooru.config.stubs(:use_s3_proxy?).returns(false) # don't fail on post ids < 10000
|
||||||
|
|
||||||
@@ -1612,11 +1615,38 @@ class PostTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
context "replacing a post from a generic source" do
|
context "replacing a post from a generic source" do
|
||||||
setup do
|
setup do
|
||||||
|
@post.update(source: "https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png")
|
||||||
@post.replace!("https://www.google.com/intl/en_ALL/images/logo.gif")
|
@post.replace!("https://www.google.com/intl/en_ALL/images/logo.gif")
|
||||||
@upload = Upload.last
|
@upload = Upload.last
|
||||||
@mod_action = ModAction.last
|
@mod_action = ModAction.last
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "that is then undone" do
|
||||||
|
setup do
|
||||||
|
Timecop.travel(Time.now + PostReplacement::DELETION_GRACE_PERIOD + 1.day) do
|
||||||
|
Delayed::Worker.new.work_off
|
||||||
|
end
|
||||||
|
|
||||||
|
@replacement = @post.replacements.first
|
||||||
|
@replacement.undo!
|
||||||
|
@post.reload
|
||||||
|
end
|
||||||
|
|
||||||
|
should "update the attributes" do
|
||||||
|
assert_equal(272, @post.image_width)
|
||||||
|
assert_equal(92, @post.image_height)
|
||||||
|
assert_equal(5969, @post.file_size)
|
||||||
|
assert_equal("png", @post.file_ext)
|
||||||
|
assert_equal("8f9327db2597fa57d2f42b4a6c5a9855", @post.md5)
|
||||||
|
assert_equal("8f9327db2597fa57d2f42b4a6c5a9855", Digest::MD5.file(@post.file_path).hexdigest)
|
||||||
|
assert_equal("https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png", @post.source)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
should "create a post replacement record" do
|
||||||
|
assert_equal(@post.id, PostReplacement.last.post_id)
|
||||||
|
end
|
||||||
|
|
||||||
should "correctly update the attributes" do
|
should "correctly update the attributes" do
|
||||||
assert_equal(@post.id, @upload.post.id)
|
assert_equal(@post.id, @upload.post.id)
|
||||||
assert_equal("completed", @upload.status)
|
assert_equal("completed", @upload.status)
|
||||||
@@ -1670,7 +1700,7 @@ class PostTest < ActiveSupport::TestCase
|
|||||||
assert(File.exists?(old_preview_file_path))
|
assert(File.exists?(old_preview_file_path))
|
||||||
assert(File.exists?(old_large_file_path))
|
assert(File.exists?(old_large_file_path))
|
||||||
|
|
||||||
Timecop.travel(Time.now + Post::DELETION_GRACE_PERIOD + 1.day) do
|
Timecop.travel(Time.now + PostReplacement::DELETION_GRACE_PERIOD + 1.day) do
|
||||||
Delayed::Worker.new.work_off
|
Delayed::Worker.new.work_off
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user