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).
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
36
app/policies/post_event_policy.rb
Normal file
36
app/policies/post_event_policy.rb
Normal file
@@ -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
|
||||
@@ -43,3 +43,5 @@
|
||||
<%= numbered_paginator(@post_appeals) %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= render "post_events/secondary_links" %>
|
||||
|
||||
@@ -23,3 +23,5 @@
|
||||
<%= numbered_paginator(@post_approvals) %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= render "post_events/secondary_links" %>
|
||||
|
||||
@@ -39,3 +39,5 @@
|
||||
<%= numbered_paginator(@post_disapprovals) %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= render "post_events/secondary_links" %>
|
||||
|
||||
17
app/views/post_events/_secondary_links.html.erb
Normal file
17
app/views/post_events/_secondary_links.html.erb
Normal file
@@ -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 %>
|
||||
@@ -1,25 +1,106 @@
|
||||
<div id="c-post-events">
|
||||
<div id="a-index">
|
||||
<h1>Post Events</h1>
|
||||
<% if @post %>
|
||||
<h1>Events: <%= link_to @post.dtext_shortlink, @post %></h1>
|
||||
<%= link_to "« Back", post_events_path, class: "text-xs" %>
|
||||
<% else %>
|
||||
<h1>Events</h1>
|
||||
<% 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| %>
|
||||
<div class="prose">
|
||||
<%= format_text event.reason %>
|
||||
</div>
|
||||
<% 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" %>
|
||||
<div class="prose">
|
||||
<%= 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? %>.
|
||||
</div>
|
||||
<% when "PostApproval" %>
|
||||
<%= link_to post.dtext_shortlink, post %> was approved by <%= link_to_user creator %>.
|
||||
<% when "PostDisapproval" %>
|
||||
<div class="prose">
|
||||
<% 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 %>
|
||||
</div>
|
||||
<% when "PostFlag" %>
|
||||
<div class="prose">
|
||||
<% 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 %>
|
||||
</div>
|
||||
<% 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 %>
|
||||
<i>hidden</i>
|
||||
<% end %>
|
||||
<br><%= time_ago_in_words_tagged event.created_at %>
|
||||
<div><%= time_ago_in_words_tagged(event.event_at) %></div>
|
||||
<% 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) %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= render "secondary_links" %>
|
||||
|
||||
@@ -48,3 +48,5 @@
|
||||
<%= numbered_paginator(@post_flags) %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= render "post_events/secondary_links" %>
|
||||
|
||||
@@ -66,3 +66,5 @@
|
||||
<%= numbered_paginator(@post_replacements) %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= render "post_events/secondary_links" %>
|
||||
|
||||
@@ -188,9 +188,8 @@
|
||||
<li id="post-history-tags"><%= link_to "Tags", post_versions_path(search: { post_id: @post.id }) %></li>
|
||||
<li id="post-history-pools"><%= link_to "Pools", pool_versions_path(search: { post_id: @post.id }) %></li>
|
||||
<li id="post-history-notes"><%= link_to "Notes", note_versions_path(search: { post_id: @post.id }) %></li>
|
||||
<li id="post-history-moderation"><%= link_to "Moderation", post_events_path(@post.id) %></li>
|
||||
<li id="post-history-moderation"><%= link_to "Moderation", post_post_events_path(@post.id) %></li>
|
||||
<li id="post-history-commentary"><%= link_to "Commentary", artist_commentary_versions_path(search: { post_id: @post.id }) %></li>
|
||||
<li id="post-history-replacements"><%= link_to "Replacements", post_replacements_path(search: {post_id: @post.id }) %></li>
|
||||
</ul>
|
||||
</section>
|
||||
<% end %>
|
||||
|
||||
@@ -18,9 +18,10 @@
|
||||
<ul>
|
||||
<li><h2>Post Events</h2></li>
|
||||
<li><%= link_to("Tag History", post_versions_path) %></li>
|
||||
<li><%= link_to("Events", post_events_path) %></li>
|
||||
<li><%= link_to("Appeals", post_appeals_path) %></li>
|
||||
<li><%= link_to("Approvals", post_approvals_path) %></li>
|
||||
<li><%= link_to("Disapprovals", post_disapprovals_path) %></li>
|
||||
<li><%= link_to("Appeals", post_appeals_path) %></li>
|
||||
<li><%= link_to("Flags", post_flags_path) %></li>
|
||||
<li><%= link_to("Replacements", post_replacements_path) %></li>
|
||||
</ul>
|
||||
|
||||
@@ -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
|
||||
|
||||
5
db/migrate/20220924092056_create_post_events.rb
Normal file
5
db/migrate/20220924092056_create_post_events.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
class CreatePostEvents < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
create_view :post_events
|
||||
end
|
||||
end
|
||||
177
db/structure.sql
177
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');
|
||||
|
||||
|
||||
|
||||
17
db/views/post_events_v01.sql
Normal file
17
db/views/post_events_v01.sql
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user