Refactor models so that we define attribute API permissions in policy files instead of directly in models. This is cleaner because a) permissions are better handled by policies and b) which attributes are visible to the API is an API-level concern that models shouldn't have to care about. This fixes an issue with not being able to precompile CSS/JS assets unless the database was up and running. This was a problem when building Docker images because we don't have a database at build time. We needed the database because `api_attributes` was a class-level macro in some places, which meant it ran at boot time, but this triggered a database call because api_attributes used database introspection to get the list of allowed API attributes.
178 lines
5.1 KiB
Ruby
178 lines
5.1 KiB
Ruby
class ApplicationRecord < ActiveRecord::Base
|
|
self.abstract_class = true
|
|
|
|
include Deletable
|
|
include Mentionable
|
|
extend HasBitFlags
|
|
extend Searchable
|
|
|
|
concerning :PaginationMethods do
|
|
class_methods do
|
|
def paginate(*args, **options)
|
|
extending(PaginationExtension).paginate(*args, **options)
|
|
end
|
|
|
|
def paginated_search(params, count_pages: params[:search].present?, **defaults)
|
|
search_params = params.fetch(:search, {}).permit!
|
|
search_params = defaults.merge(search_params).with_indifferent_access
|
|
|
|
search(search_params).paginate(params[:page], limit: params[:limit], search_count: count_pages)
|
|
end
|
|
end
|
|
end
|
|
|
|
concerning :PrivilegeMethods do
|
|
class_methods do
|
|
def visible(user)
|
|
all
|
|
end
|
|
end
|
|
end
|
|
|
|
concerning :ApiMethods do
|
|
class_methods do
|
|
def available_includes
|
|
[]
|
|
end
|
|
|
|
def multiple_includes
|
|
reflections.reject { |k,v| v.macro != :has_many }.keys.map(&:to_sym)
|
|
end
|
|
|
|
def associated_models(name)
|
|
if reflections[name].options[:polymorphic]
|
|
associated_models = reflections[name].active_record.try(:model_types) || []
|
|
else
|
|
associated_models = [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)
|
|
Pundit.policy!([user, nil], self).api_attributes
|
|
end
|
|
|
|
def html_data_attributes
|
|
data_attributes = self.class.columns.select do |column|
|
|
column.type.in?([:integer, :boolean]) && !column.array?
|
|
end.map(&:name).map(&:to_sym)
|
|
|
|
api_attributes & data_attributes
|
|
end
|
|
|
|
def serializable_hash(options = {})
|
|
options ||= {}
|
|
if options[:only] && 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 :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 => x
|
|
DanbooruLogger.log(x, expected: false, **new_relic_params)
|
|
return default_value
|
|
ensure
|
|
connection.execute("SET STATEMENT_TIMEOUT = #{CurrentUser.user.try(:statement_timeout) || 3_000}") unless Rails.env == "test"
|
|
end
|
|
end
|
|
end
|
|
|
|
concerning :PostgresExtensions do
|
|
class_methods do
|
|
def columns(*params)
|
|
super.reject {|x| x.sql_type == "tsvector"}
|
|
end
|
|
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 :AttributeMethods do
|
|
class_methods do
|
|
# Defines `<attribute>_string`, `<attribute>_string=`, and `<attribute>=`
|
|
# methods for converting an array attribute to or from a string.
|
|
#
|
|
# The `<attribute>=` setter parses strings into an array using the
|
|
# `parse` regex. The resulting strings can be converted to another type
|
|
# with the `cast` option.
|
|
def array_attribute(name, parse: /[^[:space:]]+/, cast: :itself)
|
|
define_method "#{name}_string" do
|
|
send(name).join(" ")
|
|
end
|
|
|
|
define_method "#{name}_string=" do |value|
|
|
raise ArgumentError, "#{name} must be a String" unless value.respond_to?(:to_str)
|
|
send("#{name}=", value)
|
|
end
|
|
|
|
define_method "#{name}=" do |value|
|
|
if value.respond_to?(:to_str)
|
|
super value.to_str.scan(parse).map(&cast)
|
|
elsif value.respond_to?(:to_a)
|
|
super value.to_a
|
|
else
|
|
raise ArgumentError, "#{name} must be a String or an Array"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def warnings
|
|
@warnings ||= ActiveModel::Errors.new(self)
|
|
end
|
|
end
|