# 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 extend Aggregatable 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 # @param current_user [User] The user performing the search def paginated_search(params, page: params[:page], limit: params[:limit], count_pages: params[:search].present?, count: nil, defaults: {}, current_user: CurrentUser.user) 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, current_user).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 visible_for_search(attribute, current_user) policy(current_user).visible_for_search(all, attribute) 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 set_timeout(n) connection.execute("SET STATEMENT_TIMEOUT = #{n}") unless Rails.env.test? yield ensure connection.execute("SET STATEMENT_TIMEOUT = #{CurrentUser.user.statement_timeout}") unless Rails.env.test? end 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 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 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) 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