[Posts] Save post events in separate table

Part of  #346
This commit is contained in:
Earlopain 2022-01-06 13:44:30 +01:00
parent fb3190a3d9
commit aa77f1a76a
No known key found for this signature in database
GPG Key ID: 6CFB948E15246897
18 changed files with 238 additions and 139 deletions

View File

@ -2,7 +2,9 @@ class PostEventsController < ApplicationController
respond_to :html, :json
def index
@events = PostEvent.find_for_post(params[:post_id])
@events = PostEventDecorator.decorate_collection(
PostEvent.includes(:creator).search(search_params).paginate(params[:page], limit: params[:limit])
)
respond_with(@events)
end
end

View File

@ -23,8 +23,8 @@ class PostFlagsController < ApplicationController
if @post_flag.errors.size > 0
@post = Post.find(params[:post_flag][:post_id])
respond_with(@post_flag)
else
redirect_to post_path(id: @post_flag.post_id)
else
redirect_to post_path(id: @post_flag.post_id)
end
end
end

View File

@ -96,30 +96,6 @@ class ModActionDecorator < ApplicationDecorator
when "created_negative_record"
"Created negative record ##{vals['record_id']} for #{user} with reason: #{vals['reason']}"
### Post ###
when "post_move_favorites"
"Moves favorites from post ##{vals['post_id']} to post ##{vals['parent_id']}"
when "post_delete"
"Deleted post ##{vals['post_id']} with reason: #{vals['reason']}"
when "post_undelete"
"Undeleted post ##{vals['post_id']}"
when "post_destroy"
"Destroyed post ##{vals['post_id']}"
when "post_rating_lock"
"Post rating was #{vals['locked'] ? 'locked' : 'unlocked'} on post ##{vals['post_id']}"
when "post_unapprove"
"Unapproved post ##{vals['post_id']}"
### Post Replacements ###
when "post_replacement_accept"
"Post replacement for post ##{vals['post_id']} was accepted"
when "post_replacement_reject"
"Post replacement for post ##{vals['post_id']} was rejected"
when "post_replacement_delete"
"Post replacement for post ##{vals['post_id']} was deleted"
### Set ###
when "set_mark_private"
@ -317,6 +293,26 @@ class ModActionDecorator < ApplicationDecorator
when "bulk_revert"
"Processed bulk revert for #{vals['constraints']} by #{user}"
### Legacy Post Events ###
when "post_move_favorites"
"Moves favorites from post ##{vals['post_id']} to post ##{vals['parent_id']}"
when "post_delete"
"Deleted post ##{vals['post_id']} with reason: #{vals['reason']}"
when "post_undelete"
"Undeleted post ##{vals['post_id']}"
when "post_destroy"
"Destroyed post ##{vals['post_id']}"
when "post_rating_lock"
"Post rating was #{vals['locked'] ? 'locked' : 'unlocked'} on post ##{vals['post_id']}"
when "post_unapprove"
"Unapproved post ##{vals['post_id']}"
when "post_replacement_accept"
"Post replacement for post ##{vals['post_id']} was accepted"
when "post_replacement_reject"
"Post replacement for post ##{vals['post_id']} was rejected"
when "post_replacement_delete"
"Post replacement for post ##{vals['post_id']} was deleted"
else
CurrentUser.is_admin? ? "Unknown action #{object.action}: #{object.values.inspect}" : "Unknown action #{object.action}"

View File

@ -0,0 +1,16 @@
class PostEventDecorator < ApplicationDecorator
delegate_all
def format_description
vals = object.extra_data
case object.action
when "deleted", "flag_created"
"Reason: #{vals['reason']}"
when "favorites_moved"
"Target: post ##{vals['parent_id']}"
when "favorites_recieved"
"Source: post ##{vals['child_id']}"
end
end
end

View File

@ -22,10 +22,10 @@ class TakedownJob
@takedown.actual_posts.find_each do |p|
if @takedown.should_delete(p.id)
next if p.is_deleted?
p.delete!("takedown ##{@takedown.id}: #{del_reason}", {force: true, without_mod_action: true})
p.delete!("takedown ##{@takedown.id}: #{del_reason}", { force: true })
else
next unless p.is_deleted?
p.undelete!({force: true, without_mod_action: true})
p.undelete!({ force: true })
end
end
end

