Files
danbooru/app/models/application_record.rb
evazion ee638f976f 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.
2022-09-16 05:39:25 -05:00

245 lines
7.6 KiB
Ruby

# frozen_string_literal: true
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
include Deletable
include Mentionable
include Normalizable
include ArrayAttribute
include HasDtextLinks
extend HasBitFlags
extend Searchable
concerning :PaginationMethods do
class_methods do
def paginate(*args, **options)
extending(PaginationExtension).paginate(*args, **options)
end
# Perform a search using the model's `search` method, then paginate the results.
#
# @param params [Hash] The URL request params from the user
# @param page [Integer] The page number
# @param limit [Integer] The number of posts per page
# @param count_pages [Boolean] If true, calculate the exact number of pages of
# results. If false (the default), don't calculate the exact number of pages
# of results; assume there are too many pages to count.
# @param count [Integer] the precalculated number of search results, or nil to calculate it
# @param defaults [Hash] The default params for the search
def paginated_search(params, page: params[:page], limit: params[:limit], count_pages: params[:search].present?, count: nil, defaults: {})
search_params = params.fetch(:search, {}).permit!
search_params = defaults.merge(search_params).with_indifferent_access
max_limit = (params[:format] == "sitemap") ? 10_000 : 1_000
search(search_params).paginate(page, limit: limit, max_limit: max_limit, count: count, search_count: count_pages)
end
end
end
concerning :PrivilegeMethods do
class_methods do
def visible(_user)
all
end
def policy(current_user)
Pundit.policy(current_user, self)
end
end
def policy(current_user)
Pundit.policy(current_user, self)
end
end
concerning :ApiMethods do
class_methods do
def available_includes
[]
end
def multiple_includes
reflections.select { |_, v| v.macro == :has_many }.keys.map(&:to_sym)
end
def associated_models(name)
if reflections[name].options[:polymorphic]
reflections[name].active_record.try(:model_types) || []
else
[reflections[name].class_name]
end
end
end
def available_includes
self.class.available_includes
end
# XXX deprecated, shouldn't expose this as an instance method.
def api_attributes(user: CurrentUser.user)
policy = Pundit.policy(user, self) || ApplicationPolicy.new(user, self)
policy.api_attributes
end
# XXX deprecated, shouldn't expose this as an instance method.
def html_data_attributes(user: CurrentUser.user)
policy = Pundit.policy(user, self) || ApplicationPolicy.new(user, self)
policy.html_data_attributes
end
def serializable_hash(options = {})
options ||= {}
if options[:only].is_a?(String)
options.delete(:methods)
options.delete(:include)
options.merge!(ParameterBuilder.serial_parameters(options[:only], self))
else
options[:methods] ||= []
attributes, methods = api_attributes.partition { |attr| has_attribute?(attr) }
methods += options[:methods]
options[:only] ||= attributes + methods
attributes &= options[:only]
methods &= options[:only]
options[:only] = attributes
options[:methods] = methods
options.delete(:methods) if options[:methods].empty?
end
hash = super(options)
hash.transform_keys { |key| key.delete("?") }
end
end
concerning :SearchMethods do
class_methods do
def model_restriction(table)
table.project(1)
end
def attribute_restriction(*)
all
end
end
end
concerning :ActiveRecordExtensions do
class_methods do
def without_timeout
connection.execute("SET STATEMENT_TIMEOUT = 0") unless Rails.env.test?
yield
ensure
connection.execute("SET STATEMENT_TIMEOUT = #{CurrentUser.user.try(:statement_timeout) || 3_000}") unless Rails.env.test?
end
def with_timeout(n, default_value = nil, new_relic_params = {})
connection.execute("SET STATEMENT_TIMEOUT = #{n}") unless Rails.env.test?
yield
rescue ::ActiveRecord::StatementInvalid => e
DanbooruLogger.log(e, expected: true, **new_relic_params)
default_value
ensure
connection.execute("SET STATEMENT_TIMEOUT = #{CurrentUser.user.try(:statement_timeout) || 3_000}") unless Rails.env.test?
end
def update!(*args)
all.each { |record| record.update!(*args) }
end
def each_duplicate(*columns)
return enum_for(:each_duplicate, *columns) unless block_given?
group(columns).having("count(*) > 1").count.each do |values, count|
hash = columns.zip(Array.wrap(values)).to_h
yield count: count, **hash
end
end
def destroy_duplicates!(*columns, log: true)
each_duplicate(*columns) do |count:, **columns_with_values|
records = where(columns_with_values).order(:id)
dupes = records.drop(1)
if log
data = { keep: records.first.id, destroy: dupes.map(&:id), count: count, **columns_with_values }
DanbooruLogger.info("Destroying duplicate #{self.name} #{dupes.map(&:id).join(", ")}", data)
end
dupes.each(&:destroy!)
end
end
end
# Save the record, but convert RecordNotUnique exceptions thrown by the database into
# Rails validation errors. This way duplicate records only return one type of error.
# This assumes the table only has one uniqueness constraint in the database.
def save_if_unique(column)
save
rescue ActiveRecord::RecordNotUnique => e
self.errors.add(column, :taken)
false
end
end
concerning :UserMethods do
class_methods do
def belongs_to_updater(**options)
class_eval do
belongs_to :updater, class_name: "User", **options
before_validation do |rec|
rec.updater_id = CurrentUser.id
rec.updater_ip_addr = CurrentUser.ip_addr if rec.respond_to?(:updater_ip_addr=)
end
end
end
end
end
concerning :DtextMethods do
def dtext_shortlink(**_options)
"#{self.class.name.underscore.tr("_", " ")} ##{id}"
end
end
concerning :ConcurrencyMethods do
class_methods do
def parallel_each(batch_size: 1000, in_processes: 4, in_threads: nil, &block)
# XXX We may deadlock if a transaction is open; do a non-parallel each.
return find_each(&block) if connection.transaction_open?
# XXX Use threads in testing because processes can't see each other's
# database transactions.
if Rails.env.test?
in_processes = nil
in_threads = 2
end
current_user = CurrentUser.user
current_ip = CurrentUser.ip_addr
find_in_batches(batch_size: batch_size, error_on_ignore: true) do |batch|
Parallel.each(batch, in_processes: in_processes, in_threads: in_threads) do |record|
# XXX In threaded mode, the current user isn't inherited from the
# parent thread because the current user is a thread-local
# variable. Hence, we have to set it explicitly in the child thread.
CurrentUser.scoped(current_user, current_ip) do
yield record
end
end
end
end
end
end
def revised?
updated_at > created_at
end
def warnings
@warnings ||= ActiveModel::Errors.new(self)
end
end