* Update gitignore There is a mystery postgres folder and we are going to ignore it * Add migrations * Update preview state default to pending * Adds indexes * Adds PgHero and PgStatements * Update .gitignore * Update Gemfile Production: - `newrelic_rpm` Development: - `pg_query` - `prosopite` * Configure Sidekiq Create job for cleaning up party previews * Configure Prosopite and remove CacheFreeLogger * Enable query logging * Update api_controller.rb Add N+1 detectioin via Prosopite in development/test environments * Refactor canonical object blueprints * Refactor grid object blueprints * Remove N+1 from grid object models Reimplementing `character` `summon` and `weapon` was making N+1s which made queries really slow * Add counter caches to party * Add preview generation helpers The Party model can respond to queries about its preview state with the following models: - `schedule_preview_generation` - `preview_content_changed?` - `preview_expired?` - `should_generate_preview?` - `ready_for_preview?` - `needs_preview_generation?` - `preview_relevant_attributes` Removes the following methods: - `schedule_preview_regeneration` - `preview_relevant_changes?` * Add cache to is_favorited(user) * Refactored PartyBlueprint to minimize N+1s * Remove preview expiry constants These are defined in the Coordinator instead * Add method comments * Create parties_controller.rbs * Update logic and logs * Updates excluded methods and calculate_count * Use `includes` instead of `joins` * Use a less-insane way of counting * Adds a helper method for party privacy * Update filter condition helpers Just minor refactoring * Fix old view name in PartyBlueprint * Refactor parties#create * Remove redundant return * Update parties_controller.rbs * Update parties#index * Update parties_controller.rb Updates apply_includes and apply_excludes, along with modifying id_to_table and build_query * Update parties_controller.rb Adds the rest of the changes, too tired to write them all out. Some preview generation, some filtering * Refactor parties#index and parties#favorites These are mostly the same methods, so we remove common code into build_parties_query and render_paginated_parties * Alias table name to object to maintain API consistency * Maintain API consistency with raid blueprint * Optimize party loading by adding eager loading to `set_from_slug` - Refactored `set_from_slug` to use `includes` for eager loading associated models: - `user`, `job`, `raid` (with `group`) - `characters` (with `character` and `awakening`) - `weapons` (with `weapon`, `awakenings`, `weapon_key1`, `weapon_key2`, `weapon_key3`) - `summons` (with `summon`) - `guidebooks` (`guidebook1`, `guidebook2`, `guidebook3`) - `source_party`, `remixes`, `skills`, and `accessory` - This change improves query efficiency by reducing N+1 queries and ensures all relevant associations are preloaded. - Removed redundant favorite check as it was not necessary in this context. * Refactor grid blueprints - **GridCharacterBlueprint:** - Removed `:minimal` view restriction on `party` association. - Improved nil checks for `ring1`, `ring2`, and `earring` to prevent errors. - Converted string values in `awakening_level`, `over_mastery`, and `aetherial_mastery` fields to integers for consistency. - Ensured `over_mastery` and `aetherial_mastery` only include valid entries, filtering out blank or zero-modifier values. - **GridWeaponBlueprint:** - Removed `:minimal` view restriction on `party` association. - Ensured `weapon` association exists before accessing `ax`, `series`, or `awakening`. - Improved conditional checks for `weapon_keys` to prevent errors when `weapon` or `series` is nil. - Converted `awakening_level` field to integer for consistency. - **GridCharacterBlueprint:** - Removed `:minimal` view restriction on `party` association. * Update raid blueprints - Show flat representation of raid group in RaidBlueprint's nested view - Show nested representation of raid in RaidGroupBlueprint's full view * Move n+1 detection to around_action hook * Improve handling mastery bonuses - Improved handling of nested attributes: - Replaced old mastery structure with new `rings` and `awakening` assignments. - Added `new_rings` and `new_awakening` virtual attributes for easier updates. - Updated `assign_attributes` to exclude `rings` and `awakening` to prevent conflicts. - Enhanced parameter transformation: - Introduced `transform_character_params` to process `rings`, `awakening`, and `earring` more reliably. - Ensured proper type conversion (`to_i`) for numeric values in `uncap_level`, `transcendence_step`, and `awakening_level`. - Improved error handling for missing values by setting defaults where needed. - Optimized database queries: - Added `.includes(:awakening)` to `set` to prevent N+1 query issues. - Updated strong parameters: - Changed `rings` from individual keys (`ring1`, `ring2`, etc.) to a structured array format. - Refactored permitted attributes to align with the new nested structure. * Eager-load jobs when querying job skills * Eager load raids/groups when querying * Update users_controller.rb More efficient way of denoting favorited parties. * Update awakening.rb - Removes explicitly defined associations and adds ActiveRecord associations instead * Update party.rb - Removes favorited accessor - Renames derivative_parties to remixes and adds in-built sort * Update weapon_awakening.rb - Removes redefined explicit associations * Update grid_character.rb - Adds code transforming incoming ring and awakening values into something the db understands * Update character.rb Add explicit Awakenings enum * Update coordinator.rb Adds 'queued' as a state for generation
762 lines
26 KiB
Ruby
762 lines
26 KiB
Ruby
# frozen_string_literal: true
|
||
|
||
module Api
|
||
module V1
|
||
# Controller for managing party-related operations in the API
|
||
# @api public
|
||
class PartiesController < Api::V1::ApiController
|
||
before_action :set_from_slug,
|
||
except: %w[create destroy update index favorites]
|
||
before_action :set, only: %w[update destroy]
|
||
before_action :authorize, only: %w[update destroy]
|
||
|
||
# == Constants
|
||
|
||
# Maximum number of characters allowed in a party
|
||
MAX_CHARACTERS = 5
|
||
|
||
# Maximum number of summons allowed in a party
|
||
MAX_SUMMONS = 8
|
||
|
||
# Maximum number of weapons allowed in a party
|
||
MAX_WEAPONS = 13
|
||
|
||
# Default minimum number of characters required for filtering
|
||
DEFAULT_MIN_CHARACTERS = 3
|
||
|
||
# Default minimum number of summons required for filtering
|
||
DEFAULT_MIN_SUMMONS = 2
|
||
|
||
# Default minimum number of weapons required for filtering
|
||
DEFAULT_MIN_WEAPONS = 5
|
||
|
||
# Default maximum clear time in seconds
|
||
DEFAULT_MAX_CLEAR_TIME = 5400
|
||
|
||
# == Primary CRUD Actions
|
||
|
||
# Creates a new party with optional user association
|
||
# @return [void]
|
||
def create
|
||
# Build the party with the provided parameters and assign the user
|
||
party = Party.new(party_params)
|
||
party.user = current_user if current_user
|
||
|
||
# If a raid_id is given, look it up and assign the extra flag from its group.
|
||
if party_params && party_params[:raid_id].present?
|
||
if (raid = Raid.find_by(id: party_params[:raid_id]))
|
||
party.extra = raid.group.extra
|
||
end
|
||
end
|
||
|
||
# Save and render the party, triggering preview generation if the party is ready
|
||
if party.save
|
||
party.schedule_preview_generation if party.ready_for_preview?
|
||
render json: PartyBlueprint.render(party, view: :created, root: :party),
|
||
status: :created
|
||
else
|
||
render_validation_error_response(party)
|
||
end
|
||
end
|
||
|
||
# Shows a specific party if the user has permission to view it
|
||
# @return [void]
|
||
def show
|
||
# If a party is private, check that the user is the owner or an admin
|
||
if (@party.private? && !current_user) || (@party.private? && not_owner && !admin_mode)
|
||
return render_unauthorized_response
|
||
end
|
||
|
||
return render json: PartyBlueprint.render(@party, view: :full, root: :party) if @party
|
||
|
||
render_not_found_response('project')
|
||
end
|
||
|
||
# Updates an existing party's attributes
|
||
# @return [void]
|
||
def update
|
||
@party.attributes = party_params.except(:skill1_id, :skill2_id, :skill3_id)
|
||
|
||
if party_params && party_params[:raid_id]
|
||
raid = Raid.find_by(id: party_params[:raid_id])
|
||
@party.extra = raid.group.extra
|
||
end
|
||
|
||
# TODO: Validate accessory with job
|
||
|
||
return render json: PartyBlueprint.render(@party, view: :full, root: :party) if @party.save
|
||
|
||
render_validation_error_response(@party)
|
||
end
|
||
|
||
# Deletes a party if the user has permission
|
||
# @return [void]
|
||
def destroy
|
||
render json: PartyBlueprint.render(@party, view: :destroyed, root: :checkin) if @party.destroy
|
||
end
|
||
|
||
# == Extended Party Actions
|
||
|
||
# Creates a copy of an existing party with attribution
|
||
# @return [void]
|
||
def remix
|
||
new_party = @party.amoeba_dup
|
||
new_party.attributes = {
|
||
user: current_user,
|
||
name: remixed_name(@party.name),
|
||
source_party: @party,
|
||
remix: true
|
||
}
|
||
|
||
new_party.local_id = party_params[:local_id] unless party_params.nil?
|
||
|
||
if new_party.save
|
||
# Remixed parties should have content, so generate preview
|
||
new_party.schedule_preview_generation
|
||
render json: PartyBlueprint.render(new_party, view: :created, root: :party),
|
||
status: :created
|
||
else
|
||
render_validation_error_response(new_party)
|
||
end
|
||
end
|
||
|
||
# Lists parties based on various filter criteria
|
||
# @return [void]
|
||
def index
|
||
query = build_parties_query
|
||
@parties = query.paginate(page: params[:page], per_page: COLLECTION_PER_PAGE)
|
||
render_paginated_parties(@parties)
|
||
end
|
||
|
||
# Lists parties favorited by the current user
|
||
# @return [void]
|
||
def favorites
|
||
raise Api::V1::UnauthorizedError unless current_user
|
||
|
||
query = build_parties_query(favorites: true)
|
||
@parties = query.paginate(page: params[:page], per_page: COLLECTION_PER_PAGE)
|
||
# Mark each party as favorited (if needed)
|
||
@parties.each { |party| party.favorited = true }
|
||
render_paginated_parties(@parties)
|
||
end
|
||
|
||
# == Preview Management
|
||
|
||
# Serves the party's preview image
|
||
# @return [void]
|
||
def preview
|
||
coordinator = PreviewService::Coordinator.new(@party)
|
||
|
||
if coordinator.generation_in_progress?
|
||
response.headers['Retry-After'] = '2'
|
||
default_path = Rails.root.join('public', 'default-previews', "#{@party.element || 'default'}.png")
|
||
send_file default_path,
|
||
type: 'image/png',
|
||
disposition: 'inline'
|
||
return
|
||
end
|
||
|
||
# Try to get the preview or send default
|
||
begin
|
||
if Rails.env.production?
|
||
# Stream S3 content instead of redirecting
|
||
s3_object = coordinator.get_s3_object
|
||
send_data s3_object.body.read,
|
||
filename: "#{@party.shortcode}.png",
|
||
type: 'image/png',
|
||
disposition: 'inline'
|
||
else
|
||
# In development, serve from local filesystem
|
||
send_file coordinator.local_preview_path,
|
||
type: 'image/png',
|
||
disposition: 'inline'
|
||
end
|
||
rescue Aws::S3::Errors::NoSuchKey
|
||
# Schedule generation if needed
|
||
coordinator.schedule_generation unless coordinator.generation_in_progress?
|
||
|
||
# Return default preview while generating
|
||
send_file Rails.root.join('public', 'default-previews', "#{@party.element || 'default'}.png"),
|
||
type: 'image/png',
|
||
disposition: 'inline'
|
||
end
|
||
end
|
||
|
||
# Returns the current status of a party's preview
|
||
# @return [void]
|
||
def preview_status
|
||
party = Party.find_by!(shortcode: params[:id])
|
||
render json: {
|
||
state: party.preview_state,
|
||
generated_at: party.preview_generated_at,
|
||
ready_for_preview: party.ready_for_preview?
|
||
}
|
||
end
|
||
|
||
# Forces regeneration of a party's preview image
|
||
# @return [void]
|
||
def regenerate_preview
|
||
party = Party.find_by!(shortcode: params[:id])
|
||
|
||
# Ensure only party owner can force regeneration
|
||
unless current_user && party.user_id == current_user.id
|
||
return render_unauthorized_response
|
||
end
|
||
|
||
preview_service = PreviewService::Coordinator.new(party)
|
||
if preview_service.force_regenerate
|
||
render json: { status: 'Preview regeneration started' }
|
||
else
|
||
render json: { error: 'Preview regeneration failed' },
|
||
status: :unprocessable_entity
|
||
end
|
||
end
|
||
|
||
private
|
||
|
||
# Builds the base query for parties, optionally including favorites-specific conditions.
|
||
def build_parties_query(favorites: false)
|
||
query = Party.includes(
|
||
{ raid: :group },
|
||
:job,
|
||
:user,
|
||
:skill0,
|
||
:skill1,
|
||
:skill2,
|
||
:skill3,
|
||
:guidebook1,
|
||
:guidebook2,
|
||
:guidebook3,
|
||
{ characters: :character },
|
||
{ weapons: :weapon },
|
||
{ summons: :summon }
|
||
)
|
||
# Add favorites join and condition if favorites is true.
|
||
if favorites
|
||
query = query.joins(:favorites)
|
||
.where(favorites: { user_id: current_user.id })
|
||
.distinct
|
||
query = query.order(created_at: :desc)
|
||
else
|
||
query = query.order(visibility: :asc, created_at: :desc)
|
||
end
|
||
|
||
query = apply_filters(query)
|
||
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
|
||
end
|
||
|
||
# Renders the paginated parties with blueprint and meta data.
|
||
def render_paginated_parties(parties)
|
||
render json: PartyBlueprint.render(
|
||
parties,
|
||
view: :preview,
|
||
root: :results,
|
||
meta: {
|
||
count: parties.total_entries,
|
||
total_pages: parties.total_pages,
|
||
per_page: COLLECTION_PER_PAGE
|
||
},
|
||
current_user: current_user
|
||
)
|
||
end
|
||
|
||
# == Authorization Helpers
|
||
|
||
# Checks if the current user is authorized to modify the party
|
||
# @return [void]
|
||
def authorize
|
||
return unless not_owner && !admin_mode
|
||
|
||
render_unauthorized_response
|
||
end
|
||
|
||
# Determines if the current user is not the owner of the party
|
||
# @return [Boolean]
|
||
def not_owner
|
||
if @party.user
|
||
# party has a user and current_user does not match
|
||
return true if current_user != @party.user
|
||
|
||
# party has a user, there's no current_user, but edit_key is provided
|
||
return true if current_user.nil? && edit_key
|
||
else
|
||
# party has no user, there's no current_user and there's no edit_key provided
|
||
return true if current_user.nil? && edit_key.nil?
|
||
|
||
# party has no user, there's no current_user, and the party's edit_key doesn't match the provided edit_key
|
||
return true if current_user.nil? && @party.edit_key != edit_key
|
||
end
|
||
|
||
false
|
||
end
|
||
|
||
# == Preview Generation
|
||
|
||
# Schedules a background job to generate the party preview
|
||
# @return [void]
|
||
def schedule_preview_generation
|
||
GeneratePartyPreviewJob.perform_later(id)
|
||
end
|
||
|
||
# == Query Building Helpers
|
||
|
||
def apply_filters(query)
|
||
conditions = build_filters
|
||
|
||
# Use the compound indexes effectively
|
||
query = query.where(conditions)
|
||
.where(name_quality) if params[:name_quality].present?
|
||
|
||
# Use the counters index
|
||
query = query.where(
|
||
weapons_count: build_count(params[:weapons_count], DEFAULT_MIN_WEAPONS)..MAX_WEAPONS,
|
||
characters_count: build_count(params[:characters_count], DEFAULT_MIN_CHARACTERS)..MAX_CHARACTERS,
|
||
summons_count: build_count(params[:summons_count], DEFAULT_MIN_SUMMONS)..MAX_SUMMONS
|
||
)
|
||
|
||
query
|
||
end
|
||
|
||
def apply_privacy_settings(query)
|
||
return query if admin_mode
|
||
|
||
if params[:favorites].present?
|
||
query.where('visibility < 3')
|
||
else
|
||
query.where(visibility: 1)
|
||
end
|
||
end
|
||
|
||
# Builds filter conditions from request parameters
|
||
# @return [Hash] conditions for the query
|
||
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]),
|
||
characters_count: build_count(params[:characters_count], DEFAULT_MIN_CHARACTERS)..MAX_CHARACTERS,
|
||
summons_count: build_count(params[:summons_count], DEFAULT_MIN_SUMMONS)..MAX_SUMMONS,
|
||
weapons_count: build_count(params[:weapons_count], DEFAULT_MIN_WEAPONS)..MAX_WEAPONS
|
||
}.compact
|
||
end
|
||
|
||
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
|
||
|
||
# Paginates the given query of parties and marks favorites for the current user
|
||
#
|
||
# @param query [ActiveRecord::Relation] The base query containing parties
|
||
# @param page [Integer, nil] The page number for pagination (defaults to `params[:page]`)
|
||
# @param per_page [Integer] The number of records per page (defaults to `COLLECTION_PER_PAGE`)
|
||
# @return [ActiveRecord::Relation] The paginated and processed list of parties
|
||
#
|
||
# This method orders parties by creation date in descending order, applies pagination,
|
||
# and marks each party as favorited if the current user has favorited it.
|
||
def paginate_parties(query, page: nil, per_page: COLLECTION_PER_PAGE)
|
||
query.order(created_at: :desc)
|
||
.paginate(page: page || params[:page], per_page: per_page)
|
||
.tap do |parties|
|
||
if current_user
|
||
parties.each { |party| party.favorited = party.is_favorited(current_user) }
|
||
end
|
||
end
|
||
end
|
||
|
||
# == Parameter Processing Helpers
|
||
|
||
# Converts start time parameter for filtering
|
||
# @param recency [String, nil] time period in seconds
|
||
# @return [DateTime, nil] calculated start time
|
||
def build_start_time(recency)
|
||
return unless recency.present?
|
||
|
||
(DateTime.current - recency.to_i.seconds).to_datetime.beginning_of_day
|
||
end
|
||
|
||
# Builds count parameter with default fallback
|
||
# @param value [String, nil] count value
|
||
# @param default [Integer] default value
|
||
# @return [Integer] processed count
|
||
def build_count(value, default)
|
||
value.blank? ? default : value.to_i
|
||
end
|
||
|
||
# Processes maximum clear time parameter
|
||
# @param value [String, nil] clear time value in seconds
|
||
# @return [Integer] processed maximum clear time
|
||
def build_max_clear_time(value)
|
||
value.blank? ? DEFAULT_MAX_CLEAR_TIME : value.to_i
|
||
end
|
||
|
||
# Processes element parameter
|
||
# @param element [String, nil] element identifier
|
||
# @return [Integer, nil] processed element value
|
||
def build_element(element)
|
||
element.to_i unless element.blank?
|
||
end
|
||
|
||
# Processes boolean option parameters
|
||
# @param value [String, nil] option value
|
||
# @return [Integer, nil] processed option value
|
||
def build_option(value)
|
||
value.to_i unless value.blank? || value.to_i == -1
|
||
end
|
||
|
||
# == Query Building Helpers
|
||
|
||
# Constructs the main query for party filtering
|
||
# @param conditions [Hash] filter conditions
|
||
# @param favorites [Boolean] whether to include favorites
|
||
# @return [ActiveRecord::Relation] constructed query
|
||
def build_query(conditions, favorites: false)
|
||
query = Party.distinct
|
||
# joins vs includes? -> reduces n+1s
|
||
.preload(
|
||
weapons: { object: %i[name_en name_jp granblue_id element] },
|
||
summons: { object: %i[name_en name_jp granblue_id element] },
|
||
characters: { object: %i[name_en name_jp granblue_id element] }
|
||
)
|
||
.group('parties.id')
|
||
.where(conditions)
|
||
.where(privacy(favorites: favorites))
|
||
.where(name_quality)
|
||
.where(user_quality)
|
||
.where(original)
|
||
|
||
query = query.includes(:favorites) if favorites
|
||
|
||
query
|
||
end
|
||
|
||
# Applies the include conditions to query
|
||
# @param query [ActiveRecord::Relation] base query
|
||
# @param includes [String] comma-separated list of IDs to include
|
||
# @return [ActiveRecord::Relation] modified query
|
||
def apply_includes(query, includes)
|
||
return query unless includes.present?
|
||
|
||
includes.split(',').each do |id|
|
||
grid_table, object_table = grid_table_and_object_table(id)
|
||
next unless grid_table && object_table
|
||
|
||
# Build a subquery that joins the grid table to the 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 the exclude conditions to query
|
||
# @param query [ActiveRecord::Relation] base query
|
||
# @return [ActiveRecord::Relation] modified query
|
||
def apply_excludes(query, excludes)
|
||
return query unless excludes.present?
|
||
|
||
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
|
||
|
||
# == Query Filtering Helpers
|
||
|
||
# Generates subquery for excluded characters
|
||
# @return [ActiveRecord::Relation, nil] exclusion query
|
||
def excluded_characters
|
||
return unless params[:excludes]
|
||
|
||
excluded = params[:excludes].split(',').filter { |id| id[0] == '3' }
|
||
GridCharacter.includes(:object)
|
||
.where(characters: { granblue_id: excluded })
|
||
.where('grid_characters.party_id = parties.id')
|
||
end
|
||
|
||
# Generates subquery for excluded summons
|
||
# @return [ActiveRecord::Relation, nil] exclusion query
|
||
def excluded_summons
|
||
return unless params[:excludes]
|
||
|
||
excluded = params[:excludes].split(',').filter { |id| id[0] == '2' }
|
||
GridSummon.includes(:object)
|
||
.where(summons: { granblue_id: excluded })
|
||
.where('grid_summons.party_id = parties.id')
|
||
end
|
||
|
||
# Generates subquery for excluded weapons
|
||
# @return [ActiveRecord::Relation, nil] exclusion query
|
||
def excluded_weapons
|
||
return unless params[:excludes]
|
||
|
||
excluded = params[:excludes].split(',').filter { |id| id[0] == '1' }
|
||
GridWeapon.includes(:object)
|
||
.where(weapons: { granblue_id: excluded })
|
||
.where('grid_weapons.party_id = parties.id')
|
||
end
|
||
|
||
# == Query Processing
|
||
|
||
# Fetches and processes parties query with pagination
|
||
# @param query [ActiveRecord::Relation] base query
|
||
# @return [ActiveRecord::Relation] processed and paginated parties
|
||
def fetch_parties(query)
|
||
query.order(created_at: :desc)
|
||
.paginate(page: params[:page], per_page: COLLECTION_PER_PAGE)
|
||
.each { |party| party.favorited = current_user ? party.is_favorited(current_user) : false }
|
||
end
|
||
|
||
# Calculates total count for pagination
|
||
# @param query [ActiveRecord::Relation] current query
|
||
# @return [Integer] total count
|
||
def calculate_count(query)
|
||
# query.count.values.sum
|
||
query.count
|
||
end
|
||
|
||
# Calculates total pages for pagination
|
||
# @param count [Integer] total record count
|
||
# @return [Integer] total pages
|
||
def calculate_total_pages(count)
|
||
# count.to_f / COLLECTION_PER_PAGE > 1 ? (count.to_f / COLLECTION_PER_PAGE).ceil : 1
|
||
(count.to_f / COLLECTION_PER_PAGE).ceil
|
||
end
|
||
|
||
# == Include/Exclude Processing
|
||
|
||
# Generates SQL for including specific items
|
||
# @param id [String] item identifier
|
||
# @return [String] SQL condition
|
||
def includes(id)
|
||
"(\"#{id_to_table(id)}\".\"granblue_id\" = '#{id}')"
|
||
end
|
||
|
||
# Generates SQL for excluding specific items
|
||
# @param id [String] item identifier
|
||
# @return [String] SQL condition
|
||
def excludes(id)
|
||
"(\"#{id_to_table(id)}\".\"granblue_id\" != '#{id}')"
|
||
end
|
||
|
||
# == Filter Condition Helpers
|
||
|
||
# Generates user quality condition
|
||
# @return [String, nil] SQL condition for user quality
|
||
def user_quality
|
||
return if params[:user_quality].blank? || params[:user_quality] == 'false'
|
||
|
||
'user_id IS NOT NULL'
|
||
end
|
||
|
||
# Generates name quality condition
|
||
# @return [String, nil] SQL condition for name quality
|
||
def name_quality
|
||
return if params[:name_quality].blank? || params[:name_quality] == 'false'
|
||
|
||
low_quality = [
|
||
'Untitled',
|
||
'Remix of Untitled',
|
||
'Remix of Remix of Untitled',
|
||
'Remix of Remix of Remix of Untitled',
|
||
'Remix of Remix of Remix of Remix of Untitled',
|
||
'Remix of Remix of Remix of Remix of Remix of Untitled',
|
||
'無題',
|
||
'無題のリミックス',
|
||
'無題のリミックスのリミックス',
|
||
'無題のリミックスのリミックスのリミックス',
|
||
'無題のリミックスのリミックスのリミックスのリミックス',
|
||
'無題のリミックスのリミックスのリミックスのリミックスのリミックス'
|
||
]
|
||
joined_names = low_quality.map { |name| "'#{name}'" }.join(',')
|
||
"name NOT IN (#{joined_names})"
|
||
end
|
||
|
||
# Generates original party condition
|
||
# @return [String, nil] SQL condition for original parties
|
||
def original
|
||
return if params['original'].blank? || params['original'] == 'false'
|
||
|
||
'source_party_id IS NULL'
|
||
end
|
||
|
||
# == Filter Condition Helpers
|
||
|
||
# Generates privacy condition based on favorites
|
||
# @param favorites [Boolean] whether viewing favorites
|
||
# @return [String, nil] SQL condition
|
||
def privacy(favorites: false)
|
||
return if admin_mode
|
||
|
||
if favorites
|
||
'visibility < 3'
|
||
else
|
||
'visibility = 1'
|
||
end
|
||
end
|
||
|
||
# == Utility Methods
|
||
|
||
# Maps ID prefixes to table names
|
||
# @param id [String] item identifier
|
||
# @return [Array(String, String)] corresponding table name
|
||
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
|
||
|
||
# Generates name for remixed party
|
||
# @param name [String] original party name
|
||
# @return [String] generated remix name
|
||
def remixed_name(name)
|
||
blanked_name = {
|
||
en: name.blank? ? 'Untitled team' : name,
|
||
ja: name.blank? ? '無名の編成' : name
|
||
}
|
||
|
||
if current_user
|
||
case current_user.language
|
||
when 'en'
|
||
"Remix of #{blanked_name[:en]}"
|
||
when 'ja'
|
||
"#{blanked_name[:ja]}のリミックス"
|
||
end
|
||
else
|
||
"Remix of #{blanked_name[:en]}"
|
||
end
|
||
end
|
||
|
||
# == Party Loading
|
||
|
||
# Loads party by shortcode for routes using :id
|
||
# @return [void]
|
||
def set_from_slug
|
||
@party = Party.includes(
|
||
:user,
|
||
:job,
|
||
{ raid: :group },
|
||
{ characters: [:character, :awakening] },
|
||
{
|
||
weapons: {
|
||
# Eager load the associated weapon and its awakenings.
|
||
weapon: [:awakenings],
|
||
# Eager load the grid weapon’s own awakening (if applicable).
|
||
awakening: {},
|
||
# Eager load any weapon key associations.
|
||
weapon_key1: {},
|
||
weapon_key2: {},
|
||
weapon_key3: {}
|
||
}
|
||
},
|
||
{ summons: :summon },
|
||
:guidebook1,
|
||
:guidebook2,
|
||
:guidebook3,
|
||
:source_party,
|
||
:remixes,
|
||
:skill0,
|
||
:skill1,
|
||
:skill2,
|
||
:skill3,
|
||
:accessory
|
||
).find_by(shortcode: params[:id])
|
||
|
||
render_not_found_response('party') unless @party
|
||
end
|
||
|
||
# Loads party by ID for update/destroy actions
|
||
# @return [void]
|
||
def set
|
||
@party = Party.where('id = ?', params[:id]).first
|
||
end
|
||
|
||
# == Parameter Sanitization
|
||
|
||
# Sanitizes and permits party parameters
|
||
# @return [Hash, nil] permitted parameters
|
||
def party_params
|
||
return unless params[:party].present?
|
||
|
||
params.require(:party).permit(
|
||
:user_id,
|
||
:local_id,
|
||
:edit_key,
|
||
:extra,
|
||
:name,
|
||
:description,
|
||
:raid_id,
|
||
:job_id,
|
||
:visibility,
|
||
:accessory_id,
|
||
:skill0_id,
|
||
:skill1_id,
|
||
:skill2_id,
|
||
:skill3_id,
|
||
:full_auto,
|
||
:auto_guard,
|
||
:auto_summon,
|
||
:charge_attack,
|
||
:clear_time,
|
||
:button_count,
|
||
:turn_count,
|
||
:chain_count,
|
||
:guidebook1_id,
|
||
:guidebook2_id,
|
||
:guidebook3_id,
|
||
characters_attributes: [:id, :party_id, :character_id, :position,
|
||
:uncap_level, :transcendence_step, :perpetuity,
|
||
:awakening_id, :awakening_level,
|
||
{ ring1: %i[modifier strength], ring2: %i[modifier strength],
|
||
ring3: %i[modifier strength], ring4: %i[modifier strength],
|
||
earring: %i[modifier strength] }],
|
||
summons_attributes: %i[id party_id summon_id position main friend
|
||
quick_summon uncap_level transcendence_step],
|
||
weapons_attributes: %i[id party_id weapon_id
|
||
position mainhand uncap_level transcendence_step element
|
||
weapon_key1_id weapon_key2_id weapon_key3_id
|
||
ax_modifier1 ax_modifier2 ax_strength1 ax_strength2
|
||
awakening_id awakening_level]
|
||
)
|
||
end
|
||
end
|
||
end
|
||
end
|