View File

@ -2,7 +2,6 @@ class TransferFavoritesJob < ApplicationJob
queue_as :low_prio
def perform(*args)
without_mod_action = args[2]
@post = Post.find_by(id: args[0])
@user = User.find_by(id: args[1])
unless @post && @user
@ -11,8 +10,7 @@ class TransferFavoritesJob < ApplicationJob
end
CurrentUser.as(@user) do
@post.give_favorites_to_parent!(without_mod_action: without_mod_action)
@post.give_favorites_to_parent!
end
end
end

View File

@ -39,15 +39,6 @@ class ModAction < ApplicationRecord
:ip_ban_delete,
:pool_delete,
:pool_undelete,
:post_move_favorites,
:post_destroy,
:post_delete,
:post_undelete,
:post_unapprove,
:post_rating_lock,
:post_replacement_accept,
:post_replacement_reject,
:post_replacement_delete,
:report_reason_create,
:report_reason_delete,
:report_reason_update,

View File

@ -33,7 +33,7 @@ class Post < ApplicationRecord
validate :updater_can_change_rating
before_save :update_tag_post_counts, if: :should_process_tags?
before_save :set_tag_counts, if: :should_process_tags?
after_save :create_rating_lock_mod_action, if: :saved_change_to_is_rating_locked?
after_save :create_rating_lock_post_event, if: :saved_change_to_is_rating_locked?
after_save :create_version
after_save :update_parent_on_save
after_save :apply_post_metatags
@ -364,6 +364,7 @@ class Post < ApplicationRecord
def unflag!
flags.each(&:resolve!)
update(is_flagged: false)
PostEvent.add(id, :flag_removed)
end
def appeal!(reason)
@ -382,16 +383,14 @@ class Post < ApplicationRecord
approver == user || approvals.where(user: user).exists?
end
def unapprove!(unapprover = CurrentUser.user)
ModAction.log(:post_unapprove, {post_id: id})
def unapprove!
PostEvent.add(id, :unapproved)
update(approver: nil, is_pending: true)
end
def approve!(approver = CurrentUser.user, force: false)
raise ApprovalError.new("Post already approved.") if self.approver != nil && !force
if is_deleted?
ModAction.log(:post_undelete, {post_id: id})
end
PostEvent.add(id, :approved)
approv = approvals.create(user: approver)
flags.each(&:resolve!)
@ -1464,18 +1463,16 @@ class Post < ApplicationRecord
Post.find(parent_id_before_last_save).update_has_children_flag if parent_id_before_last_save.present?
end
def give_favorites_to_parent(options = {})
TransferFavoritesJob.perform_later(id, CurrentUser.id, options[:without_mod_action])
def give_favorites_to_parent
TransferFavoritesJob.perform_later(id, CurrentUser.id)
end
def give_favorites_to_parent!(options = {})
def give_favorites_to_parent!
return if parent.nil?
FavoriteManager.give_to_parent!(self)
unless options[:without_mod_action]
ModAction.log(:post_move_favorites, {post_id: id, parent_id: parent_id})
end
PostEvent.add(id, :favorites_moved, { parent_id: parent_id })
PostEvent.add(parent_id, :favorites_recieved, { child_id: id })
end
def parent_exists?
@ -1540,7 +1537,7 @@ class Post < ApplicationRecord
transaction do
Post.without_timeout do
ModAction.log(:post_destroy, {post_id: id, md5: md5})
PostEvent.add(id, :expunged)
update_children_on_destroy
decrement_tag_post_counts
@ -1587,16 +1584,14 @@ class Post < ApplicationRecord
is_flagged: false
)
move_files_on_delete
unless options[:without_mod_action]
ModAction.log(:post_delete, {post_id: id, reason: reason})
end
PostEvent.add(id, :deleted, { reason: reason })
end
end
# XXX This must happen *after* the `is_deleted` flag is set to true (issue #3419).
# We don't care if these fail per-se so they are outside the transaction.
UserStatus.for_user(uploader_id).update_all("post_deleted_count = post_deleted_count + 1")
give_favorites_to_parent(options) if options[:move_favorites]
give_favorites_to_parent if options[:move_favorites]
give_post_sets_to_parent if options[:move_favorites]
reject_pending_replacements
end
@ -1623,9 +1618,7 @@ class Post < ApplicationRecord
flags.each {|x| x.resolve!}
save
approvals.create(user: CurrentUser.user)
unless options[:without_mod_action]
ModAction.log(:post_undelete, {post_id: id})
end
PostEvent.add(id, :undeleted)
end
move_files_on_undelete
UserStatus.for_user(uploader_id).update_all("post_deleted_count = post_deleted_count - 1")
@ -1992,8 +1985,9 @@ class Post < ApplicationRecord
end
module RatingMethods
def create_rating_lock_mod_action
ModAction.log(:post_rating_lock, {locked: is_rating_locked?, post_id: id})
def create_rating_lock_post_event
action = is_rating_locked? ? :rating_locked : :rating_unlocked
PostEvent.add(id, action)
end
end

