diff --git a/app/models/post_event.rb b/app/models/post_event.rb index d14bbb7bf..82964c7ba 100644 --- a/app/models/post_event.rb +++ b/app/models/post_event.rb @@ -6,16 +6,40 @@ class PostEvent < ApplicationRecord belongs_to :post def self.model_types - %w[Post PostAppeal PostApproval PostDisapproval PostFlag PostReplacement] + %w[Post PostAppeal PostApproval PostDisapproval PostFlag PostReplacement ModAction] + end + + def self.categories + # model_types.excluding("ModAction") + ModAction.categories.keys.grep(/\Apost_(?!permanent_delete|vote)/).map(&:camelize) + %w[Upload Flag Appeal Approval Disapproval Delete Undelete Ban Unban Replacement Regenerate RegenerateIqdb MoveFavorites NoteLockCreate NoteLockDelete RatingLockCreate RatingLockDelete] end def self.visible(user) all end + def self.category_matches(category) + category = category.squish.titleize.delete(" ") + + case category + when "Upload" + where(model_type: "Post") + when "Flag", "Appeal", "Approval", "Disapproval", "Replacement" + where(model_type: "Post" + category) + when *categories + where(model: ModAction.where(category: "post_" + category.underscore)) + else + none + end + end + def self.search(params, current_user) q = search_attributes(params, [:model, :post, :creator, :event_at], current_user: current_user) + if params[:category] + q = q.category_matches(params[:category]) + end + case params[:order] when "event_at_asc" q = q.order(event_at: :asc, model_id: :asc) @@ -34,6 +58,20 @@ class PostEvent < ApplicationRecord [:post, :model] # XXX creator isn't included because it leaks flagger/disapprover names end + def category + if model_type == "Post" + "Upload" + elsif model_type == "ModAction" + model.category.camelize.delete_prefix("Post") + else + model_type.delete_prefix("Post") + end + end + + def pretty_category + category.titleize.delete_prefix("Post ") + end + def readonly? true end diff --git a/app/policies/post_event_policy.rb b/app/policies/post_event_policy.rb index 3677c5058..984dc4660 100644 --- a/app/policies/post_event_policy.rb +++ b/app/policies/post_event_policy.rb @@ -24,8 +24,13 @@ class PostEventPolicy < ApplicationPolicy attr = attr.to_s.gsub("creator", "uploader").to_sym if type == "Post" attr = attr.to_s.gsub("creator", "user").to_sym if type in "PostApproval" | "PostDisapproval" - # XXX ordering by created_at desc is a query planner hack to make Postgres use the right indexes. - events.where(model: type.constantize.visible_for_search(attr, user).order(created_at: :desc)) + if type == "ModAction" + # XXX don't apply visible_for_search to mod actions because it's slow and we know all mod actions are visible + events.where(model_type: "ModAction") + else + # XXX ordering by created_at desc is a query planner hack to make Postgres use the right indexes. + events.where(model: type.constantize.visible_for_search(attr, user).order(created_at: :desc)) + end end.reduce(:or) else events diff --git a/app/views/post_events/_secondary_links.html.erb b/app/views/post_events/_secondary_links.html.erb index 2f89f2640..eecda0817 100644 --- a/app/views/post_events/_secondary_links.html.erb +++ b/app/views/post_events/_secondary_links.html.erb @@ -14,4 +14,5 @@ <%= subnav_link_to "Disapprovals", post_disapprovals_path %> <%= subnav_link_to "Flags", post_flags_path %> <%= subnav_link_to "Replacements", post_replacements_path %> + <%= subnav_link_to "Mod Actions", mod_actions_path(search: { subject_type: "Post" }) %> <% end %> diff --git a/app/views/post_events/index.html.erb b/app/views/post_events/index.html.erb index d4aa55aa6..1241d0864 100644 --- a/app/views/post_events/index.html.erb +++ b/app/views/post_events/index.html.erb @@ -11,7 +11,7 @@ <%= search_form_for(post_events_path) do |f| %> <%= f.input :creator_name, label: "User", input_html: { value: params[:search][:creator_name], "data-autocomplete": "user" } %> <%= f.input :post_tags_match, label: "Tags", input_html: { value: params[:search][:post_tags_match], "data-autocomplete": "tag-query" } %> - <%= f.input :model_type, label: "Category", collection: PostEvent.model_types.map { |type| [type.titleize.delete_prefix("Post "), type] }, include_blank: true, selected: params[:search][:model_type] %> + <%= f.input :category, label: "Category", collection: PostEvent.categories.map { |category| [category.titleize, category] }, include_blank: true, selected: params[:search][:category] %> <%= f.input :order, collection: [%w[Newest event_at], %w[Oldest event_at_asc]], include_blank: true, selected: params[:search][:order] %> <%= f.submit "Search" %> <% end %> @@ -60,11 +60,38 @@ (<%= external_link_to model.original_url.presence || "none", Source::URL.site_name(model.original_url) || model.original_url %> -> <%= external_link_to model.replacement_url, Source::URL.site_name(model.replacement_url) || model.replacement_url %>). <% end %> + <% when "ModAction" %> + <% case model.category %> + <% when "post_ban" %> + <%= link_to post.dtext_shortlink, post %> was banned by <%= link_to_user creator %>. + <% when "post_unban" %> + <%= link_to post.dtext_shortlink, post %> was unbanned by <%= link_to_user creator %>. + <% when "post_delete" %> + <%= link_to post.dtext_shortlink, post %> was deleted by <%= link_to_user creator %>. + <% when "post_undelete" %> + <%= link_to post.dtext_shortlink, post %> was undeleted by <%= link_to_user creator %>. + <% when "post_regenerate" %> + <%= link_to post.dtext_shortlink, post %> had its thumbnails regenerated by <%= link_to_user creator %>. + <% when "post_regenerate_iqdb" %> + <%= link_to post.dtext_shortlink, post %> was reindexed in IQDB by <%= link_to_user creator %>. + <% when "post_rating_lock_create" %> + <%= link_to post.dtext_shortlink, post %> was rating locked by <%= link_to_user creator %>. + <% when "post_rating_lock_delete" %> + <%= link_to post.dtext_shortlink, post %> was rating unlocked by <%= link_to_user creator %>. + <% when "post_note_lock_create" %> + <%= link_to post.dtext_shortlink, post %> was note locked by <%= link_to_user creator %>. + <% when "post_note_lock_delete" %> + <%= link_to post.dtext_shortlink, post %> was note unlocked by <%= link_to_user creator %>. + <% else %> +
+ <%= link_to_user creator %> <%= format_text(model.description.chomp(".").strip, inline: true) %>. +
+ <% end %> <% end %> <% end %> <% t.column "Category" do |event| %> - <%= link_to event.model_type.titleize.delete_prefix("Post "), post_events_path(search: { model_type: event.model_type, **search_params }) %> + <%= link_to event.pretty_category, post_events_path(search: { category: event.category, **search_params }) %> <% end %> <% t.column "User" do |event| %> diff --git a/db/migrate/20220926050108_update_post_events_to_version_2.rb b/db/migrate/20220926050108_update_post_events_to_version_2.rb new file mode 100644 index 000000000..cce556061 --- /dev/null +++ b/db/migrate/20220926050108_update_post_events_to_version_2.rb @@ -0,0 +1,5 @@ +class UpdatePostEventsToVersion2 < ActiveRecord::Migration[7.0] + def change + replace_view :post_events, version: 2, revert_to_version: 1 + end +end diff --git a/db/structure.sql b/db/structure.sql index 763fa35e4..23ae54c55 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -1513,7 +1513,16 @@ UNION ALL post_replacements.post_id, post_replacements.creator_id, post_replacements.created_at AS event_at - FROM public.post_replacements; + FROM public.post_replacements +UNION ALL +( SELECT 'ModAction'::character varying AS model_type, + mod_actions.id AS model_id, + mod_actions.subject_id AS post_id, + mod_actions.creator_id, + mod_actions.created_at AS event_at + FROM public.mod_actions + WHERE ((mod_actions.subject_type)::text = 'Post'::text) + ORDER BY mod_actions.created_at DESC); -- @@ -6739,6 +6748,7 @@ INSERT INTO "schema_migrations" (version) VALUES ('20220922014326'), ('20220923010905'), ('20220924092056'), -('20220925045236'); +('20220925045236'), +('20220926050108'); diff --git a/db/views/post_events_v02.sql b/db/views/post_events_v02.sql new file mode 100644 index 000000000..329bf4dde --- /dev/null +++ b/db/views/post_events_v02.sql @@ -0,0 +1,23 @@ + SELECT 'Post'::character varying AS model_type, id AS model_id, id AS post_id, uploader_id AS creator_id, created_at AS event_at + FROM posts +UNION ALL + SELECT 'PostAppeal'::character varying, id, post_id, creator_id, created_at + FROM post_appeals +UNION ALL + SELECT 'PostApproval'::character varying, id, post_id, user_id, created_at + FROM post_approvals +UNION ALL + SELECT 'PostDisapproval'::character varying, id, post_id, user_id, created_at + FROM post_disapprovals +UNION ALL + SELECT 'PostFlag'::character varying, id, post_id, creator_id, created_at + FROM post_flags +UNION ALL + SELECT 'PostReplacement'::character varying, id, post_id, creator_id, created_at + FROM post_replacements +UNION ALL ( + SELECT 'ModAction'::character varying, id, subject_id, creator_id, created_at + FROM mod_actions + WHERE mod_actions.subject_type = 'Post' + ORDER BY created_at DESC +) diff --git a/test/functional/post_events_controller_test.rb b/test/functional/post_events_controller_test.rb index 9f80d260a..13e612685 100644 --- a/test/functional/post_events_controller_test.rb +++ b/test/functional/post_events_controller_test.rb @@ -13,6 +13,18 @@ class PostEventsControllerTest < ActionDispatch::IntegrationTest @appeal = create(:post_appeal, post: @post, creator: @user) @disapproval = create(:post_disapproval, post: @post, user: @user) @replacement = create(:post_replacement, post: @post, creator: @user) + + create(:mod_action, category: :post_delete, description: "deleted post ##{@post.id}", subject: @post) + create(:mod_action, category: :post_undelete, description: "undeleted post ##{@post.id}", subject: @post) + create(:mod_action, category: :post_ban, description: "banned post ##{@post.id}", subject: @post) + create(:mod_action, category: :post_unban, description: "unbanned post ##{@post.id}", subject: @post) + create(:mod_action, category: :post_move_favorites, description: "moved favorites from post ##{@post.id} to post #1234", subject: @post) + create(:mod_action, category: :post_regenerate, description: "regenerated post ##{@post.id}", subject: @post) + create(:mod_action, category: :post_regenerate_iqdb, description: "regenerated IQDB for post ##{@post.id}", subject: @post) + create(:mod_action, category: :post_note_lock_create, description: "locked notes for post ##{@post.id}", subject: @post) + create(:mod_action, category: :post_note_lock_delete, description: "unlocked notes for post ##{@post.id}", subject: @post) + create(:mod_action, category: :post_rating_lock_create, description: "locked rating for post ##{@post.id}", subject: @post) + create(:mod_action, category: :post_rating_lock_delete, description: "unlocked ratineg for post ##{@post.id}", subject: @post) end should "render for a global listing" do @@ -54,6 +66,11 @@ class PostEventsControllerTest < ActionDispatch::IntegrationTest assert_response :success assert_equal(@flag.creator_id, response.parsed_body.find { |event| event["model_type"] == "PostFlag" }["creator_id"]) end + + should respond_to_search(category: "Upload").with { PostEvent.find_by!(model: @post) } + should respond_to_search(category: "Flag").with { PostEvent.find_by!(model: @flag) } + should respond_to_search(category: "Delete").with { PostEvent.find_by!(model: ModAction.post_delete.first) } + should respond_to_search(category: "Blah").with { [] } end context "for a non-moderator" do @@ -72,6 +89,11 @@ class PostEventsControllerTest < ActionDispatch::IntegrationTest assert_response :success assert_nil(response.parsed_body.find { |event| event["model_type"] == "PostFlag" }["creator_id"]) end + + should respond_to_search(category: "Upload").with { PostEvent.find_by!(model: @post) } + should respond_to_search(category: "Flag").with { PostEvent.find_by!(model: @flag) } + should respond_to_search(category: "Delete").with { PostEvent.find_by!(model: ModAction.post_delete.first) } + should respond_to_search(category: "Blah").with { [] } end end end