diff --git a/app/controllers/post_approvals_controller.rb b/app/controllers/post_approvals_controller.rb
index f2be0294a..3fbf7a141 100644
--- a/app/controllers/post_approvals_controller.rb
+++ b/app/controllers/post_approvals_controller.rb
@@ -15,4 +15,12 @@ class PostApprovalsController < ApplicationController
respond_with(@post_approvals)
end
+
+ def show
+ @approval = authorize PostApproval.find(params[:id])
+
+ respond_with(@approval) do |format|
+ format.html { redirect_to post_approvals_path(search: { id: @approval.id }) }
+ end
+ end
end
diff --git a/app/controllers/post_events_controller.rb b/app/controllers/post_events_controller.rb
index 1e1c5e65e..5bb779a4d 100644
--- a/app/controllers/post_events_controller.rb
+++ b/app/controllers/post_events_controller.rb
@@ -4,7 +4,13 @@ class PostEventsController < ApplicationController
respond_to :html, :xml, :json
def index
- @events = PostEvent.find_for_post(params[:post_id])
- respond_with(@events)
+ if post_id = params[:post_id] || params.dig(:search, :post_id)
+ @post = Post.find(post_id)
+ end
+
+ @post_events = authorize PostEvent.paginated_search(params, defaults: { post_id: @post&.id }, count_pages: @post.present?)
+ @post_events = @post_events.includes(:creator, :post, model: [:post]) if request.format.html?
+
+ respond_with(@post_events)
end
end
diff --git a/app/controllers/post_replacements_controller.rb b/app/controllers/post_replacements_controller.rb
index 05ec2a175..8720a0a52 100644
--- a/app/controllers/post_replacements_controller.rb
+++ b/app/controllers/post_replacements_controller.rb
@@ -35,4 +35,12 @@ class PostReplacementsController < ApplicationController
respond_with(@post_replacements)
end
+
+ def show
+ @post_replacement = authorize PostReplacement.find(params[:id])
+
+ respond_with(@post_replacement) do |format|
+ format.html { redirect_to post_replacements_path(search: { id: @post_replacement.id }) }
+ end
+ end
end
diff --git a/app/logical/bigquery_export_service.rb b/app/logical/bigquery_export_service.rb
index 4f17b0b2f..6a0801bd6 100644
--- a/app/logical/bigquery_export_service.rb
+++ b/app/logical/bigquery_export_service.rb
@@ -41,7 +41,7 @@ class BigqueryExportService
Rails.application.eager_load!
models = ApplicationRecord.descendants.sort_by(&:name)
- models -= [GoodJob::BaseRecord, GoodJob::Process, GoodJob::Execution, GoodJob::ActiveJobJob, GoodJob::Job, GoodJob::Setting, TagRelationship, ArtistVersion, ArtistCommentaryVersion, NoteVersion, PoolVersion, PostVersion, WikiPageVersion, Post, PostVote, MediaAsset, Favorite, AITag]
+ models -= [GoodJob::BaseRecord, GoodJob::Process, GoodJob::Execution, GoodJob::ActiveJobJob, GoodJob::Job, GoodJob::Setting, TagRelationship, ArtistVersion, ArtistCommentaryVersion, NoteVersion, PoolVersion, PostVersion, WikiPageVersion, Post, PostEvent, PostVote, MediaAsset, Favorite, AITag, UserAction]
models
end
diff --git a/app/logical/source/url.rb b/app/logical/source/url.rb
index 6423080ba..4cac2fd7e 100644
--- a/app/logical/source/url.rb
+++ b/app/logical/source/url.rb
@@ -185,6 +185,10 @@ module Source
nil
end
+ def self.site_name(url)
+ Source::URL.parse(url)&.site_name
+ end
+
def self.image_url?(url)
Source::URL.parse(url)&.image_url?
end
diff --git a/app/models/post.rb b/app/models/post.rb
index f2f1d6f3d..34327ab45 100644
--- a/app/models/post.rb
+++ b/app/models/post.rb
@@ -70,6 +70,7 @@ class Post < ApplicationRecord
has_many :favorites, dependent: :destroy
has_many :replacements, class_name: "PostReplacement", :dependent => :destroy
has_many :ai_tags, through: :media_asset
+ has_many :events, class_name: "PostEvent"
attr_accessor :old_tag_string, :old_parent_id, :old_source, :old_rating, :has_constraints, :disable_versioning, :post_edit
@@ -1631,7 +1632,7 @@ class Post < ApplicationRecord
def self.available_includes
# attributes accessible through the ?only= parameter
%i[
- uploader approver flags appeals parent children notes
+ uploader approver flags appeals events parent children notes
comments approvals disapprovals replacements pixiv_ugoira_frame_data
artist_commentary media_asset ai_tags
]
diff --git a/app/models/post_event.rb b/app/models/post_event.rb
index 6ae0cd6dc..d14bbb7bf 100644
--- a/app/models/post_event.rb
+++ b/app/models/post_event.rb
@@ -1,82 +1,40 @@
# frozen_string_literal: true
-class PostEvent
- include ActiveModel::Model
- include ActiveModel::Serializers::JSON
- include ActiveModel::Serializers::Xml
+class PostEvent < ApplicationRecord
+ belongs_to :model, polymorphic: true
+ belongs_to :creator, class_name: "User"
+ belongs_to :post
- 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.model_types
+ %w[Post PostAppeal PostApproval PostDisapproval PostFlag PostReplacement]
end
- def type_name
- case event
- when PostFlag
- "flag"
- when PostAppeal
- "appeal"
- when PostApproval
- "approval"
- end
+ def self.visible(user)
+ all
end
- def type
- type_name.first
- end
+ def self.search(params, current_user)
+ q = search_attributes(params, [:model, :post, :creator, :event_at], current_user: current_user)
- def reason
- event.try(:reason) || ""
- end
-
- def creator_id
- event.try(:creator_id) || event.try(:user_id)
- end
-
- def creator
- event.try(:creator) || event.try(:user)
- end
-
- def status
- if event.is_a?(PostApproval)
- "approved"
- elsif (event.is_a?(PostAppeal) && event.succeeded?) || (event.is_a?(PostFlag) && event.rejected?)
- "approved"
- elsif (event.is_a?(PostAppeal) && event.rejected?) || (event.is_a?(PostFlag) && event.succeeded?)
- "deleted"
+ case params[:order]
+ when "event_at_asc"
+ q = q.order(event_at: :asc, model_id: :asc)
else
- "pending"
+ q = q.apply_default_order(params)
end
+
+ q
end
- def is_creator_visible?(user = CurrentUser.user)
- case event
- when PostAppeal, PostApproval
- true
- when PostFlag
- flag = event
- Pundit.policy!(user, flag).can_view_flagger?
- end
+ def self.default_order
+ order(event_at: :desc, model_id: :desc)
end
- def attributes
- {
- creator_id: nil,
- created_at: nil,
- reason: nil,
- status: nil,
- type: nil,
- }
+ def self.available_includes
+ [:post, :model] # XXX creator isn't included because it leaks flagger/disapprover names
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 readonly?
+ true
end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 8d069c525..96c8156e2 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -115,6 +115,7 @@ class User < ApplicationRecord
has_many :post_appeals, foreign_key: :creator_id
has_many :post_approvals, :dependent => :destroy
has_many :post_disapprovals, :dependent => :destroy
+ has_many :post_events, class_name: "PostEvent", foreign_key: :creator_id
has_many :post_flags, foreign_key: :creator_id
has_many :post_votes
has_many :post_versions, foreign_key: :updater_id
diff --git a/app/policies/post_event_policy.rb b/app/policies/post_event_policy.rb
new file mode 100644
index 000000000..3677c5058
--- /dev/null
+++ b/app/policies/post_event_policy.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+class PostEventPolicy < ApplicationPolicy
+ def can_see_creator?
+ case event.model_type
+ when "PostFlag"
+ policy(event.model).can_view_flagger?
+ when "PostDisapproval"
+ policy(event.model).can_view_creator?
+ else
+ true
+ end
+ end
+
+ def api_attributes
+ [:model_type, :model_id, :post_id, (:creator_id if can_see_creator?), :event_at].compact
+ end
+
+ def visible_for_search(events, attribute)
+ case attribute
+ in :creator | :creator_id
+ events.model_types.map do |type|
+ attr = attribute
+ 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))
+ end.reduce(:or)
+ else
+ events
+ end
+ end
+
+ alias_method :event, :record
+end
diff --git a/app/views/post_appeals/index.html.erb b/app/views/post_appeals/index.html.erb
index de0196ccb..4610f4810 100644
--- a/app/views/post_appeals/index.html.erb
+++ b/app/views/post_appeals/index.html.erb
@@ -43,3 +43,5 @@
<%= numbered_paginator(@post_appeals) %>
+
+<%= render "post_events/secondary_links" %>
diff --git a/app/views/post_approvals/index.html.erb b/app/views/post_approvals/index.html.erb
index 17b5aa52e..d1497a7da 100644
--- a/app/views/post_approvals/index.html.erb
+++ b/app/views/post_approvals/index.html.erb
@@ -23,3 +23,5 @@
<%= numbered_paginator(@post_approvals) %>
+
+<%= render "post_events/secondary_links" %>
diff --git a/app/views/post_disapprovals/index.html.erb b/app/views/post_disapprovals/index.html.erb
index c7b2779c7..0f5696864 100644
--- a/app/views/post_disapprovals/index.html.erb
+++ b/app/views/post_disapprovals/index.html.erb
@@ -39,3 +39,5 @@
<%= numbered_paginator(@post_disapprovals) %>
+
+<%= render "post_events/secondary_links" %>
diff --git a/app/views/post_events/_secondary_links.html.erb b/app/views/post_events/_secondary_links.html.erb
new file mode 100644
index 000000000..2f89f2640
--- /dev/null
+++ b/app/views/post_events/_secondary_links.html.erb
@@ -0,0 +1,17 @@
+<% content_for(:secondary_links) do %>
+ <% case controller_name %>
+ <% when "post_approvals" %>
+ <%= quick_search_form_for(:user_name, post_approvals_path, "user", autocomplete: "user") %>
+ <% when "post_disapprovals" %>
+ <%= quick_search_form_for(:user_name, post_disapprovals_path, "user", autocomplete: "user") %>
+ <% else %>
+ <%= quick_search_form_for(:creator_name, url_for, "user", autocomplete: "user") %>
+ <% end %>
+
+ <%= subnav_link_to "Events", post_events_path %>
+ <%= subnav_link_to "Appeals", post_appeals_path %>
+ <%= subnav_link_to "Approvals", post_approvals_path %>
+ <%= subnav_link_to "Disapprovals", post_disapprovals_path %>
+ <%= subnav_link_to "Flags", post_flags_path %>
+ <%= subnav_link_to "Replacements", post_replacements_path %>
+<% end %>
diff --git a/app/views/post_events/index.html.erb b/app/views/post_events/index.html.erb
index 02c9ae080..d4aa55aa6 100644
--- a/app/views/post_events/index.html.erb
+++ b/app/views/post_events/index.html.erb
@@ -1,25 +1,106 @@
-
Post Events
+ <% if @post %>
+
Events: <%= link_to @post.dtext_shortlink, @post %>
+ <%= link_to "« Back", post_events_path, class: "text-xs" %>
+ <% else %>
+
Events
+ <% end %>
- <%= table_for @events, class: "striped autofit", width: "100%" do |t| %>
- <% t.column :type_name, name: "Type" %>
- <% t.column "Description", td: { class: "col-expand" } do |event| %>
-
- <%= format_text event.reason %>
-
+ <% unless @post %>
+ <%= 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 :order, collection: [%w[Newest event_at], %w[Oldest event_at_asc]], include_blank: true, selected: params[:search][:order] %>
+ <%= f.submit "Search" %>
<% end %>
- <% t.column "Status" do |event| %>
- <%= event.status %>
+ <% end %>
+
+ <%= table_for @post_events, class: "striped autofit mt-4", width: "100%" do |t| %>
+ <% t.column "Event", td: { class: "col-expand" } do |event| %>
+ <% post = event.post %>
+ <% model = event.model %>
+ <% creator = event.creator %>
+
+ <% case event.model_type %>
+ <% when "Post" %>
+ <%= link_to post.dtext_shortlink, post %> was uploaded by <%= link_to_user creator %>.
+ <% when "PostAppeal" %>
+
+ <%= link_to post.dtext_shortlink, post %> was appealed by <%= link_to_user creator %><%= " (#{format_text(model.reason.strip.chomp("."), inline: true)})".html_safe if model.reason.present? %>.
+
+ <% when "PostApproval" %>
+ <%= link_to post.dtext_shortlink, post %> was approved by <%= link_to_user creator %>.
+ <% when "PostDisapproval" %>
+
+ <% if policy(model).can_view_creator? %>
+ <%= link_to post.dtext_shortlink, post %> was disapproved by <%= link_to_user creator %> (<%= model.reason.titleize.downcase %><%= ": ".html_safe + format_text(model.message.strip.chomp("."), inline: true) if model.message.present? %>).
+ <% else %>
+ <%= link_to post.dtext_shortlink, post %> was disapproved (<%= model.reason.titleize.downcase %><%= ": ".html_safe + format_text(model.message.strip.chomp("."), inline: true) if model.message.present? %>).
+ <% end %>
+
+ <% when "PostFlag" %>
+
+ <% if policy(model).can_view_flagger? %>
+ <%= link_to post.dtext_shortlink, post %> was flagged by <%= link_to_user creator %> (<%= format_text(model.reason.strip.chomp("."), inline: true) %>).
+ <% else %>
+ <%= link_to post.dtext_shortlink, post %> was flagged (<%= format_text(model.reason.strip.chomp("."), inline: true) %>).
+ <% end %>
+
+ <% when "PostReplacement" %>
+ <% if model.old_file_size && model.old_file_ext && model.old_image_width && model.old_image_height && model.file_size && model.file_ext && model.image_width && model.image_height %>
+ <%= link_to post.dtext_shortlink, post %> was replaced by <%= link_to_user creator %>
+ (<%= external_link_to model.original_url.presence || "none", Source::URL.site_name(model.original_url) || model.original_url %>,
+ <%= model.old_file_size.to_formatted_s(:human_size, precision: 4) %> .<%= model.old_file_ext %>, <%= model.old_image_width %>x<%= model.old_image_height %> ->
+ <%= external_link_to model.replacement_url, Source::URL.site_name(model.replacement_url) || model.replacement_url %>,
+ <%= model.file_size.to_formatted_s(:human_size, precision: 4) %> .<%= model.file_ext %>, <%= model.image_width %>x<%= model.image_height %>).
+ <% else %>
+ <%= link_to post.dtext_shortlink, post %> was replaced by <%= link_to_user creator %>
+ (<%= 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 %>
+ <% 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 }) %>
+ <% end %>
+
<% t.column "User" do |event| %>
- <% if event.is_creator_visible? %>
- <%= link_to_user event.creator %>
+ <% if policy(event).can_see_creator? %>
+ <%= link_to_user event.creator %> <%= link_to "»", post_events_path(search: { **search_params, creator_name: event.creator.name }) %>
<% else %>
hidden
<% end %>
-
<%= time_ago_in_words_tagged event.created_at %>
+
<%= time_ago_in_words_tagged(event.event_at) %>
+ <% end %>
+
+ <% t.column column: "control" do |event| %>
+ <%= render PopupMenuComponent.new do |menu| %>
+ <% unless @post %>
+ <% menu.item do %>
+ <%= link_to "Post history", post_post_events_path(event.post) %>
+ <% end %>
+ <% end %>
+
+ <% if policy(event).can_see_creator? %>
+ <% menu.item do %>
+ <%= link_to "User history", post_events_path(search: { creator_name: event.creator.name }) %>
+ <% end %>
+ <% end %>
+
+ <% if policy(event).can_see_creator? %>
+ <% menu.item do %>
+ <%= link_to "Details", event.model %>
+ <% end %>
+ <% end %>
+ <% end %>
<% end %>
<% end %>
+
+ <%= numbered_paginator(@post_events) %>
+
+<%= render "secondary_links" %>
diff --git a/app/views/post_flags/index.html.erb b/app/views/post_flags/index.html.erb
index 715fc938e..b32bd628d 100644
--- a/app/views/post_flags/index.html.erb
+++ b/app/views/post_flags/index.html.erb
@@ -48,3 +48,5 @@
<%= numbered_paginator(@post_flags) %>
+
+<%= render "post_events/secondary_links" %>
diff --git a/app/views/post_replacements/index.html.erb b/app/views/post_replacements/index.html.erb
index 67e7893a2..7c30ba9ae 100644
--- a/app/views/post_replacements/index.html.erb
+++ b/app/views/post_replacements/index.html.erb
@@ -66,3 +66,5 @@
<%= numbered_paginator(@post_replacements) %>
+
+<%= render "post_events/secondary_links" %>
diff --git a/app/views/posts/show.html.erb b/app/views/posts/show.html.erb
index 8b0b8099c..20aa680e1 100644
--- a/app/views/posts/show.html.erb
+++ b/app/views/posts/show.html.erb
@@ -188,9 +188,8 @@
<%= link_to "Tags", post_versions_path(search: { post_id: @post.id }) %>
<%= link_to "Pools", pool_versions_path(search: { post_id: @post.id }) %>
<%= link_to "Notes", note_versions_path(search: { post_id: @post.id }) %>
- <%= link_to "Moderation", post_events_path(@post.id) %>
+ <%= link_to "Moderation", post_post_events_path(@post.id) %>
- <%= link_to "Replacements", post_replacements_path(search: {post_id: @post.id }) %>
<% end %>
diff --git a/app/views/static/site_map.html.erb b/app/views/static/site_map.html.erb
index 6e091726e..cb811ed8a 100644
--- a/app/views/static/site_map.html.erb
+++ b/app/views/static/site_map.html.erb
@@ -18,9 +18,10 @@
Post Events
- <%= link_to("Tag History", post_versions_path) %>
+ - <%= link_to("Events", post_events_path) %>
+ - <%= link_to("Appeals", post_appeals_path) %>
- <%= link_to("Approvals", post_approvals_path) %>
- <%= link_to("Disapprovals", post_disapprovals_path) %>
- - <%= link_to("Appeals", post_appeals_path) %>
- <%= link_to("Flags", post_flags_path) %>
- <%= link_to("Replacements", post_replacements_path) %>
diff --git a/config/routes.rb b/config/routes.rb
index 0d530a955..be40b590a 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -184,13 +184,14 @@ Rails.application.routes.draw do
get :search
end
end
+ resources :post_events, only: [:index]
resources :post_regenerations, :only => [:create]
- resources :post_replacements, :only => [:index, :new, :create, :update]
+ resources :post_replacements, only: [:index, :show, :new, :create, :update]
resources :post_votes, only: [:index, :show, :create, :destroy]
# XXX Use `only: []` to avoid redefining post routes defined at top of file.
resources :posts, only: [] do
- resources :events, :only => [:index], :controller => "post_events"
+ resources :events, only: [:index], controller: "post_events", as: "post_events"
resources :favorites, only: [:index, :create, :destroy]
resources :replacements, :only => [:index, :new, :create], :controller => "post_replacements"
resource :artist_commentary, only: [:show] do
@@ -208,7 +209,7 @@ Rails.application.routes.draw do
end
resources :post_appeals
resources :post_flags
- resources :post_approvals, only: [:create, :index]
+ resources :post_approvals, only: [:create, :index, :show]
resources :post_disapprovals
resources :post_versions, :only => [:index, :search] do
member do
diff --git a/db/migrate/20220924092056_create_post_events.rb b/db/migrate/20220924092056_create_post_events.rb
new file mode 100644
index 000000000..9cf6ee178
--- /dev/null
+++ b/db/migrate/20220924092056_create_post_events.rb
@@ -0,0 +1,5 @@
+class CreatePostEvents < ActiveRecord::Migration[7.0]
+ def change
+ create_view :post_events
+ end
+end
diff --git a/db/structure.sql b/db/structure.sql
index bfa3676c2..895eba1cb 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -1398,26 +1398,6 @@ CREATE TABLE public.post_flags (
);
---
--- Name: post_flags_id_seq; Type: SEQUENCE; Schema: public; Owner: -
---
-
-CREATE SEQUENCE public.post_flags_id_seq
- AS integer
- START WITH 1
- INCREMENT BY 1
- NO MINVALUE
- NO MAXVALUE
- CACHE 1;
-
-
---
--- Name: post_flags_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
---
-
-ALTER SEQUENCE public.post_flags_id_seq OWNED BY public.post_flags.id;
-
-
--
-- Name: post_replacements; Type: TABLE; Schema: public; Owner: -
--
@@ -1443,6 +1423,117 @@ CREATE TABLE public.post_replacements (
);
+--
+-- Name: posts; Type: TABLE; Schema: public; Owner: -
+--
+
+CREATE TABLE public.posts (
+ id integer NOT NULL,
+ created_at timestamp without time zone NOT NULL,
+ uploader_id integer NOT NULL,
+ score integer DEFAULT 0 NOT NULL,
+ source character varying DEFAULT ''::character varying NOT NULL,
+ md5 character varying NOT NULL,
+ last_comment_bumped_at timestamp without time zone,
+ rating character(1) DEFAULT 'q'::bpchar NOT NULL,
+ image_width integer NOT NULL,
+ image_height integer NOT NULL,
+ tag_string text DEFAULT ''::text NOT NULL,
+ fav_count integer DEFAULT 0 NOT NULL,
+ file_ext character varying NOT NULL,
+ last_noted_at timestamp without time zone,
+ parent_id integer,
+ has_children boolean DEFAULT false NOT NULL,
+ approver_id integer,
+ tag_count_general integer DEFAULT 0 NOT NULL,
+ tag_count_artist integer DEFAULT 0 NOT NULL,
+ tag_count_character integer DEFAULT 0 NOT NULL,
+ tag_count_copyright integer DEFAULT 0 NOT NULL,
+ file_size integer NOT NULL,
+ up_score integer DEFAULT 0 NOT NULL,
+ down_score integer DEFAULT 0 NOT NULL,
+ is_pending boolean DEFAULT false NOT NULL,
+ is_flagged boolean DEFAULT false NOT NULL,
+ is_deleted boolean DEFAULT false NOT NULL,
+ tag_count integer DEFAULT 0 NOT NULL,
+ updated_at timestamp without time zone NOT NULL,
+ is_banned boolean DEFAULT false NOT NULL,
+ pixiv_id integer,
+ last_commented_at timestamp without time zone,
+ has_active_children boolean DEFAULT false,
+ bit_flags bigint DEFAULT 0 NOT NULL,
+ tag_count_meta integer DEFAULT 0 NOT NULL
+);
+
+
+--
+-- Name: post_events; Type: VIEW; Schema: public; Owner: -
+--
+
+CREATE VIEW public.post_events AS
+ SELECT 'Post'::character varying AS model_type,
+ posts.id AS model_id,
+ posts.id AS post_id,
+ posts.uploader_id AS creator_id,
+ posts.created_at AS event_at
+ FROM public.posts
+UNION ALL
+ SELECT 'PostAppeal'::character varying AS model_type,
+ post_appeals.id AS model_id,
+ post_appeals.post_id,
+ post_appeals.creator_id,
+ post_appeals.created_at AS event_at
+ FROM public.post_appeals
+UNION ALL
+ SELECT 'PostApproval'::character varying AS model_type,
+ post_approvals.id AS model_id,
+ post_approvals.post_id,
+ post_approvals.user_id AS creator_id,
+ post_approvals.created_at AS event_at
+ FROM public.post_approvals
+UNION ALL
+ SELECT 'PostDisapproval'::character varying AS model_type,
+ post_disapprovals.id AS model_id,
+ post_disapprovals.post_id,
+ post_disapprovals.user_id AS creator_id,
+ post_disapprovals.created_at AS event_at
+ FROM public.post_disapprovals
+UNION ALL
+ SELECT 'PostFlag'::character varying AS model_type,
+ post_flags.id AS model_id,
+ post_flags.post_id,
+ post_flags.creator_id,
+ post_flags.created_at AS event_at
+ FROM public.post_flags
+UNION ALL
+ SELECT 'PostReplacement'::character varying AS model_type,
+ post_replacements.id AS model_id,
+ post_replacements.post_id,
+ post_replacements.creator_id,
+ post_replacements.created_at AS event_at
+ FROM public.post_replacements;
+
+
+--
+-- Name: post_flags_id_seq; Type: SEQUENCE; Schema: public; Owner: -
+--
+
+CREATE SEQUENCE public.post_flags_id_seq
+ AS integer
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+
+--
+-- Name: post_flags_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
+--
+
+ALTER SEQUENCE public.post_flags_id_seq OWNED BY public.post_flags.id;
+
+
--
-- Name: post_replacements_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
@@ -1540,49 +1631,6 @@ CREATE SEQUENCE public.post_votes_id_seq
ALTER SEQUENCE public.post_votes_id_seq OWNED BY public.post_votes.id;
---
--- Name: posts; Type: TABLE; Schema: public; Owner: -
---
-
-CREATE TABLE public.posts (
- id integer NOT NULL,
- created_at timestamp without time zone NOT NULL,
- uploader_id integer NOT NULL,
- score integer DEFAULT 0 NOT NULL,
- source character varying DEFAULT ''::character varying NOT NULL,
- md5 character varying NOT NULL,
- last_comment_bumped_at timestamp without time zone,
- rating character(1) DEFAULT 'q'::bpchar NOT NULL,
- image_width integer NOT NULL,
- image_height integer NOT NULL,
- tag_string text DEFAULT ''::text NOT NULL,
- fav_count integer DEFAULT 0 NOT NULL,
- file_ext character varying NOT NULL,
- last_noted_at timestamp without time zone,
- parent_id integer,
- has_children boolean DEFAULT false NOT NULL,
- approver_id integer,
- tag_count_general integer DEFAULT 0 NOT NULL,
- tag_count_artist integer DEFAULT 0 NOT NULL,
- tag_count_character integer DEFAULT 0 NOT NULL,
- tag_count_copyright integer DEFAULT 0 NOT NULL,
- file_size integer NOT NULL,
- up_score integer DEFAULT 0 NOT NULL,
- down_score integer DEFAULT 0 NOT NULL,
- is_pending boolean DEFAULT false NOT NULL,
- is_flagged boolean DEFAULT false NOT NULL,
- is_deleted boolean DEFAULT false NOT NULL,
- tag_count integer DEFAULT 0 NOT NULL,
- updated_at timestamp without time zone NOT NULL,
- is_banned boolean DEFAULT false NOT NULL,
- pixiv_id integer,
- last_commented_at timestamp without time zone,
- has_active_children boolean DEFAULT false,
- bit_flags bigint DEFAULT 0 NOT NULL,
- tag_count_meta integer DEFAULT 0 NOT NULL
-);
-
-
--
-- Name: posts_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
@@ -6673,6 +6721,7 @@ INSERT INTO "schema_migrations" (version) VALUES
('20220920224005'),
('20220921022408'),
('20220922014326'),
-('20220923010905');
+('20220923010905'),
+('20220924092056');
diff --git a/db/views/post_events_v01.sql b/db/views/post_events_v01.sql
new file mode 100644
index 000000000..4433b0144
--- /dev/null
+++ b/db/views/post_events_v01.sql
@@ -0,0 +1,17 @@
+ 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', id, post_id, user_id, created_at
+ FROM post_disapprovals
+UNION ALL
+ SELECT 'PostFlag', id, post_id, creator_id, created_at
+ FROM post_flags
+UNION ALL
+ SELECT 'PostReplacement', id, post_id, creator_id, created_at
+ FROM post_replacements
diff --git a/test/functional/post_approvals_controller_test.rb b/test/functional/post_approvals_controller_test.rb
index 9655733c5..609be3c9b 100644
--- a/test/functional/post_approvals_controller_test.rb
+++ b/test/functional/post_approvals_controller_test.rb
@@ -80,5 +80,23 @@ class PostApprovalsControllerTest < ActionDispatch::IntegrationTest
should respond_to_search(post: {uploader_name: "komachi"}).with { @post_approval }
end
end
+
+ context "show action" do
+ setup do
+ @approval = create(:post_approval)
+ end
+
+ should "render for html" do
+ get post_approval_path(@approval)
+
+ assert_redirected_to post_approvals_path(search: { id: @approval.id })
+ end
+
+ should "render for json" do
+ get post_approval_path(@approval), as: :json
+
+ assert_response :success
+ end
+ end
end
end
diff --git a/test/functional/post_events_controller_test.rb b/test/functional/post_events_controller_test.rb
index 70f373ab8..9f80d260a 100644
--- a/test/functional/post_events_controller_test.rb
+++ b/test/functional/post_events_controller_test.rb
@@ -1,42 +1,78 @@
require 'test_helper'
class PostEventsControllerTest < ActionDispatch::IntegrationTest
- setup do
- travel_to(2.weeks.ago) do
- @user = create(:user)
- @mod = create(:mod_user)
- end
+ context "The post approvals controller" do
+ context "index action" do
+ setup do
+ @user = create(:user)
+ @post = create(:post, uploader: @user, is_pending: true)
- as(@user) do
- @post = create(:post, is_flagged: true)
- create(:post_flag, post: @post, status: :rejected)
- @post.update(is_deleted: true)
- create(:post_appeal, post: @post, status: :succeeded)
- create(:post_approval, post: @post, user: @mod)
- end
- end
+ @approval = create(:post_approval, post: @post)
+ @flag = create(:post_flag, post: @post, creator: @user, is_deletion: true)
+ @post.update!(is_deleted: true)
+ @appeal = create(:post_appeal, post: @post, creator: @user)
+ @disapproval = create(:post_disapproval, post: @post, user: @user)
+ @replacement = create(:post_replacement, post: @post, creator: @user)
+ end
- context "get /posts/:post_id/events" do
- should "render" do
- get_auth post_events_path(post_id: @post.id), @user
- assert_response :ok
- end
+ should "render for a global listing" do
+ get post_events_path
- should "render for mods" do
- get_auth post_events_path(post_id: @post.id), @mod
- assert_response :success
- end
- end
+ assert_response :success
+ end
- context "get /posts/:post_id/events.xml" do
- setup do
- get_auth post_events_path(post_id: @post.id), @user, params: {:format => "xml"}
- @xml = Hash.from_xml(response.body)
- @appeal = @xml["post_events"].find { |e| e["type"] == "a" }
- end
+ should "render for a single post listing" do
+ get post_post_events_path(@post.id)
- should "render" do
- assert_not_nil(@appeal)
+ assert_response :success
+ end
+
+ should "render for a json response" do
+ get post_events_path, as: :json
+
+ assert_response :success
+ end
+
+ context "for a moderator" do
+ should "render" do
+ get_auth post_events_path, create(:mod_user)
+ assert_response :success
+ end
+
+ should "allow searching flags by creator" do
+ get_auth post_events_path(search: { creator_name: @user.name }), create(:mod_user), as: :json
+
+ assert_response :success
+ assert_equal(5, response.parsed_body.size)
+ assert_equal(@flag.creator_id, response.parsed_body.find { |event| event["model_type"] == "PostFlag" }["creator_id"])
+ assert_equal(@disapproval.user_id, response.parsed_body.find { |event| event["model_type"] == "PostDisapproval" }["creator_id"])
+ end
+
+ should "include the creator_id in the API" do
+ get_auth post_events_path, create(:mod_user), as: :json
+
+ assert_response :success
+ assert_equal(@flag.creator_id, response.parsed_body.find { |event| event["model_type"] == "PostFlag" }["creator_id"])
+ end
+ end
+
+ context "for a non-moderator" do
+ should "not allow searching flags by creator" do
+ get post_events_path(search: { creator_name: @user.name }), as: :json
+
+ assert_response :success
+ assert_equal(3, response.parsed_body.size)
+ assert_nil(response.parsed_body.find { |event| event["model_type"] == "PostFlag" })
+ assert_nil(response.parsed_body.find { |event| event["model_type"] == "PostDisapproval" })
+ end
+
+ should "not include the creator_id in the API" do
+ get post_events_path, as: :json
+
+ assert_response :success
+ assert_nil(response.parsed_body.find { |event| event["model_type"] == "PostFlag" }["creator_id"])
+ end
+ end
end
end
end
diff --git a/test/functional/post_replacements_controller_test.rb b/test/functional/post_replacements_controller_test.rb
index 05aa2a7ae..9ab38c29a 100644
--- a/test/functional/post_replacements_controller_test.rb
+++ b/test/functional/post_replacements_controller_test.rb
@@ -224,5 +224,23 @@ class PostReplacementsControllerTest < ActionDispatch::IntegrationTest
should respond_to_search(creator_name: "yukari").with { @post_replacement }
end
end
+
+ context "show action" do
+ setup do
+ @replacement = create(:post_replacement)
+ end
+
+ should "render for html" do
+ get post_replacement_path(@replacement)
+
+ assert_redirected_to post_replacements_path(search: { id: @replacement.id })
+ end
+
+ should "render for json" do
+ get post_replacement_path(@replacement), as: :json
+
+ assert_response :success
+ end
+ end
end
end
diff --git a/test/unit/post_event_test.rb b/test/unit/post_event_test.rb
deleted file mode 100644
index 46072a443..000000000
--- a/test/unit/post_event_test.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-require 'test_helper'
-
-class PostEventTest < ActiveSupport::TestCase
- def setup
- @user = create(:user, created_at: 2.weeks.ago)
- @post = create(:post)
- @post_flag = create(:post_flag, creator: @user, post: @post)
- @post.update(is_deleted: true)
- @post_appeal = create(:post_appeal, creator: @user, post: @post)
- end
-
- context "PostEvent.find_for_post" do
- should "work" do
- results = PostEvent.find_for_post(@post.id)
- assert_equal(2, results.size)
- assert_equal("appeal", results[0].type_name)
- assert_equal("flag", results[1].type_name)
- end
- end
-end