View File

@ -1,71 +1,74 @@
class PostEvent
include ActiveModel::Model
include ActiveModel::Serializers::JSON
include ActiveModel::Serializers::Xml
class PostEvent < ApplicationRecord
belongs_to :creator, class_name: "User"
before_validation :initialize_creator, on: :create
enum action: {
deleted: 0,
undeleted: 1,
approved: 2,
unapproved: 3,
favorites_moved: 4,
favorites_recieved: 5,
rating_locked: 6,
rating_unlocked: 7,
flag_created: 8,
flag_removed: 9,
replacement_accepted: 10,
replacement_rejected: 11,
replacement_deleted: 12,
expunged: 13
}
attr_accessor :event
delegate :created_at, to: :event
def self.find_for_post(post_id)
post = Post.find(post_id)
(post.appeals + post.flags + post.approvals).sort_by(&:created_at).reverse.map { |e| new(event: e) }
def self.add(post_id, action, data = {})
create!(post_id: post_id, action: action.to_s, extra_data: data)
end
def type_name
case event
when PostFlag
"flag"
when PostAppeal
"appeal"
when PostApproval
"approval"
end
end
def type
type_name.first
end
def reason
event.try(:reason) || ""
end
def is_resolved
event.try(:is_resolved) || false
end
def creator_id
event.try(:creator_id) || event.try(:user_id)
end
def creator
event.try(:creator) || event.try(:user)
end
def is_creator_visible?(user = CurrentUser.user)
case event
when PostAppeal, PostApproval
def is_creator_visible?(user)
case action
when "flag_created"
user.can_view_flagger?(creator_id)
else
true
when PostFlag
flag = event
user.can_view_flagger_on_post?(flag)
end
end
def attributes
{
"creator_id": nil,
"created_at": nil,
"reason": nil,
"is_resolved": nil,
"type": nil,
}
def self.for_user(q, user_id)
q = q.where("creator_id = ?", user_id)
unless CurrentUser.can_view_flagger?(user_id)
q = q.where.not(action: actions[:flag_created])
end
q
end
# XXX can't use hidden_attributes because we don't inherit from ApplicationRecord.
def serializable_hash(**options)
hash = super
hash = hash.except(:creator_id) unless is_creator_visible?
hash
def self.search(params)
q = super
if params[:post_id].present?
q = q.where("post_id = ?", params[:post_id].to_i)
end
if params[:creator_name].present?
creator_id = User.name_to_id(params[:creator_name].strip)
q = for_user(q, creator_id.to_i)
end
if params[:creator_id].present?
q = for_user(q, params[:creator_id].to_i)
end
if params[:action].present?
q = q.where('action = ?', actions[params[:action]])
end
q.apply_default_order(params)
end
def initialize_creator
self.creator_id = CurrentUser.id
end
def hidden_attributes
hidden = super + [:extra_data]
hidden += [:creator_id] unless is_creator_visible?(CurrentUser.user)
hidden
end
end

View File

@ -20,6 +20,7 @@ class PostFlag < ApplicationRecord
validate :update_reason, on: :create
validates :reason, presence: true
before_save :update_post
after_create :create_post_event
after_commit :index_post
scope :by_users, -> { where.not(creator: User.system) }
@ -247,4 +248,9 @@ class PostFlag < ApplicationRecord
nil
end
end
def create_post_event
# Deletions also create flags, but they create a deletion event instead
PostEvent.add(id, :flag_created, { reason: reason }) unless is_deletion
end
end

