From 361af6a4cb706058bce5519792822b8fbed0395c Mon Sep 17 00:00:00 2001 From: evazion Date: Sat, 24 Sep 2022 17:41:23 -0500 Subject: [PATCH] posts: rework post events page. * Add a global /post_events page that shows the history of all approvals, disapprovals, flags, appeals, and replacements on a single page. * Redesign the /posts/:id/events page to show all approval, disapproval, flag, appeal, and replacement events for a single post (before it only showed approvals, flags, and appeals). * Remove the replacement history link from the post show page. Replacements are now included in the post events page (closes #4948: Highlighed replacements). * Add /post_approvals/:id and /post_replacements/:id routes (these are used by the "Details" link on the post events page). --- app/controllers/post_approvals_controller.rb | 8 + app/controllers/post_events_controller.rb | 10 +- .../post_replacements_controller.rb | 8 + app/logical/bigquery_export_service.rb | 2 +- app/logical/source/url.rb | 4 + app/models/post.rb | 3 +- app/models/post_event.rb | 86 +++------ app/models/user.rb | 1 + app/policies/post_event_policy.rb | 36 ++++ app/views/post_appeals/index.html.erb | 2 + app/views/post_approvals/index.html.erb | 2 + app/views/post_disapprovals/index.html.erb | 2 + .../post_events/_secondary_links.html.erb | 17 ++ app/views/post_events/index.html.erb | 105 +++++++++-- app/views/post_flags/index.html.erb | 2 + app/views/post_replacements/index.html.erb | 2 + app/views/posts/show.html.erb | 3 +- app/views/static/site_map.html.erb | 3 +- config/routes.rb | 7 +- .../20220924092056_create_post_events.rb | 5 + db/structure.sql | 177 +++++++++++------- db/views/post_events_v01.sql | 17 ++ .../post_approvals_controller_test.rb | 18 ++ .../functional/post_events_controller_test.rb | 98 +++++++--- .../post_replacements_controller_test.rb | 18 ++ test/unit/post_event_test.rb | 20 -- 26 files changed, 455 insertions(+), 201 deletions(-) create mode 100644 app/policies/post_event_policy.rb create mode 100644 app/views/post_events/_secondary_links.html.erb create mode 100644 db/migrate/20220924092056_create_post_events.rb create mode 100644 db/views/post_events_v01.sql delete mode 100644 test/unit/post_event_test.rb 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 "Commentary", artist_commentary_versions_path(search: { post_id: @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 @@ 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