* Moved queries into PartyQueryBuilder service PartyQueryBuilder supersedes PartyQueryingConcern as it is also used for UsersController (and is our fix for profiles being broken) * Implement PartyQueryBuilder in controllers * Update summon_transformer.rb This should fix the transformer so that we properly capture summons and subaura summons * Update parties_controller_spec.rb * Add NewRelic license key * Add Sentry Why not?
221 lines
7.7 KiB
Ruby
221 lines
7.7 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)
|
||
conditions = build_filters
|
||
query = query.where(conditions)
|
||
# If name_quality filtering is enabled via params, apply it.
|
||
query = query.where(name_quality) if @params[:name_quality].present?
|
||
query.where(
|
||
weapons_count: build_count(@params[:weapons_count], default_weapons_count)..max_weapons,
|
||
characters_count: build_count(@params[:characters_count], default_characters_count)..max_characters,
|
||
summons_count: build_count(@params[:summons_count], default_summons_count)..max_summons
|
||
)
|
||
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
|
||
|
||
# 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
|