View File

@ -97,7 +97,7 @@ class PostReplacement < ApplicationRecord
module StorageMethods
def remove_files
ModAction.log(:post_replacement_delete, {id: id, post_id: post_id, md5: md5, storage_id: storage_id})
PostEvent.add(post_id, :replacement_deleted, { replacement_id: id, md5: md5, storage_id: storage_id})
Danbooru.config.storage_manager.delete_replacement(self)
end
@ -181,7 +181,7 @@ class PostReplacement < ApplicationRecord
end
transaction do
ModAction.log(:post_replacement_accept, {post_id: post.id, replacement_id: self.id, old_md5: post.md5, new_md5: self.md5})
PostEvent.add(post.id, :replacement_accepted, { replacement_id: id, old_md5: post.md5, new_md5: md5 })
processor = UploadService::Replacer.new(post: post, replacement: self)
processor.process!(penalize_current_uploader: penalize_current_uploader)
end
@ -223,7 +223,7 @@ class PostReplacement < ApplicationRecord
return
end
ModAction.log(:post_replacement_reject, {post_id: post.id, replacement_id: self.id})
PostEvent.add(post.id, :replacement_rejected, { replacement_id: id })
update_attribute(:status, 'rejected')
UserStatus.for_user(creator_id).update_all("post_replacement_rejected_count = post_replacement_rejected_count + 1")
post.update_index

View File

@ -0,0 +1,6 @@
<%= hideable_form_search path: post_events_path do |f| %>
<%= f.input :post_id, label: "Post #", input_html: {value: params.dig(:search, :post_id)} %>
<%= f.input :creator_name, label: "Creator", input_html: { value: params.dig(:search, :creator_name), data: { autocomplete: "user" } } %>
<%= f.input :action, label: "Action", collection: PostEvent.actions.keys.map {|k| [k.to_s.capitalize.tr("_"," "), k.to_s]}, include_blank: true, selected: params.dig(:search, :action) %>
<%= f.submit "Search" %>
<% end %>

View File

@ -0,0 +1,9 @@
<% content_for(:secondary_links) do %>
<menu>
<%= subnav_link_to "Flags", post_flags_path %>
<%= subnav_link_to "Approvals", post_approvals_path %>
<%= subnav_link_to "Deletions", deleted_posts_path %>
<%= subnav_link_to "Replacements", post_replacements_path %>
<%= subnav_link_to "Tag History", post_versions_path %>
</menu>
<% end %>

View File

@ -2,29 +2,31 @@
<div id="a-index">
<h1>Post Events</h1>
<%= render "search" %>
<table width="100%" class="table autofit">
<thead>
<tr>
<th>Post</th>
<th>Type</th>
<th>User</th>
<th>Description</th>
<th>Resolved?</th>
</tr>
</thead>
<tbody>
<% @events.each do |event| %>
<tr class="resolved-<%= event.is_resolved %>">
<td><%= event.type_name %></td>
<tr>
<td> <%= link_to "post ##{event.post_id}", post_path(event.post_id) %> </td>
<td><%= event.action.capitalize.tr("_"," ") %></td>
<td>
<% if event.is_creator_visible? %>
<% if event.is_creator_visible?(CurrentUser.user) %>
<%= link_to_user event.creator %>
<% else %>
<i>hidden</i>
<% end %>
<br><%= time_ago_in_words_tagged event.created_at %>
</td>
<td class="col-expand dtext-container"><%= format_text event.reason %></td>
<td><%= event.is_resolved %></td>
<td class="col-expand dtext-container"><%= format_text event.format_description %></td>
</tr>
<% end %>
</tbody>
@ -32,6 +34,8 @@
</div>
</div>
<%= render "secondary_links" %>
<% content_for(:page_title) do %>
Events
<% end %>

View File

