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 @@
Admin
- <%= link_to("Mod Actions", mod_actions_path) %>
+
+ <% if policy(UserAction).index? %>
+ - <%= link_to("User Actions", user_actions_path) %>
+ <% end %>
+
- <%= link_to("Bulk Update Requests", bulk_update_requests_path) %>
<% if policy(UserNameChangeRequest).index? %>
diff --git a/app/views/user_actions/_secondary_links.html.erb b/app/views/user_actions/_secondary_links.html.erb
new file mode 100644
index 000000000..9cd2c4b93
--- /dev/null
+++ b/app/views/user_actions/_secondary_links.html.erb
@@ -0,0 +1,11 @@
+<% content_for(:secondary_links) do %>
+ <%= quick_search_form_for(:user_name, user_actions_path, "users", autocomplete: "user") %>
+ <%= subnav_link_to "Mod Actions", mod_actions_path %>
+ <%= subnav_link_to "User Actions", user_actions_path %>
+ <%= subnav_link_to "User Events", user_events_path %>
+ <%= subnav_link_to "Bans", bans_path %>
+ <%= subnav_link_to "IP Bans", ip_bans_path %>
+ <%= subnav_link_to "Feedbacks", user_feedbacks_path %>
+ <%= subnav_link_to "Mod Reports", moderation_reports_path %>
+ <%= subnav_link_to "Modqueue", modqueue_index_path %>
+<% end %>
diff --git a/app/views/user_actions/index.html.erb b/app/views/user_actions/index.html.erb
new file mode 100644
index 000000000..9d0d23ea5
--- /dev/null
+++ b/app/views/user_actions/index.html.erb
@@ -0,0 +1,161 @@
+
+
+ <% if @user %>
+
User Actions: <%= link_to_user @user %>
+ <%= link_to "« Back", user_actions_path, class: "text-xs" %>
+ <% else %>
+
User Actions
+ <% end %>
+
+ <%= search_form_for(user_actions_path) do |f| %>
+ <%= f.input :user_name, label: "User", input_html: { value: @user&.name, "data-autocomplete": "user" } %>
+ <%= f.input :model_type, label: "Category", collection: UserAction.model_types.map { |type| [type.delete_suffix("Version").titleize, 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 %>
+
+
+ <%= table_for @user_actions, class: "striped autofit", width: "100%" do |t| %>
+ <% t.column "Event", td: { class: "col-expand" } do |user_action| %>
+ <% model = user_action.model %>
+ <% user = user_action.user %>
+ <% event_type = user_action.event_type %>
+
+ <% case user_action.model_type %>
+ <% when "ArtistVersion" %>
+ <%= link_to_user user %> <%= link_to "updated", model %> the artist <%= link_to model.artist.pretty_name, model.artist, class: "tag-type-#{Tag.categories.artist}" %>.
+ <% when "ArtistCommentaryVersion" %>
+ <%= link_to_user user %> updated the <%= link_to "commentary", model %> for <%= link_to model.post.dtext_shortlink, model.post %>.
+ <% when "NoteVersion" %>
+ <%= link_to_user user %> <%= link_to "updated", model %> a <%= link_to "note", model.note %> on <%= link_to model.post.dtext_shortlink, model.post %>.
+ <% when "TagVersion" %>
+ <%= link_to_user user %> updated the tag <%= link_to model.tag.name, model, class: tag_class(model) %>.
+ <% when "WikiPageVersion" %>
+ <%= link_to_user user %> <%= link_to "updated", model %> the <%= link_to_wiki model.title %> wiki.
+ <% when "Ban" %>
+
+ <%= link_to_user user %> was <%= link_to (model.forever? ? "banned forever" : "banned for #{model.humanized_duration}"), model %> by <%= link_to_user model.banner %> (<%= format_text(model.reason.chomp(".").strip, inline: true) %>).
+
+ <% when "BulkUpdateRequest" %>
+ <%= link_to_user user %> created a <%= link_to "new BUR", model %> in <%= link_to model.forum_topic.title, model.forum_post %>.
+ <% when "Comment" %>
+ <% if model.is_deleted? && !policy(model).can_see_deleted? %>
+ [deleted] posted a <%= link_to "deleted comment", model %> on <%= link_to model.post.dtext_shortlink, model.post %>.
+ <% else %>
+ <%= link_to_user user %> <%= link_to "commented", model %> on <%= link_to model.post.dtext_shortlink, model.post %>.
+ <% end %>
+ <% when "CommentVote" %>
+ <%= link_to_user user %> <%= model.is_positive? ? "upvoted" : "downvoted" %> a <%= link_to "comment", model.comment %> by <%= link_to_user model.comment.creator %> on <%= link_to model.comment.post.dtext_shortlink, model.comment.post %>.
+ <% when "Dmail" %>
+ <%= link_to_user user %> sent a dmail to <%= link_to_user model.to %> (<%= link_to model.title.strip, model %>).
+ <% when "FavoriteGroup" %>
+ <%= link_to_user user %> create <%= model.is_public? ? "public" : "private" %> favgroup <%= link_to model.pretty_name, model %>.
+ <% when "ForumPost" %>
+ <%= link_to_user user %> <%= link_to "posted", model %> in topic <%= link_to model.topic.title.chomp(".").strip, model.topic %>.
+ <% when "ForumTopic" %>
+ <%= link_to_user user %> created topic "<%= link_to model.title.strip, model %>".
+ <% when "ForumPostVote" %>
+ <% if model&.bulk_update_request.present? %>
+ <%= link_to_user user %> <%= model.vote_type %>voted a <%= link_to "BUR", model.bulk_update_request %> in topic <%= link_to model.forum_post.topic.title.strip, model.forum_post %>.
+ <% else %>
+ <%= link_to_user user %> <%= model.vote_type %>voted a <%= link_to "post", model.forum_post %> in topic <%= link_to model.forum_post.topic.title.strip, model.forum_post %>.
+ <% end %>
+ <% when "ModAction" %>
+
+ <%= link_to_user user %> <%= format_text(model.description, inline: true) %>.
+
+ <% when "ModerationReport" %>
+
+ <%= link_to_user user %> <%= link_to "reported", model %> a <%= link_to model.model.class.name.titleize.downcase, model.model %> by <%= link_to_user model.reported_user %> (<%= format_text(model.reason.chomp(".").strip, inline: true) %>).
+
+ <% when "TagAlias" %>
+ <%= link_to_user user %> aliased <%= link_to_search model.antecedent_tag %> to <%= link_to_search model.consequent_tag %>.
+ <% when "TagImplication" %>
+ <%= link_to_user user %> implied <%= link_to_search model.antecedent_tag %> to <%= link_to_search model.consequent_tag %>.
+ <% when "Post" %>
+ <%= link_to_user user %> created <%= link_to model.dtext_shortlink, model %>.
+ <% when "PostAppeal" %>
+ <%= link_to_user user %> appealed <%= link_to model.post.dtext_shortlink, model.post %>.
+ <% when "PostApproval" %>
+ <%= link_to_user user %> approved <%= link_to model.post.dtext_shortlink, model.post %>.
+ <% when "PostDisapproval" %>
+
+ <% if policy(model).can_view_creator? %>
+ <%= link_to_user user %> disapproved <%= link_to model.post.dtext_shortlink, model.post %> (<%= model.reason.titleize.downcase %><%= ": ".html_safe + format_text(model.message.chomp(".").strip, inline: true) if model.message.present? %>).
+ <% else %>
+ <%= link_to model.post.dtext_shortlink, model.post %> was disapproved (<%= model.reason.titleize.downcase %><%= ": ".html_safe + format_text(model.message.chomp(".").strip, inline: true) if model.message.present? %>).
+ <% end %>
+
+ <% when "PostFlag" %>
+
+ <% if policy(model).can_view_flagger? %>
+ <%= link_to_user user %> flagged <%= link_to model.post.dtext_shortlink, model.post %> (<%= format_text(model.reason.chomp(".").strip, inline: true) %>).
+ <% else %>
+ <%= link_to model.post.dtext_shortlink, model.post %> was flagged (<%= format_text(model.reason.chomp(".").strip, inline: true) %>).
+ <% end %>
+
+ <% when "PostReplacement" %>
+ <%= link_to_user user %> replaced <%= link_to model.post.dtext_shortlink, model.post %> with <%= external_link_to Source::URL.page_url(model.replacement_url) || model.replacement_url %>.
+ <% when "PostVote" %>
+ <%= link_to_user user %> <%= model.is_positive? ? "upvoted" : "downvoted" %> <%= link_to model.post.dtext_shortlink, model.post %>.
+ <% when "SavedSearch" %>
+ <%= link_to_user user %> saved search <%= link_to model.query, posts_path(tag: model.query) %>.
+ <% when "Upload" %>
+ <%= link_to_user user %> created <%= link_to model.dtext_shortlink, model %>.
+ <% when "User" %>
+ <%= link_to_user user %> created their account.
+ <% when "UserEvent" %>
+ <% case model.category %>
+ <% when "login" %>
+ <%= link_to_user user %> logged in.
+ <% when "failed_login" %>
+ Failed login attempt for <%= link_to_user user %>.
+ <% when "logout" %>
+ <%= link_to_user user %> logged out.
+ <% when "user_creation" %>
+ <%= link_to_user user %> created their account.
+ <% when "user_deletion" %>
+ <%= link_to_user user %> deleted their account.
+ <% when "password_reset" %>
+ <%= link_to_user user %> reset their password.
+ <% when "password_reset" %>
+ <%= link_to_user user %> changed their password.
+ <% when "email_change" %>
+ <%= link_to_user user %> changed their email address.
+ <% end %>
+ <% when "UserFeedback" %>
+ <% case event_type %>
+ <% when "create" %>
+ <%= link_to_user model.creator %> created a <%= link_to "#{model.category} feedback", model %> for <%= link_to_user model.user %>.
+ <% when "subject" %>
+ <%= link_to_user model.user %> received a <%= link_to "#{model.category} feedback", model %> from <%= link_to_user model.creator %>.
+ <% end %>
+ <% when "UserNameChangeRequest" %>
+ <%= link_to_user model.user %> <%= link_to "changed their name", model %> from <%= model.original_name %> to <%= model.desired_name %>.
+ <% when "UserUpgrade" %>
+ <% if model.complete? %>
+ <%= link_to_user model.purchaser %> <%= link_to "upgraded", model %> <%= link_to_user model.recipient if model.is_gift? %> to <%= model.level_string %>.
+ <% elsif model.refunded? %>
+ <%= link_to_user model.purchaser %> <%= link_to "upgraded", model %> <%= link_to_user model.recipient if model.is_gift? %> to <%= model.level_string %> (refunded).
+ <% end %>
+ <% end %>
+ <% end %>
+
+ <% t.column "Category" do |user_action| %>
+ <%= link_to user_action.model_type.delete_suffix("Version").titleize, user_actions_path(search: { model_type: user_action.model_type }) %>
+ <% end %>
+
+ <% t.column "Date" do |user_action| %>
+ <% if policy(user_action).can_see_user? %>
+ <%= link_to_user user_action.user %> <%= link_to "»", user_actions_path(search: { **search_params, user_id: user_action.user_id }) %>
+ <% end %>
+
<%= time_ago_in_words_tagged(user_action.event_at) %>
+ <% end %>
+ <% end %>
+
+
+ <%= numbered_paginator(@user_actions) %>
+
+
+
+<%= render "secondary_links" %>
diff --git a/config/routes.rb b/config/routes.rb
index 668bdf226..8576c29a1 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -261,7 +261,9 @@ Rails.application.routes.draw do
resources :upload_media_assets, only: [:show, :index], path: "assets"
end
resources :upload_media_assets, only: [:show, :index]
+ resources :user_actions, only: [:index, :show]
resources :users do
+ resources :actions, only: [:index]
resources :favorites, only: [:index, :create, :destroy]
resources :favorite_groups, controller: "favorite_groups", only: [:index], as: "favorite_groups"
resource :email, only: [:show, :edit, :update] do
diff --git a/test/factories/moderation_report.rb b/test/factories/moderation_report.rb
index d2d668406..3ae5e7a4d 100644
--- a/test/factories/moderation_report.rb
+++ b/test/factories/moderation_report.rb
@@ -3,5 +3,6 @@ FactoryBot.define do
creator
reason {"xxx"}
status { :pending }
+ model { build(:comment) }
end
end
diff --git a/test/factories/user_event.rb b/test/factories/user_event.rb
index 7ecdeb5e9..ec2b5a7a3 100644
--- a/test/factories/user_event.rb
+++ b/test/factories/user_event.rb
@@ -2,5 +2,6 @@ FactoryBot.define do
factory(:user_event) do
user
user_session
+ category { :login }
end
end
diff --git a/test/functional/user_actions_controller_test.rb b/test/functional/user_actions_controller_test.rb
new file mode 100644
index 000000000..69dfe3dca
--- /dev/null
+++ b/test/functional/user_actions_controller_test.rb
@@ -0,0 +1,72 @@
+require 'test_helper'
+
+class UserActionsControllerTest < ActionDispatch::IntegrationTest
+ context "The user actions controller" do
+ context "index action" do
+ setup do
+ @user = create(:user)
+
+ as(@user) do
+ create(:artist)
+ create(:artist_commentary)
+ create(:ban)
+ create(:bulk_update_request)
+ create(:comment)
+ create(:comment_vote)
+ create(:dmail)
+ create(:favorite_group)
+ create(:forum_post)
+ create(:forum_post_vote)
+ create(:forum_topic)
+ create(:mod_action)
+ create(:moderation_report)
+ create(:note)
+ create(:post)
+ create(:post_appeal)
+ create(:post_approval)
+ create(:post_disapproval)
+ create(:post_flag)
+ create(:post_replacement)
+ create(:post_vote)
+ create(:wiki_page)
+ create(:saved_search)
+ create(:tag)
+ create(:tag_alias)
+ create(:tag_implication)
+ create(:upload)
+ create(:user_event)
+ create(:user_feedback)
+ create(:user_name_change_request)
+ create(:user_upgrade)
+ create(:wiki_page)
+ end
+ end
+
+ should "render for the owner" do
+ get_auth user_events_path(limit: 1000), create(:admin_user)
+ assert_response :success
+ end
+
+ should "render for an admin" do
+ get_auth user_events_path(limit: 1000), create(:admin_user)
+ assert_response :success
+ end
+
+ should "render for a mod" do
+ get_auth user_events_path(limit: 1000), create(:moderator_user)
+ assert_response :success
+ end
+
+ should "fail for a normal user" do
+ get_auth user_actions_path(limit: 1000), create(:user)
+ assert_response 403
+ end
+
+ should "render when filtering on a single user" do
+ get_auth user_events_path(user_id: @user.id, limit: 1000), create(:owner_user)
+ assert_response :success
+ end
+ end
+ end
+end
+