From ee638f976ff088f028e6a5a3e74e1fad6779336c Mon Sep 17 00:00:00 2001 From: evazion Date: Wed, 14 Sep 2022 16:30:46 -0500 Subject: [PATCH] Add /user_actions page. Add a /user_actions page. This page shows you a global timeline of (almost) all activity on the site, including uploads, comments, votes, edits, forum posts, and so on. The main things it doesn't include are post edits, pool edits, and favorites (posts and pools live in a separate database, and favorites don't have the timestamps we need for ordering). This page is useful for moderation purposes because it lets you see a history of almost all of a user's activity on a single page. Currently this page is mod-only. In the future it will be open to all users, so you can view the history of your own site activity, or the activity of others. --- app/controllers/user_actions_controller.rb | 23 +++ app/helpers/application_helper.rb | 2 +- app/models/application_record.rb | 4 + app/models/ban.rb | 4 + app/models/dmail.rb | 6 +- app/models/favorite.rb | 2 +- app/models/favorite_group.rb | 11 +- app/models/forum_post_vote.rb | 2 + app/models/moderation_report.rb | 4 +- app/models/post_vote.rb | 2 +- app/models/saved_search.rb | 6 +- app/models/tag_version.rb | 4 + app/models/user_action.rb | 122 +++++++++++++ app/policies/comment_policy.rb | 4 + app/policies/user_action_policy.rb | 20 +++ app/views/static/site_map.html.erb | 5 + .../user_actions/_secondary_links.html.erb | 11 ++ app/views/user_actions/index.html.erb | 161 ++++++++++++++++++ config/routes.rb | 2 + test/factories/moderation_report.rb | 1 + test/factories/user_event.rb | 1 + .../user_actions_controller_test.rb | 72 ++++++++ 22 files changed, 462 insertions(+), 7 deletions(-) create mode 100644 app/controllers/user_actions_controller.rb create mode 100644 app/models/user_action.rb create mode 100644 app/policies/user_action_policy.rb create mode 100644 app/views/user_actions/_secondary_links.html.erb create mode 100644 app/views/user_actions/index.html.erb create mode 100644 test/functional/user_actions_controller_test.rb diff --git a/app/controllers/user_actions_controller.rb b/app/controllers/user_actions_controller.rb new file mode 100644 index 000000000..23ccaf514 --- /dev/null +++ b/app/controllers/user_actions_controller.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class UserActionsController < ApplicationController + respond_to :html, :xml, :json + + def index + if user_id = params[:user_id] || params.dig(:search, :user_id) + @user = User.find(user_id) + elsif user_name = params.dig(:search, :user_name) + @user = User.find_by_name(user_name) + end + + @user_actions = authorize UserAction.for_user(CurrentUser.user).paginated_search(params, count_pages: @user.present?) + @user_actions = @user_actions.includes(:user, model: [:artist, :post, :note, :user, :creator, :banner, :bulk_update_request, :tag, :antecedent_tag, :consequent_tag, :model, :topic, :purchaser, :recipient, :forum_topic, forum_post: [:topic], comment: [:creator, :post]]) if request.format.html? + + respond_with(@user_actions) + end + + def show + @user_actions = authorize UserAction.find(params[:id]) + respond_with(@user_actions) + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 423376eea..f3791026d 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -199,7 +199,7 @@ module ApplicationHelper end def link_to_search(tag, **options) - link_to tag.name, posts_path(tags: tag.name), class: tag_class(tag), **options + link_to tag.pretty_name, posts_path(tags: tag.name), class: tag_class(tag), **options end def link_to_wiki(text, title = text, **options) diff --git a/app/models/application_record.rb b/app/models/application_record.rb index e6c268682..faca00400 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -234,6 +234,10 @@ class ApplicationRecord < ActiveRecord::Base end end + def revised? + updated_at > created_at + end + def warnings @warnings ||= ActiveModel::Errors.new(self) end diff --git a/app/models/ban.rb b/app/models/ban.rb index 115a9618c..c0cd8e31f 100644 --- a/app/models/ban.rb +++ b/app/models/ban.rb @@ -69,6 +69,10 @@ class Ban < ApplicationRecord ApplicationController.helpers.humanized_duration(duration) end + def forever? + duration.present? && duration >= 100.years + end + def expired? persisted? && expires_at < Time.zone.now end diff --git a/app/models/dmail.rb b/app/models/dmail.rb index 57aa89cac..b48afc2d2 100644 --- a/app/models/dmail.rb +++ b/app/models/dmail.rb @@ -76,7 +76,11 @@ class Dmail < ApplicationRecord module SearchMethods def visible(user) - where(owner: user) + if user.is_anonymous? + none + else + where(owner: user) + end end def sent_by(user) diff --git a/app/models/favorite.rb b/app/models/favorite.rb index 0c7a32219..24c721cbc 100644 --- a/app/models/favorite.rb +++ b/app/models/favorite.rb @@ -8,7 +8,7 @@ class Favorite < ApplicationRecord after_create :upvote_post_on_create after_destroy :unvote_post_on_destroy - scope :public_favorites, -> { where(user: User.has_public_favorites) } + scope :public_favorites, -> { where.not(user: User.has_private_favorites) } def self.visible(user) if user.is_admin? diff --git a/app/models/favorite_group.rb b/app/models/favorite_group.rb index 2e3430ec4..63ef60cc0 100644 --- a/app/models/favorite_group.rb +++ b/app/models/favorite_group.rb @@ -17,6 +17,9 @@ class FavoriteGroup < ApplicationRecord array_attribute :post_ids, parse: /\d+/, cast: :to_i + scope :is_public, -> { where(is_public: true) } + scope :is_private, -> { where(is_public: false) } + module SearchMethods def for_post(post_id) where_array_includes_any(:post_ids, [post_id]) @@ -29,7 +32,13 @@ class FavoriteGroup < ApplicationRecord end def visible(user) - where(is_public: true).or(where(creator_id: user.id)) + if user.is_owner? + all + elsif user.is_anonymous? + is_public + else + is_public.or(where(creator: user)) + end end def search(params) diff --git a/app/models/forum_post_vote.rb b/app/models/forum_post_vote.rb index 739b1d2cc..07230da7e 100644 --- a/app/models/forum_post_vote.rb +++ b/app/models/forum_post_vote.rb @@ -3,6 +3,8 @@ class ForumPostVote < ApplicationRecord belongs_to :creator, class_name: "User" belongs_to :forum_post + belongs_to :bulk_update_request, primary_key: :forum_post_id, foreign_key: :forum_post_id, optional: true + validates :creator_id, uniqueness: {scope: :forum_post_id} validates :score, inclusion: {in: [-1, 0, 1]} diff --git a/app/models/moderation_report.rb b/app/models/moderation_report.rb index ed2a7ad10..66301d6b5 100644 --- a/app/models/moderation_report.rb +++ b/app/models/moderation_report.rb @@ -35,8 +35,10 @@ class ModerationReport < ApplicationRecord def self.visible(user) if user.is_moderator? all - else + elsif !user.is_anonymous? where(creator: user) + else + none end end diff --git a/app/models/post_vote.rb b/app/models/post_vote.rb index 8ffb3f9f3..a9f4699a5 100644 --- a/app/models/post_vote.rb +++ b/app/models/post_vote.rb @@ -17,7 +17,7 @@ class PostVote < ApplicationRecord scope :positive, -> { where("post_votes.score > 0") } scope :negative, -> { where("post_votes.score < 0") } - scope :public_votes, -> { active.positive.where(user: User.has_public_favorites) } + scope :public_votes, -> { active.positive.where.not(user: User.has_private_favorites) } deletable diff --git a/app/models/saved_search.rb b/app/models/saved_search.rb index f0783777b..63d8af7b0 100644 --- a/app/models/saved_search.rb +++ b/app/models/saved_search.rb @@ -18,7 +18,11 @@ class SavedSearch < ApplicationRecord scope :has_tag, ->(name) { where_regex(:query, "(^| )[~-]?#{Regexp.escape(name)}( |$)", flags: "i") } def self.visible(user) - where(user: user) + if user.is_anonymous? + none + else + where(user: user) + end end concerning :Redis do diff --git a/app/models/tag_version.rb b/app/models/tag_version.rb index fbbbba6f1..262c3d4af 100644 --- a/app/models/tag_version.rb +++ b/app/models/tag_version.rb @@ -39,4 +39,8 @@ class TagVersion < ApplicationRecord def category_name TagCategory.reverse_mapping[category].capitalize end + + def pretty_name + name.tr("_", " ") + end end diff --git a/app/models/user_action.rb b/app/models/user_action.rb new file mode 100644 index 000000000..93b831fcd --- /dev/null +++ b/app/models/user_action.rb @@ -0,0 +1,122 @@ +# frozen_string_literal: true + +class UserAction < ApplicationRecord + belongs_to :model, polymorphic: true + belongs_to :user + + attribute :model_type, :string + attribute :model_id, :integer + attribute :user_id, :integer + attribute :event_type, :string + attribute :event_at, :time + + def self.model_types + %w[ArtistVersion ArtistCommentaryVersion Ban BulkUpdateRequest Comment + CommentVote Dmail FavoriteGroup ForumPost ForumPostVote ForumTopic + ModAction ModerationReport NoteVersion Post PostAppeal PostApproval + PostDisapproval PostFlag PostReplacement PostVote SavedSearch TagAlias + TagImplication TagVersion Upload User UserEvent UserFeedback UserUpgrade + UserNameChangeRequest WikiPageVersion] + end + + def self.for_user(user) + sql = <<~SQL.squish + (#{ArtistVersion.visible(user).select("'ArtistVersion'::character varying AS model_type, id AS model_id, updater_id AS user_id, 'create'::character varying AS event_type, created_at AS event_at").to_sql}) + UNION ALL + (#{ArtistCommentaryVersion.visible(user).select("'ArtistCommentaryVersion', id, updater_id, 'create', created_at").to_sql}) + UNION ALL + (#{Ban.visible(user).select("'Ban', id, user_id, 'subject', created_at").to_sql}) + UNION ALL + (#{BulkUpdateRequest.visible(user).select("'BulkUpdateRequest', id, user_id, 'create', created_at").to_sql}) + UNION ALL + (#{Comment.visible(user).select("'Comment', id, creator_id, 'create', created_at").to_sql}) + UNION ALL + (#{CommentVote.visible(user).select("'CommentVote', id, user_id, 'create', created_at").to_sql}) + UNION ALL + (#{Dmail.visible(user).sent.select("'Dmail', id, from_id, 'create', created_at").order(created_at: :desc).to_sql}) + UNION ALL + (#{FavoriteGroup.visible(user).select("'FavoriteGroup', id, creator_id, 'create', created_at").order(created_at: :desc).to_sql}) + UNION ALL + (#{ForumPost.visible(user).select("'ForumPost', id, creator_id, 'create', created_at").order(created_at: :desc).to_sql}) + UNION ALL + (#{ForumPostVote.visible(user).select("'ForumPostVote', id, creator_id, 'create', created_at").order(created_at: :desc).to_sql}) + UNION ALL + (#{ForumTopic.visible(user).select("'ForumTopic', id, creator_id, 'create', created_at").to_sql}) + UNION ALL + (#{ModAction.visible(user).select("'ModAction', id, creator_id, 'create', created_at").to_sql}) + UNION ALL + (#{ModerationReport.visible(user).select("'ModerationReport', id, creator_id, 'create', created_at").to_sql}) + UNION ALL + (#{NoteVersion.visible(user).select("'NoteVersion', id, updater_id, 'create', created_at").to_sql}) + UNION ALL + (#{Post.visible(user).select("'Post', id, uploader_id, 'create', created_at").to_sql}) + UNION ALL + (#{PostAppeal.visible(user).select("'PostAppeal', id, creator_id, 'create', created_at").to_sql}) + UNION ALL + (#{PostApproval.visible(user).select("'PostApproval', id, user_id, 'create', created_at").to_sql}) + UNION ALL + (#{PostDisapproval.visible(user).select("'PostDisapproval', id, user_id, 'create', created_at").to_sql}) + UNION ALL + (#{PostFlag.visible(user).select("'PostFlag', id, creator_id, 'create', created_at").to_sql}) + UNION ALL + (#{PostReplacement.visible(user).select("'PostReplacement', id, creator_id, 'create', created_at").to_sql}) + UNION ALL + (#{PostVote.visible(user).select("'PostVote', id, user_id, 'create', created_at").order(created_at: :desc).to_sql}) + UNION ALL + (#{SavedSearch.visible(user).select("'SavedSearch', id, user_id, 'create', created_at").order(created_at: :desc).to_sql}) + UNION ALL + (#{TagAlias.visible(user).select("'TagAlias', id, creator_id, 'create', created_at").to_sql}) + UNION ALL + (#{TagImplication.visible(user).select("'TagImplication', id, creator_id, 'create', created_at").to_sql}) + UNION ALL + (#{TagVersion.visible(user).select("'TagVersion', id, updater_id, 'create', created_at").where("updater_id IS NOT NULL").order(created_at: :desc).to_sql}) + UNION ALL + (#{Upload.visible(user).select("'Upload', id, uploader_id, 'create', created_at").order(created_at: :desc).to_sql}) + UNION ALL + (#{User.visible(user).select("'User', id, id, 'create', created_at").to_sql}) + UNION ALL + (#{UserEvent.visible(user).select("'UserEvent', id, user_id, 'create', created_at").to_sql}) + UNION ALL + (#{UserFeedback.visible(user).select("'UserFeedback', id, creator_id, 'create', created_at").to_sql}) + UNION ALL + (#{UserFeedback.visible(user).select("'UserFeedback', id, user_id, 'subject', created_at").to_sql}) + UNION ALL ( + (#{UserUpgrade.visible(user).select("'UserUpgrade', id, purchaser_id, 'create', created_at").where(status: [:complete, :refunded]).order(created_at: :desc).to_sql}) + ) UNION ALL + (#{UserNameChangeRequest.visible(user).select("'UserNameChangeRequest', id, user_id, 'create', created_at").to_sql}) + UNION ALL + (#{WikiPageVersion.visible(user).select("'WikiPageVersion', id, updater_id, 'create', created_at").to_sql}) + SQL + + from("(#{sql}) user_actions") + end + + def self.visible(user) + all + end + + def self.search(params) + q = search_attributes(params, :event_type, :user, :model) + + case params[:order] + when "event_at_asc" + q = q.order(event_at: :asc, model_id: :asc) + else + q = q.apply_default_order(params) + end + + q + end + + def self.default_order + order(event_at: :desc, model_id: :desc) + end + + def self.available_includes + [:user, :model] + end + + def readonly? + true + end +end diff --git a/app/policies/comment_policy.rb b/app/policies/comment_policy.rb index 0bcbd4c3e..20df232c1 100644 --- a/app/policies/comment_policy.rb +++ b/app/policies/comment_policy.rb @@ -21,6 +21,10 @@ class CommentPolicy < ApplicationPolicy user.is_moderator? end + def can_see_creator? + !record.is_deleted? || can_see_deleted? + end + def reply? !record.is_deleted? end diff --git a/app/policies/user_action_policy.rb b/app/policies/user_action_policy.rb new file mode 100644 index 000000000..bfb2bbc94 --- /dev/null +++ b/app/policies/user_action_policy.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class UserActionPolicy < ApplicationPolicy + def index? + user.is_moderator? + end + + def can_see_user? + case record.model_type + when "Comment" + policy(record.model).can_see_creator? + when "PostFlag" + policy(record.model).can_view_flagger? + when "PostDisapproval" + policy(record.model).can_view_creator? + else + true + end + end +end diff --git a/app/views/static/site_map.html.erb b/app/views/static/site_map.html.erb index a3f798f4b..1361113c8 100644 --- a/app/views/static/site_map.html.erb +++ b/app/views/static/site_map.html.erb @@ -145,6 +145,11 @@