@ -24,10 +24,9 @@
<section id="post-history">
<h1>History</h1>
<ul>
<li><%= fast_link_to "Tags/Desc", post_versions_path(:search => {:post_id => @post.id}) %></li>
<li><%= fast_link_to "Notes", note_versions_path(:search => {:post_id => @post.id}) %></li>
<li><%= fast_link_to "Moderation", post_events_path(@post.id) %></li>
<li><%= fast_link_to "Replacements", post_replacements_path(:search => {:post_id => @post.id}) %></li>
<li><%= fast_link_to "Tags/Desc", post_versions_path(search: { post_id: @post.id }) %></li>
<li><%= fast_link_to "Notes", note_versions_path(search: { post_id: @post.id }) %></li>
<li><%= fast_link_to "Events", post_events_path(search: { post_id: @post.id }) %></li>
</ul>
</section>

View File

@ -256,7 +256,6 @@ Rails.application.routes.draw do
end
resources :deleted_posts, only: [:index]
resources :posts, :only => [:index, :show, :update] do
resources :events, :only => [:index], :controller => "post_events"
resources :replacements, :only => [:index, :new, :create], :controller => "post_replacements"
resource :votes, :controller => "post_votes", :only => [:create, :destroy]
resource :flag, controller: 'post_flags', only: [:destroy]
@ -280,6 +279,7 @@ Rails.application.routes.draw do
post :delete
end
end
resources :post_events, only: :index
resources :post_appeals
resources :post_flags, except: [:destroy]
resources :post_approvals, only: [:index]

View File

@ -0,0 +1,11 @@
class AddPostEventsTable < ActiveRecord::Migration[6.1]
def change
create_table :post_events do |t|
t.references :creator, foreign_key: { to_table: :users }, null: false
t.references :post, null: false
t.integer :action, null: false
t.jsonb :extra_data, null: false
t.datetime :created_at, null: false
end
end
end

View File

@ -1520,6 +1520,39 @@ CREATE SEQUENCE public.post_disapprovals_id_seq
ALTER SEQUENCE public.post_disapprovals_id_seq OWNED BY public.post_disapprovals.id;
--
-- Name: post_events; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.post_events (
id bigint NOT NULL,
creator_id bigint NOT NULL,
post_id bigint NOT NULL,
action integer NOT NULL,
extra_data jsonb NOT NULL,
created_at timestamp without time zone NOT NULL
);
--
-- Name: post_events_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.post_events_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: post_events_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.post_events_id_seq OWNED BY public.post_events.id;
--
-- Name: post_flags; Type: TABLE; Schema: public; Owner: -
--
@ -3011,6 +3044,13 @@ ALTER TABLE ONLY public.post_approvals ALTER COLUMN id SET DEFAULT nextval('publ
ALTER TABLE ONLY public.post_disapprovals ALTER COLUMN id SET DEFAULT nextval('public.post_disapprovals_id_seq'::regclass);
--
-- Name: post_events id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.post_events ALTER COLUMN id SET DEFAULT nextval('public.post_events_id_seq'::regclass);
--
-- Name: post_flags id; Type: DEFAULT; Schema: public; Owner: -
--
@ -3533,6 +3573,14 @@ ALTER TABLE ONLY public.post_disapprovals
ADD CONSTRAINT post_disapprovals_pkey PRIMARY KEY (id);
--
-- Name: post_events post_events_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.post_events
ADD CONSTRAINT post_events_pkey PRIMARY KEY (id);
--
-- Name: post_flags post_flags_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
@ -4412,6 +4460,20 @@ CREATE INDEX index_post_disapprovals_on_post_id ON public.post_disapprovals USIN
CREATE INDEX index_post_disapprovals_on_user_id ON public.post_disapprovals USING btree (user_id);
--
-- Name: index_post_events_on_creator_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_post_events_on_creator_id ON public.post_events USING btree (creator_id);
--
-- Name: index_post_events_on_post_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_post_events_on_post_id ON public.post_events USING btree (post_id);
--
-- Name: index_post_flags_on_creator_id; Type: INDEX; Schema: public; Owner: -
--
@ -5268,6 +5330,8 @@ INSERT INTO "schema_migrations" (version) VALUES
('20210426025625'),
('20210430201028'),
('20210506235640'),
('20210625155528'),
('20210718172512'),
('20210625155528');
('20220106081415');