* Update test csvs * Fix count filters and refactor apply_filters * Update party_querying_concern.rb * +tests/-debug logs * Make party association optional in Job * Updates for weapon series - Change to new series numbers - Add static method for querying whether the weapon's element is changeable - Add a new method to return a text slug for the weapon's series * Add and update test data - Updates canonical.rb for loading multiple types of data with multiple types of associations - Adds test data for Guidebooks, Job Accessories, Job Skills, and Jobs - Updates test data for Weapons and Summons * Migrations - Adds series of migrations for changing the weapon's series to the values used by Cygames - Shuffled around some foreign keys * Implement BaseProcessor Processors are in charge of processing deck data straight from Granblue. * Implement CharacterProcessor Process character data from deck * Implement WeaponProcessor Process weapon data from deck * Implement JobProcessor Process job, job skill, and job accessory data from deck * Implement SummonProcessor Process summon data from deck * Update SummonProcessor to work like the others * ImportController should use processors * Process element for changeable weapons
274 lines
9.8 KiB
Ruby
274 lines
9.8 KiB
Ruby
# frozen_string_literal: true
|
||
|
||
# PartyQueryBuilder is responsible for building an ActiveRecord query for parties
|
||
# by applying a series of filters, includes, and excludes based on request parameters.
|
||
# It is used to build both the general parties query and specialized queries (like
|
||
# for a user’s profile) while keeping the filtering logic DRY.
|
||
#
|
||
# Usage:
|
||
# base_query = Party.includes(:user, :job, ... ) # a starting query
|
||
# query_builder = PartyQueryBuilder.new(base_query, params: params, current_user: current_user, options: { default_status: 'active' })
|
||
# final_query = query_builder.build
|
||
#
|
||
class PartyQueryBuilder
|
||
# Initialize with a base query, a params hash, and the current user.
|
||
# Options may include default filters like :default_status, default counts, and max values.
|
||
def initialize(base_query, params:, current_user:, options: {})
|
||
@base_query = base_query
|
||
@params = params
|
||
@current_user = current_user
|
||
@options = options
|
||
end
|
||
|
||
# Builds the final ActiveRecord query by applying filters, includes, and excludes.
|
||
#
|
||
# Edge cases handled:
|
||
# - If a parameter is missing or blank, default values are used.
|
||
# - If no recency is provided, no date range is applied.
|
||
# - If includes/excludes parameters are missing, those methods are skipped.
|
||
#
|
||
# Also applies a default status filter (if provided via options) using a dedicated callback.
|
||
def build
|
||
query = @base_query
|
||
query = apply_filters(query)
|
||
query = apply_default_status(query) if @options[:default_status]
|
||
query = apply_privacy_settings(query)
|
||
query = apply_includes(query, @params[:includes]) if @params[:includes].present?
|
||
query = apply_excludes(query, @params[:excludes]) if @params[:excludes].present?
|
||
query.order(created_at: :desc)
|
||
end
|
||
|
||
private
|
||
|
||
# Applies filtering conditions to the given query.
|
||
# Combines generic filters (like element, raid_id, created_at) with object count ranges.
|
||
#
|
||
# Example edge case: If the request does not specify 'characters_count',
|
||
# then the default (e.g. 3) will be used, with the upper bound coming from a constant.
|
||
def apply_filters(query)
|
||
query = apply_base_filters(query)
|
||
query = apply_name_quality_filter(query)
|
||
query = apply_count_filters(query)
|
||
|
||
query
|
||
end
|
||
|
||
# Example callback method: if no explicit status filter is provided, we may want
|
||
# to force the query to include only records with a given default status.
|
||
# This method encapsulates that behavior.
|
||
def apply_default_status(query)
|
||
query.where(status: @options[:default_status])
|
||
end
|
||
|
||
# Applies privacy settings based on whether the current user is an admin.
|
||
def apply_privacy_settings(query)
|
||
# If the options say to skip privacy filtering (e.g. when viewing your own profile),
|
||
# then return the query unchanged.
|
||
return query if @options[:skip_privacy]
|
||
|
||
# Otherwise, if not admin, only show public parties.
|
||
return query if @current_user&.admin?
|
||
|
||
query.where('visibility = ?', 1)
|
||
end
|
||
|
||
# Builds a hash of filtering conditions from the params.
|
||
#
|
||
# Uses guard clauses to ignore keys when a parameter is missing.
|
||
def build_filters
|
||
{
|
||
element: (@params[:element].present? ? @params[:element].to_i : nil),
|
||
raid_id: @params[:raid],
|
||
created_at: build_date_range,
|
||
full_auto: build_option(@params[:full_auto]),
|
||
auto_guard: build_option(@params[:auto_guard]),
|
||
charge_attack: build_option(@params[:charge_attack])
|
||
}.compact
|
||
end
|
||
|
||
# Returns a date range based on the 'recency' parameter.
|
||
# If recency is not provided, returns nil so no date filter is applied.
|
||
def build_date_range
|
||
return nil unless @params[:recency].present?
|
||
start_time = DateTime.current - @params[:recency].to_i.seconds
|
||
start_time.beginning_of_day..DateTime.current
|
||
end
|
||
|
||
# Returns the count from the parameter or a default value if the parameter is blank.
|
||
def build_count(value, default_value)
|
||
value.blank? ? default_value : value.to_i
|
||
end
|
||
|
||
# Processes an option parameter.
|
||
# Returns the integer value unless the value is blank or equal to -1.
|
||
def build_option(value)
|
||
value.to_i unless value.blank? || value.to_i == -1
|
||
end
|
||
|
||
# Applies "includes" filtering to the query based on a comma-separated string.
|
||
# For each provided ID, it adds a condition using an EXISTS subquery.
|
||
#
|
||
# Edge case example: If an ID does not start with a known prefix,
|
||
# grid_table_and_object_table returns [nil, nil] and the condition is skipped.
|
||
def apply_includes(query, includes)
|
||
includes.split(',').each do |id|
|
||
grid_table, object_table = grid_table_and_object_table(id)
|
||
next unless grid_table && object_table
|
||
condition = <<-SQL.squish
|
||
EXISTS (
|
||
SELECT 1 FROM #{grid_table}
|
||
JOIN #{object_table} ON #{grid_table}.#{object_table.singularize}_id = #{object_table}.id
|
||
WHERE #{object_table}.granblue_id = ? AND #{grid_table}.party_id = parties.id
|
||
)
|
||
SQL
|
||
query = query.where(condition, id)
|
||
end
|
||
query
|
||
end
|
||
|
||
# Applies "excludes" filtering to the query based on a comma-separated string.
|
||
# Works similarly to apply_includes, but with a NOT EXISTS clause.
|
||
def apply_excludes(query, excludes)
|
||
excludes.split(',').each do |id|
|
||
grid_table, object_table = grid_table_and_object_table(id)
|
||
next unless grid_table && object_table
|
||
condition = <<-SQL.squish
|
||
NOT EXISTS (
|
||
SELECT 1 FROM #{grid_table}
|
||
JOIN #{object_table} ON #{grid_table}.#{object_table.singularize}_id = #{object_table}.id
|
||
WHERE #{object_table}.granblue_id = ? AND #{grid_table}.party_id = parties.id
|
||
)
|
||
SQL
|
||
query = query.where(condition, id)
|
||
end
|
||
query
|
||
end
|
||
|
||
# Applies base filtering conditions from build_filters to the query.
|
||
# @param query [ActiveRecord::QueryMethods::WhereChain] The current query.
|
||
# @return [ActiveRecord::Relation] The query with base filters applied.
|
||
def apply_base_filters(query)
|
||
query.where(build_filters)
|
||
end
|
||
|
||
# Applies the name quality filter to the query if the parameter is present.
|
||
# @param query [ActiveRecord::QueryMethods::WhereChain] The current query.
|
||
# @return [ActiveRecord::Relation] The query with the name quality filter applied.
|
||
def apply_name_quality_filter(query)
|
||
@params[:name_quality].present? ? query.where(name_quality) : query
|
||
end
|
||
|
||
# Applies count filters to the query based on provided parameters or default options.
|
||
# If apply_defaults is set in options, default ranges are applied.
|
||
# Otherwise, count ranges are built from provided parameters.
|
||
# @param query [ | ActiveRecord::QueryMethods::WhereChain] The current query.
|
||
# @return [ActiveRecord::Relation] The query with count filters applied.
|
||
def apply_count_filters(query)
|
||
if @options[:apply_defaults]
|
||
query.where(
|
||
weapons_count: default_weapons_count..max_weapons,
|
||
characters_count: default_characters_count..max_characters,
|
||
summons_count: default_summons_count..max_summons
|
||
)
|
||
elsif count_filter_provided?
|
||
query.where(build_count_conditions)
|
||
else
|
||
query
|
||
end
|
||
end
|
||
|
||
# Determines if any count filter parameters have been provided.
|
||
# @return [Boolean] True if any count filters are provided, false otherwise.
|
||
def count_filter_provided?
|
||
@params.key?(:weapons_count) || @params.key?(:characters_count) || @params.key?(:summons_count)
|
||
end
|
||
|
||
# Builds a hash of count conditions based on the count filter parameters.
|
||
# @return [Hash] A hash with keys :weapons_count, :characters_count, and :summons_count.
|
||
def build_count_conditions
|
||
{
|
||
weapons_count: build_range(@params[:weapons_count], max_weapons),
|
||
characters_count: build_range(@params[:characters_count], max_characters),
|
||
summons_count: build_range(@params[:summons_count], max_summons)
|
||
}
|
||
end
|
||
|
||
# Constructs a range for a given count parameter.
|
||
# @param param_value [String, nil] The count filter parameter value.
|
||
# @param max_value [Integer] The maximum allowed value for the count.
|
||
# @return [Range] A range from the provided count (or 0 if blank) to the max_value.
|
||
def build_range(param_value, max_value)
|
||
param_value.present? ? param_value.to_i..max_value : 0..max_value
|
||
end
|
||
|
||
# Maps an ID’s first character to the corresponding grid table and object table names.
|
||
#
|
||
# For example:
|
||
# '3...' => %w[grid_characters characters]
|
||
# '2...' => %w[grid_summons summons]
|
||
# '1...' => %w[grid_weapons weapons]
|
||
# Returns [nil, nil] for unknown prefixes.
|
||
def grid_table_and_object_table(id)
|
||
case id[0]
|
||
when '3'
|
||
%w[grid_characters characters]
|
||
when '2'
|
||
%w[grid_summons summons]
|
||
when '1'
|
||
%w[grid_weapons weapons]
|
||
else
|
||
[nil, nil]
|
||
end
|
||
end
|
||
|
||
# Default values and maximum limits for counts.
|
||
def default_weapons_count
|
||
@options[:default_weapons_count] || 5
|
||
end
|
||
|
||
def default_characters_count
|
||
@options[:default_characters_count] || 3
|
||
end
|
||
|
||
def default_summons_count
|
||
@options[:default_summons_count] || 2
|
||
end
|
||
|
||
def max_weapons
|
||
@options[:max_weapons] || 13
|
||
end
|
||
|
||
def max_characters
|
||
@options[:max_characters] || 5
|
||
end
|
||
|
||
def max_summons
|
||
@options[:max_summons] || 8
|
||
end
|
||
|
||
# Stub method for name quality filtering.
|
||
# In your application, this might be defined in a helper or concern.
|
||
def name_quality
|
||
# Example: exclude parties with names like 'Untitled' (edge case)
|
||
"name NOT LIKE 'Untitled%'"
|
||
end
|
||
|
||
# Stub method for user quality filtering.
|
||
# Adjust as needed for your actual implementation.
|
||
def user_quality
|
||
'user_id IS NOT NULL'
|
||
end
|
||
|
||
# Stub method for original filtering.
|
||
def original
|
||
'source_party_id IS NULL'
|
||
end
|
||
|
||
# Stub method for privacy filtering.
|
||
# Here we assume that if the current user is not an admin, only public parties (visibility = 1) are returned.
|
||
def privacy
|
||
return nil if @current_user && @current_user.admin?
|
||
|
||
'visibility = 1'
|
||
end
|
||
end
|