Jedmund/fix image embeds 4 (#177)
* 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
This commit is contained in:
parent
11d324efe9
commit
6cf11e6517
40 changed files with 1301 additions and 540 deletions
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -7,6 +7,9 @@
|
||||||
# Ignore bundler config.
|
# Ignore bundler config.
|
||||||
/.bundle
|
/.bundle
|
||||||
|
|
||||||
|
# Ignore mystery Postgres folder
|
||||||
|
/postgres
|
||||||
|
|
||||||
# Ignore the default SQLite database.
|
# Ignore the default SQLite database.
|
||||||
/db/*.sqlite3
|
/db/*.sqlite3
|
||||||
/db/*.sqlite3-journal
|
/db/*.sqlite3-journal
|
||||||
|
|
@ -46,3 +49,6 @@ config/application.yml
|
||||||
.vscode/*
|
.vscode/*
|
||||||
|
|
||||||
/config/credentials/production.key
|
/config/credentials/production.key
|
||||||
|
|
||||||
|
# Ignore AI Codebase-generated files
|
||||||
|
codebase.md
|
||||||
|
|
|
||||||
9
Gemfile
9
Gemfile
|
|
@ -2,7 +2,6 @@ source 'https://rubygems.org'
|
||||||
ruby '3.3.7'
|
ruby '3.3.7'
|
||||||
|
|
||||||
gem 'bootsnap'
|
gem 'bootsnap'
|
||||||
gem 'pg'
|
|
||||||
gem 'rack-cors'
|
gem 'rack-cors'
|
||||||
gem 'rails'
|
gem 'rails'
|
||||||
gem 'sprockets-rails'
|
gem 'sprockets-rails'
|
||||||
|
|
@ -10,6 +9,9 @@ gem 'sprockets-rails'
|
||||||
# A Ruby Web Server Built For Concurrency
|
# A Ruby Web Server Built For Concurrency
|
||||||
gem 'puma'
|
gem 'puma'
|
||||||
|
|
||||||
|
# Pg is the Ruby interface to the PostgreSQL RDBMS
|
||||||
|
gem 'pg'
|
||||||
|
|
||||||
# A sophisticated and secure hash algorithm for
|
# A sophisticated and secure hash algorithm for
|
||||||
# hashing passwords.
|
# hashing passwords.
|
||||||
gem 'bcrypt'
|
gem 'bcrypt'
|
||||||
|
|
@ -71,6 +73,9 @@ gem 'httparty'
|
||||||
# StringScanner provides for lexical scanning operations on a String.
|
# StringScanner provides for lexical scanning operations on a String.
|
||||||
gem 'strscan'
|
gem 'strscan'
|
||||||
|
|
||||||
|
# New Relic Ruby Agent
|
||||||
|
gem 'newrelic_rpm'
|
||||||
|
|
||||||
group :doc do
|
group :doc do
|
||||||
gem 'apipie-rails'
|
gem 'apipie-rails'
|
||||||
gem 'sdoc'
|
gem 'sdoc'
|
||||||
|
|
@ -88,6 +93,8 @@ end
|
||||||
|
|
||||||
group :development do
|
group :development do
|
||||||
gem 'listen'
|
gem 'listen'
|
||||||
|
gem 'pg_query'
|
||||||
|
gem 'prosopite'
|
||||||
gem 'solargraph'
|
gem 'solargraph'
|
||||||
gem 'spring'
|
gem 'spring'
|
||||||
gem 'spring-commands-rspec'
|
gem 'spring-commands-rspec'
|
||||||
|
|
|
||||||
96
Gemfile.lock
96
Gemfile.lock
|
|
@ -86,8 +86,8 @@ GEM
|
||||||
awesome_nested_set (3.8.0)
|
awesome_nested_set (3.8.0)
|
||||||
activerecord (>= 4.0.0, < 8.1)
|
activerecord (>= 4.0.0, < 8.1)
|
||||||
aws-eventstream (1.3.0)
|
aws-eventstream (1.3.0)
|
||||||
aws-partitions (1.1038.0)
|
aws-partitions (1.1044.0)
|
||||||
aws-sdk-core (3.216.0)
|
aws-sdk-core (3.217.1)
|
||||||
aws-eventstream (~> 1, >= 1.3.0)
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
aws-partitions (~> 1, >= 1.992.0)
|
aws-partitions (~> 1, >= 1.992.0)
|
||||||
aws-sigv4 (~> 1.9)
|
aws-sigv4 (~> 1.9)
|
||||||
|
|
@ -95,7 +95,7 @@ GEM
|
||||||
aws-sdk-kms (1.97.0)
|
aws-sdk-kms (1.97.0)
|
||||||
aws-sdk-core (~> 3, >= 3.216.0)
|
aws-sdk-core (~> 3, >= 3.216.0)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sdk-s3 (1.178.0)
|
aws-sdk-s3 (1.179.0)
|
||||||
aws-sdk-core (~> 3, >= 3.216.0)
|
aws-sdk-core (~> 3, >= 3.216.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
|
|
@ -135,14 +135,13 @@ GEM
|
||||||
dotenv (= 3.1.7)
|
dotenv (= 3.1.7)
|
||||||
railties (>= 6.1)
|
railties (>= 6.1)
|
||||||
drb (2.2.1)
|
drb (2.2.1)
|
||||||
e2mmap (0.1.0)
|
|
||||||
email_validator (2.2.4)
|
email_validator (2.2.4)
|
||||||
activemodel
|
activemodel
|
||||||
erubi (1.13.1)
|
erubi (1.13.1)
|
||||||
et-orbi (1.2.11)
|
et-orbi (1.2.11)
|
||||||
tzinfo
|
tzinfo
|
||||||
factory_bot (6.5.0)
|
factory_bot (6.5.1)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 6.1.0)
|
||||||
factory_bot_rails (6.4.4)
|
factory_bot_rails (6.4.4)
|
||||||
factory_bot (~> 6.5)
|
factory_bot (~> 6.5)
|
||||||
railties (>= 5.0.0)
|
railties (>= 5.0.0)
|
||||||
|
|
@ -166,14 +165,30 @@ GEM
|
||||||
gemoji (>= 2.1.0)
|
gemoji (>= 2.1.0)
|
||||||
globalid (1.2.1)
|
globalid (1.2.1)
|
||||||
activesupport (>= 6.1)
|
activesupport (>= 6.1)
|
||||||
|
google-protobuf (4.29.3)
|
||||||
|
bigdecimal
|
||||||
|
rake (>= 13)
|
||||||
|
google-protobuf (4.29.3-aarch64-linux)
|
||||||
|
bigdecimal
|
||||||
|
rake (>= 13)
|
||||||
|
google-protobuf (4.29.3-arm64-darwin)
|
||||||
|
bigdecimal
|
||||||
|
rake (>= 13)
|
||||||
|
google-protobuf (4.29.3-x86_64-darwin)
|
||||||
|
bigdecimal
|
||||||
|
rake (>= 13)
|
||||||
|
google-protobuf (4.29.3-x86_64-linux)
|
||||||
|
bigdecimal
|
||||||
|
rake (>= 13)
|
||||||
httparty (0.22.0)
|
httparty (0.22.0)
|
||||||
csv
|
csv
|
||||||
mini_mime (>= 1.0.0)
|
mini_mime (>= 1.0.0)
|
||||||
multi_xml (>= 0.5.2)
|
multi_xml (>= 0.5.2)
|
||||||
i18n (1.14.6)
|
i18n (1.14.7)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
io-console (0.8.0)
|
io-console (0.8.0)
|
||||||
irb (1.14.3)
|
irb (1.15.1)
|
||||||
|
pp (>= 0.6.0)
|
||||||
rdoc (>= 4.0.0)
|
rdoc (>= 4.0.0)
|
||||||
reline (>= 0.4.2)
|
reline (>= 0.4.2)
|
||||||
jaro_winkler (1.6.0)
|
jaro_winkler (1.6.0)
|
||||||
|
|
@ -183,7 +198,7 @@ GEM
|
||||||
rexml (>= 3.3.9)
|
rexml (>= 3.3.9)
|
||||||
kramdown-parser-gfm (1.1.0)
|
kramdown-parser-gfm (1.1.0)
|
||||||
kramdown (~> 2.0)
|
kramdown (~> 2.0)
|
||||||
language_server-protocol (3.17.0.3)
|
language_server-protocol (3.17.0.4)
|
||||||
listen (3.9.0)
|
listen (3.9.0)
|
||||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||||
rb-inotify (~> 0.9, >= 0.9.10)
|
rb-inotify (~> 0.9, >= 0.9.10)
|
||||||
|
|
@ -213,46 +228,54 @@ GEM
|
||||||
timeout
|
timeout
|
||||||
net-smtp (0.5.0)
|
net-smtp (0.5.0)
|
||||||
net-protocol
|
net-protocol
|
||||||
|
newrelic_rpm (9.17.0)
|
||||||
nio4r (2.7.4)
|
nio4r (2.7.4)
|
||||||
nokogiri (1.18.1-aarch64-linux-gnu)
|
nokogiri (1.18.2-aarch64-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.1-aarch64-linux-musl)
|
nokogiri (1.18.2-aarch64-linux-musl)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.1-arm-linux-gnu)
|
nokogiri (1.18.2-arm-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.1-arm-linux-musl)
|
nokogiri (1.18.2-arm-linux-musl)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.1-arm64-darwin)
|
nokogiri (1.18.2-arm64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.1-x86_64-darwin)
|
nokogiri (1.18.2-x86_64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.1-x86_64-linux-gnu)
|
nokogiri (1.18.2-x86_64-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.1-x86_64-linux-musl)
|
nokogiri (1.18.2-x86_64-linux-musl)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
|
observer (0.1.2)
|
||||||
oj (3.16.9)
|
oj (3.16.9)
|
||||||
bigdecimal (>= 3.0)
|
bigdecimal (>= 3.0)
|
||||||
ostruct (>= 0.2)
|
ostruct (>= 0.2)
|
||||||
ostruct (0.6.1)
|
ostruct (0.6.1)
|
||||||
parallel (1.26.3)
|
parallel (1.26.3)
|
||||||
parser (3.3.6.0)
|
parser (3.3.7.0)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
racc
|
racc
|
||||||
pg (1.5.9)
|
pg (1.5.9)
|
||||||
|
pg_query (6.0.0)
|
||||||
|
google-protobuf (>= 3.25.3)
|
||||||
pg_search (2.3.7)
|
pg_search (2.3.7)
|
||||||
activerecord (>= 6.1)
|
activerecord (>= 6.1)
|
||||||
activesupport (>= 6.1)
|
activesupport (>= 6.1)
|
||||||
|
pp (0.6.2)
|
||||||
|
prettyprint
|
||||||
|
prettyprint (0.2.0)
|
||||||
|
prosopite (1.4.2)
|
||||||
pry (0.15.2)
|
pry (0.15.2)
|
||||||
coderay (~> 1.1)
|
coderay (~> 1.1)
|
||||||
method_source (~> 1.0)
|
method_source (~> 1.0)
|
||||||
psych (5.2.2)
|
psych (5.2.3)
|
||||||
date
|
date
|
||||||
stringio
|
stringio
|
||||||
puma (6.5.0)
|
puma (6.6.0)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
raabro (1.4.0)
|
raabro (1.4.0)
|
||||||
racc (1.8.1)
|
racc (1.8.1)
|
||||||
rack (3.1.8)
|
rack (3.1.9)
|
||||||
rack-cors (2.0.2)
|
rack-cors (2.0.2)
|
||||||
rack (>= 2.0.0)
|
rack (>= 2.0.0)
|
||||||
rack-session (2.1.0)
|
rack-session (2.1.0)
|
||||||
|
|
@ -296,8 +319,9 @@ GEM
|
||||||
rb-fsevent (0.11.2)
|
rb-fsevent (0.11.2)
|
||||||
rb-inotify (0.11.1)
|
rb-inotify (0.11.1)
|
||||||
ffi (~> 1.0)
|
ffi (~> 1.0)
|
||||||
rbs (2.8.4)
|
rbs (3.8.1)
|
||||||
rdoc (6.10.0)
|
logger
|
||||||
|
rdoc (6.11.0)
|
||||||
psych (>= 4.0.0)
|
psych (>= 4.0.0)
|
||||||
redis (5.3.0)
|
redis (5.3.0)
|
||||||
redis-client (>= 0.22.0)
|
redis-client (>= 0.22.0)
|
||||||
|
|
@ -309,7 +333,7 @@ GEM
|
||||||
responders (3.1.1)
|
responders (3.1.1)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 5.2)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
reverse_markdown (2.1.1)
|
reverse_markdown (3.0.0)
|
||||||
nokogiri
|
nokogiri
|
||||||
rexml (3.4.0)
|
rexml (3.4.0)
|
||||||
rspec (3.13.0)
|
rspec (3.13.0)
|
||||||
|
|
@ -335,17 +359,17 @@ GEM
|
||||||
rspec-support (3.13.2)
|
rspec-support (3.13.2)
|
||||||
rspec_junit_formatter (0.6.0)
|
rspec_junit_formatter (0.6.0)
|
||||||
rspec-core (>= 2, < 4, != 2.12.0)
|
rspec-core (>= 2, < 4, != 2.12.0)
|
||||||
rubocop (1.70.0)
|
rubocop (1.71.1)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (>= 3.17.0)
|
language_server-protocol (>= 3.17.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
parser (>= 3.3.0.2)
|
parser (>= 3.3.0.2)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
regexp_parser (>= 2.9.3, < 3.0)
|
regexp_parser (>= 2.9.3, < 3.0)
|
||||||
rubocop-ast (>= 1.36.2, < 2.0)
|
rubocop-ast (>= 1.38.0, < 2.0)
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 2.4.0, < 4.0)
|
unicode-display_width (>= 2.4.0, < 4.0)
|
||||||
rubocop-ast (1.37.0)
|
rubocop-ast (1.38.0)
|
||||||
parser (>= 3.3.1.0)
|
parser (>= 3.3.1.0)
|
||||||
ruby-progressbar (1.13.0)
|
ruby-progressbar (1.13.0)
|
||||||
rufus-scheduler (3.9.2)
|
rufus-scheduler (3.9.2)
|
||||||
|
|
@ -355,7 +379,8 @@ GEM
|
||||||
securerandom (0.4.1)
|
securerandom (0.4.1)
|
||||||
shoulda-matchers (6.4.0)
|
shoulda-matchers (6.4.0)
|
||||||
activesupport (>= 5.2.0)
|
activesupport (>= 5.2.0)
|
||||||
sidekiq (7.3.7)
|
sidekiq (7.3.8)
|
||||||
|
base64
|
||||||
connection_pool (>= 2.3.0)
|
connection_pool (>= 2.3.0)
|
||||||
logger
|
logger
|
||||||
rack (>= 2.2.4)
|
rack (>= 2.2.4)
|
||||||
|
|
@ -366,18 +391,20 @@ GEM
|
||||||
simplecov_json_formatter (~> 0.1)
|
simplecov_json_formatter (~> 0.1)
|
||||||
simplecov-html (0.13.1)
|
simplecov-html (0.13.1)
|
||||||
simplecov_json_formatter (0.1.4)
|
simplecov_json_formatter (0.1.4)
|
||||||
solargraph (0.50.0)
|
solargraph (0.51.1)
|
||||||
backport (~> 1.2)
|
backport (~> 1.2)
|
||||||
benchmark
|
benchmark
|
||||||
bundler (~> 2.0)
|
bundler (~> 2.0)
|
||||||
diff-lcs (~> 1.4)
|
diff-lcs (~> 1.4)
|
||||||
e2mmap
|
jaro_winkler (~> 1.6)
|
||||||
jaro_winkler (~> 1.5)
|
|
||||||
kramdown (~> 2.3)
|
kramdown (~> 2.3)
|
||||||
kramdown-parser-gfm (~> 1.1)
|
kramdown-parser-gfm (~> 1.1)
|
||||||
|
logger (~> 1.6)
|
||||||
|
observer (~> 0.1)
|
||||||
|
ostruct (~> 0.6)
|
||||||
parser (~> 3.0)
|
parser (~> 3.0)
|
||||||
rbs (~> 2.0)
|
rbs (~> 3.0)
|
||||||
reverse_markdown (~> 2.0)
|
reverse_markdown (>= 2.0, < 4)
|
||||||
rubocop (~> 1.38)
|
rubocop (~> 1.38)
|
||||||
thor (~> 1.0)
|
thor (~> 1.0)
|
||||||
tilt (~> 2.0)
|
tilt (~> 2.0)
|
||||||
|
|
@ -446,9 +473,12 @@ DEPENDENCIES
|
||||||
httparty
|
httparty
|
||||||
listen
|
listen
|
||||||
mini_magick
|
mini_magick
|
||||||
|
newrelic_rpm
|
||||||
oj
|
oj
|
||||||
pg
|
pg
|
||||||
|
pg_query
|
||||||
pg_search
|
pg_search
|
||||||
|
prosopite
|
||||||
pry
|
pry
|
||||||
puma
|
puma
|
||||||
rack-cors
|
rack-cors
|
||||||
|
|
|
||||||
|
|
@ -3,72 +3,77 @@
|
||||||
module Api
|
module Api
|
||||||
module V1
|
module V1
|
||||||
class CharacterBlueprint < ApiBlueprint
|
class CharacterBlueprint < ApiBlueprint
|
||||||
field :name do |w|
|
field :name do |c|
|
||||||
{
|
{
|
||||||
en: w.name_en,
|
en: c.name_en,
|
||||||
ja: w.name_jp
|
ja: c.name_jp
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
fields :granblue_id, :character_id, :rarity,
|
fields :granblue_id, :character_id, :rarity,
|
||||||
:element, :gender, :special
|
:element, :gender, :special
|
||||||
|
|
||||||
field :uncap do |w|
|
field :uncap do |c|
|
||||||
{
|
{
|
||||||
flb: w.flb,
|
flb: c.flb,
|
||||||
ulb: w.ulb
|
ulb: c.ulb
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
field :hp do |w|
|
field :race do |c|
|
||||||
{
|
[c.race1, c.race2].compact
|
||||||
min_hp: w.min_hp,
|
|
||||||
max_hp: w.max_hp,
|
|
||||||
max_hp_flb: w.max_hp_flb
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
field :atk do |w|
|
field :proficiency do |c|
|
||||||
{
|
[c.proficiency1, c.proficiency2].compact
|
||||||
min_atk: w.min_atk,
|
|
||||||
max_atk: w.max_atk,
|
|
||||||
max_atk_flb: w.max_atk_flb
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
field :race do |w|
|
view :full do
|
||||||
[
|
include_view :stats
|
||||||
w.race1,
|
include_view :rates
|
||||||
w.race2
|
include_view :dates
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
field :proficiency do |w|
|
field :awakenings do
|
||||||
[
|
Character::AWAKENINGS.map do |awakening|
|
||||||
w.proficiency1,
|
AwakeningBlueprint.render_as_hash(OpenStruct.new(awakening))
|
||||||
w.proficiency2
|
end
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
field :data do |w|
|
|
||||||
{
|
|
||||||
base_da: w.base_da,
|
|
||||||
base_ta: w.base_ta
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
field :ougi_ratio do |w|
|
|
||||||
{
|
|
||||||
ougi_ratio: w.ougi_ratio,
|
|
||||||
ougi_ratio_flb: w.ougi_ratio_flb
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
field :awakenings do
|
|
||||||
Awakening.where(object_type: 'Character').map do |a|
|
|
||||||
AwakeningBlueprint.render_as_hash(a)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
view :stats do
|
||||||
|
field :hp do |c|
|
||||||
|
{
|
||||||
|
min_hp: c.min_hp,
|
||||||
|
max_hp: c.max_hp,
|
||||||
|
max_hp_flb: c.max_hp_flb
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
field :atk do |c|
|
||||||
|
{
|
||||||
|
min_atk: c.min_atk,
|
||||||
|
max_atk: c.max_atk,
|
||||||
|
max_atk_flb: c.max_atk_flb
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
view :rates do
|
||||||
|
fields :base_da, :base_ta
|
||||||
|
|
||||||
|
field :ougi_ratio do |c|
|
||||||
|
{
|
||||||
|
ougi_ratio: c.ougi_ratio,
|
||||||
|
ougi_ratio_flb: c.ougi_ratio_flb
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
view :dates do
|
||||||
|
field :release_date
|
||||||
|
field :flb_date
|
||||||
|
field :ulb_date
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -3,57 +3,64 @@
|
||||||
module Api
|
module Api
|
||||||
module V1
|
module V1
|
||||||
class GridCharacterBlueprint < ApiBlueprint
|
class GridCharacterBlueprint < ApiBlueprint
|
||||||
view :uncap do
|
fields :position, :uncap_level, :perpetuity
|
||||||
association :party, blueprint: PartyBlueprint, view: :minimal
|
|
||||||
fields :position, :uncap_level
|
field :transcendence_step, if: ->(_field, gc, _options) { gc.character&.ulb } do |gc|
|
||||||
|
gc.transcendence_step
|
||||||
end
|
end
|
||||||
|
|
||||||
view :nested do
|
view :preview do
|
||||||
fields :position, :uncap_level, :perpetuity
|
|
||||||
|
|
||||||
field :transcendence_step, if: lambda { |_fn, obj, _opt|
|
|
||||||
obj.character.ulb
|
|
||||||
} do |c|
|
|
||||||
c.transcendence_step
|
|
||||||
end
|
|
||||||
|
|
||||||
field :awakening do |c|
|
|
||||||
{
|
|
||||||
type: AwakeningBlueprint.render_as_hash(c.awakening),
|
|
||||||
level: c.awakening_level
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
field :over_mastery, if: lambda { |_fn, obj, _opt|
|
|
||||||
!obj.ring1['modifier'].nil? && !obj.ring2['modifier'].nil?
|
|
||||||
} do |c|
|
|
||||||
rings = []
|
|
||||||
|
|
||||||
rings.push(c.ring1) unless c.ring1['modifier'].nil?
|
|
||||||
rings.push(c.ring2) unless c.ring2['modifier'].nil?
|
|
||||||
rings.push(c.ring3) unless c.ring3['modifier'].nil?
|
|
||||||
rings.push(c.ring4) unless c.ring4['modifier'].nil?
|
|
||||||
|
|
||||||
rings
|
|
||||||
end
|
|
||||||
|
|
||||||
field :aetherial_mastery, if: lambda { |_fn, obj, _opt|
|
|
||||||
!obj.earring['modifier'].nil?
|
|
||||||
} do |c|
|
|
||||||
c.earring
|
|
||||||
end
|
|
||||||
|
|
||||||
association :character, name: :object, blueprint: CharacterBlueprint
|
association :character, name: :object, blueprint: CharacterBlueprint
|
||||||
end
|
end
|
||||||
|
|
||||||
view :full do
|
view :nested do
|
||||||
include_view :nested
|
include_view :mastery_bonuses
|
||||||
association :party, blueprint: PartyBlueprint, view: :minimal
|
association :character, name: :object, blueprint: CharacterBlueprint, view: :full
|
||||||
|
end
|
||||||
|
|
||||||
|
view :uncap do
|
||||||
|
association :party, blueprint: PartyBlueprint
|
||||||
|
fields :position, :uncap_level
|
||||||
end
|
end
|
||||||
|
|
||||||
view :destroyed do
|
view :destroyed do
|
||||||
fields :position, :created_at, :updated_at
|
fields :position, :created_at, :updated_at
|
||||||
end
|
end
|
||||||
|
|
||||||
|
view :mastery_bonuses do
|
||||||
|
field :awakening, if: ->(_field_name, gc, _options) { gc.association(:awakening).loaded? } do |gc|
|
||||||
|
{
|
||||||
|
type: AwakeningBlueprint.render_as_hash(gc.awakening),
|
||||||
|
level: gc.awakening_level.to_i
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
field :over_mastery, if: lambda { |_fn, obj, _opt|
|
||||||
|
obj.ring1.present? && obj.ring2.present? && !obj.ring1['modifier'].nil? && !obj.ring2['modifier'].nil?
|
||||||
|
} do |c|
|
||||||
|
mapped_rings = [c.ring1, c.ring2, c.ring3, c.ring4].each_with_object([]) do |ring, arr|
|
||||||
|
# Skip if the ring is nil or its modifier is blank.
|
||||||
|
next if ring.blank? || ring['modifier'].blank?
|
||||||
|
|
||||||
|
# Convert the string values to numbers.
|
||||||
|
mod = ring['modifier'].to_i
|
||||||
|
|
||||||
|
# Only include if modifier is non-zero.
|
||||||
|
next if mod.zero?
|
||||||
|
|
||||||
|
arr << { modifier: mod, strength: ring['strength'].to_i }
|
||||||
|
end
|
||||||
|
|
||||||
|
mapped_rings
|
||||||
|
end
|
||||||
|
|
||||||
|
field :aetherial_mastery, if: ->(_fn, obj, _opt) { obj.earring.present? && !obj.earring['modifier'].nil? } do |gc, _options|
|
||||||
|
{
|
||||||
|
modifier: gc.earring['modifier'].to_i,
|
||||||
|
strength: gc.earring['strength'].to_i
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -3,19 +3,24 @@
|
||||||
module Api
|
module Api
|
||||||
module V1
|
module V1
|
||||||
class GridSummonBlueprint < ApiBlueprint
|
class GridSummonBlueprint < ApiBlueprint
|
||||||
view :uncap do
|
fields :main, :friend, :position, :quick_summon, :uncap_level, :transcendence_step
|
||||||
association :party, blueprint: PartyBlueprint, view: :minimal
|
|
||||||
fields :position, :uncap_level, :transcendence_step
|
view :preview do
|
||||||
|
association :summon, name: :object, blueprint: SummonBlueprint
|
||||||
end
|
end
|
||||||
|
|
||||||
view :nested do
|
view :nested do
|
||||||
fields :main, :friend, :position, :quick_summon, :uncap_level, :transcendence_step
|
association :summon, name: :object, blueprint: SummonBlueprint, view: :full
|
||||||
association :summon, name: :object, blueprint: SummonBlueprint
|
|
||||||
end
|
end
|
||||||
|
|
||||||
view :full do
|
view :full do
|
||||||
include_view :nested
|
include_view :nested
|
||||||
association :party, blueprint: PartyBlueprint, view: :minimal
|
association :party, blueprint: PartyBlueprint
|
||||||
|
end
|
||||||
|
|
||||||
|
view :uncap do
|
||||||
|
association :party, blueprint: PartyBlueprint
|
||||||
|
fields :position, :uncap_level, :transcendence_step
|
||||||
end
|
end
|
||||||
|
|
||||||
view :destroyed do
|
view :destroyed do
|
||||||
|
|
|
||||||
|
|
@ -3,45 +3,47 @@
|
||||||
module Api
|
module Api
|
||||||
module V1
|
module V1
|
||||||
class GridWeaponBlueprint < ApiBlueprint
|
class GridWeaponBlueprint < ApiBlueprint
|
||||||
view :uncap do
|
fields :mainhand, :position, :uncap_level, :transcendence_step, :element
|
||||||
association :party, blueprint: PartyBlueprint, view: :minimal
|
|
||||||
fields :position, :uncap_level
|
view :preview do
|
||||||
|
association :weapon, name: :object, blueprint: WeaponBlueprint
|
||||||
end
|
end
|
||||||
|
|
||||||
view :nested do
|
view :nested do
|
||||||
fields :mainhand, :position, :uncap_level, :transcendence_step, :element
|
field :ax, if: ->(_field_name, w, _options) { w.weapon.present? && w.weapon.ax } do |w|
|
||||||
association :weapon, name: :object, blueprint: WeaponBlueprint
|
[
|
||||||
|
{ modifier: w.ax_modifier1, strength: w.ax_strength1 },
|
||||||
|
{ modifier: w.ax_modifier2, strength: w.ax_strength2 }
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
field :awakening, if: ->(_field_name, w, _options) { w.awakening.present? } do |w|
|
||||||
|
{
|
||||||
|
type: AwakeningBlueprint.render_as_hash(w.awakening),
|
||||||
|
level: w.awakening_level
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
association :weapon, name: :object, blueprint: WeaponBlueprint, view: :full,
|
||||||
|
if: ->(_field_name, w, _options) { w.weapon.present? }
|
||||||
|
|
||||||
association :weapon_keys,
|
association :weapon_keys,
|
||||||
blueprint: WeaponKeyBlueprint,
|
blueprint: WeaponKeyBlueprint,
|
||||||
if: lambda { |_field_name, w, _options|
|
if: ->(_field_name, w, _options) {
|
||||||
[2, 3, 17, 24, 34].include?(w.weapon.series)
|
w.weapon.present? &&
|
||||||
|
w.weapon.series.present? &&
|
||||||
|
[2, 3, 17, 24, 34].include?(w.weapon.series)
|
||||||
}
|
}
|
||||||
|
|
||||||
field :ax, if: ->(_field_name, w, _options) { w.weapon.ax } do |w|
|
|
||||||
[
|
|
||||||
{
|
|
||||||
modifier: w.ax_modifier1,
|
|
||||||
strength: w.ax_strength1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
modifier: w.ax_modifier2,
|
|
||||||
strength: w.ax_strength2
|
|
||||||
}
|
|
||||||
]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
field :awakening, if: ->(_field_name, w, _options) { w.awakening_id } do |w|
|
|
||||||
{
|
|
||||||
type: AwakeningBlueprint.render_as_hash(w.awakening),
|
|
||||||
level: w.awakening_level
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
view :full do
|
view :full do
|
||||||
include_view :nested
|
include_view :nested
|
||||||
association :party, blueprint: PartyBlueprint, view: :minimal
|
association :party, blueprint: PartyBlueprint
|
||||||
|
end
|
||||||
|
|
||||||
|
view :uncap do
|
||||||
|
association :party, blueprint: PartyBlueprint
|
||||||
|
fields :position, :uncap_level
|
||||||
end
|
end
|
||||||
|
|
||||||
view :destroyed do
|
view :destroyed do
|
||||||
|
|
|
||||||
|
|
@ -3,105 +3,131 @@
|
||||||
module Api
|
module Api
|
||||||
module V1
|
module V1
|
||||||
class PartyBlueprint < ApiBlueprint
|
class PartyBlueprint < ApiBlueprint
|
||||||
view :weapons do
|
# Base fields that are always needed
|
||||||
|
fields :local_id, :description, :shortcode, :visibility,
|
||||||
|
:name, :element, :extra, :charge_attack,
|
||||||
|
:button_count, :turn_count, :chain_count, :clear_time,
|
||||||
|
:full_auto, :auto_guard, :auto_summon,
|
||||||
|
:created_at, :updated_at
|
||||||
|
|
||||||
|
fields :local_id, :description, :charge_attack,
|
||||||
|
:button_count, :turn_count, :chain_count,
|
||||||
|
:master_level, :ultimate_mastery
|
||||||
|
|
||||||
|
# Party associations
|
||||||
|
association :user,
|
||||||
|
blueprint: UserBlueprint,
|
||||||
|
view: :minimal
|
||||||
|
|
||||||
|
association :job,
|
||||||
|
blueprint: JobBlueprint
|
||||||
|
|
||||||
|
association :raid,
|
||||||
|
blueprint: RaidBlueprint,
|
||||||
|
view: :nested
|
||||||
|
|
||||||
|
# Metadata associations
|
||||||
|
field :favorited do |party, options|
|
||||||
|
party.is_favorited(options[:current_user])
|
||||||
|
end
|
||||||
|
|
||||||
|
# For collection views
|
||||||
|
view :preview do
|
||||||
|
include_view :preview_objects # Characters, Weapons, Summons
|
||||||
|
include_view :preview_metadata # Object counts
|
||||||
|
end
|
||||||
|
|
||||||
|
# For object views
|
||||||
|
view :full do
|
||||||
|
# Primary object associations
|
||||||
|
include_view :nested_objects # Characters, Weapons, Summons
|
||||||
|
include_view :nested_metadata # Remixes, Source party
|
||||||
|
include_view :job_metadata # Accessory, Skills, Guidebooks
|
||||||
|
end
|
||||||
|
|
||||||
|
# Primary object associations
|
||||||
|
view :preview_objects do
|
||||||
|
association :characters,
|
||||||
|
blueprint: GridCharacterBlueprint,
|
||||||
|
view: :preview
|
||||||
|
|
||||||
|
association :weapons,
|
||||||
|
blueprint: GridWeaponBlueprint,
|
||||||
|
view: :preview
|
||||||
|
|
||||||
|
association :summons,
|
||||||
|
blueprint: GridSummonBlueprint,
|
||||||
|
view: :preview
|
||||||
|
end
|
||||||
|
|
||||||
|
view :nested_objects do
|
||||||
|
association :characters,
|
||||||
|
blueprint: GridCharacterBlueprint,
|
||||||
|
view: :nested
|
||||||
|
|
||||||
association :weapons,
|
association :weapons,
|
||||||
blueprint: GridWeaponBlueprint,
|
blueprint: GridWeaponBlueprint,
|
||||||
view: :nested
|
view: :nested
|
||||||
end
|
|
||||||
|
|
||||||
view :summons do
|
|
||||||
association :summons,
|
association :summons,
|
||||||
blueprint: GridSummonBlueprint,
|
blueprint: GridSummonBlueprint,
|
||||||
view: :nested
|
view: :nested
|
||||||
end
|
end
|
||||||
|
|
||||||
view :characters do
|
# Metadata views
|
||||||
association :characters,
|
view :preview_metadata do
|
||||||
blueprint: GridCharacterBlueprint,
|
field :counts do |party|
|
||||||
view: :nested
|
|
||||||
end
|
|
||||||
|
|
||||||
view :job_skills do
|
|
||||||
field :job_skills do |job|
|
|
||||||
{
|
{
|
||||||
'0' => !job.skill0.nil? ? JobSkillBlueprint.render_as_hash(job.skill0) : nil,
|
weapons: party.weapons_count,
|
||||||
'1' => !job.skill1.nil? ? JobSkillBlueprint.render_as_hash(job.skill1) : nil,
|
characters: party.characters_count,
|
||||||
'2' => !job.skill2.nil? ? JobSkillBlueprint.render_as_hash(job.skill2) : nil,
|
summons: party.summons_count
|
||||||
'3' => !job.skill3.nil? ? JobSkillBlueprint.render_as_hash(job.skill3) : nil
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
view :minimal do
|
view :nested_metadata do
|
||||||
fields :name, :element, :shortcode, :favorited, :remix,
|
|
||||||
:extra, :full_auto, :clear_time, :auto_guard, :auto_summon,
|
|
||||||
:visibility, :created_at, :updated_at
|
|
||||||
|
|
||||||
field :guidebooks do |p|
|
|
||||||
{
|
|
||||||
'1' => !p.guidebook1.nil? ? GuidebookBlueprint.render_as_hash(p.guidebook1) : nil,
|
|
||||||
'2' => !p.guidebook2.nil? ? GuidebookBlueprint.render_as_hash(p.guidebook2) : nil,
|
|
||||||
'3' => !p.guidebook3.nil? ? GuidebookBlueprint.render_as_hash(p.guidebook3) : nil
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
association :raid,
|
|
||||||
blueprint: RaidBlueprint,
|
|
||||||
view: :full
|
|
||||||
|
|
||||||
association :job,
|
|
||||||
blueprint: JobBlueprint
|
|
||||||
|
|
||||||
association :user,
|
|
||||||
blueprint: UserBlueprint,
|
|
||||||
view: :minimal
|
|
||||||
end
|
|
||||||
|
|
||||||
view :jobs do
|
|
||||||
association :job,
|
|
||||||
blueprint: JobBlueprint
|
|
||||||
include_view :job_skills
|
|
||||||
end
|
|
||||||
|
|
||||||
view :preview do
|
|
||||||
include_view :minimal
|
|
||||||
include_view :characters
|
|
||||||
include_view :weapons
|
|
||||||
include_view :summons
|
|
||||||
end
|
|
||||||
|
|
||||||
view :full do
|
|
||||||
include_view :preview
|
|
||||||
include_view :summons
|
|
||||||
include_view :characters
|
|
||||||
include_view :job_skills
|
|
||||||
|
|
||||||
fields :local_id, :description, :charge_attack,
|
|
||||||
:button_count, :turn_count, :chain_count,
|
|
||||||
:master_level, :ultimate_mastery
|
|
||||||
|
|
||||||
association :accessory,
|
|
||||||
blueprint: JobAccessoryBlueprint
|
|
||||||
|
|
||||||
association :source_party,
|
association :source_party,
|
||||||
blueprint: PartyBlueprint,
|
blueprint: PartyBlueprint,
|
||||||
view: :minimal
|
view: :minimal,
|
||||||
|
if: ->(_field_name, party, _options) { party.source_party_id.present? }
|
||||||
|
|
||||||
# TODO: This should probably be paginated
|
# Re-added remixes association
|
||||||
association :remixes,
|
association :remixes,
|
||||||
blueprint: PartyBlueprint,
|
blueprint: PartyBlueprint,
|
||||||
view: :collection
|
view: :preview
|
||||||
end
|
end
|
||||||
|
|
||||||
view :collection do
|
# Job-related views
|
||||||
include_view :preview
|
view :job_metadata do
|
||||||
|
field :job_skills, cache: true do |party|
|
||||||
|
{
|
||||||
|
'0' => party.skill0 ? JobSkillBlueprint.render_as_hash(party.skill0) : nil,
|
||||||
|
'1' => party.skill1 ? JobSkillBlueprint.render_as_hash(party.skill1) : nil,
|
||||||
|
'2' => party.skill2 ? JobSkillBlueprint.render_as_hash(party.skill2) : nil,
|
||||||
|
'3' => party.skill3 ? JobSkillBlueprint.render_as_hash(party.skill3) : nil
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
field :guidebooks, cache: true do |party|
|
||||||
|
{
|
||||||
|
'1' => party.guidebook1 ? GuidebookBlueprint.render_as_hash(party.guidebook1) : nil,
|
||||||
|
'2' => party.guidebook2 ? GuidebookBlueprint.render_as_hash(party.guidebook2) : nil,
|
||||||
|
'3' => party.guidebook3 ? GuidebookBlueprint.render_as_hash(party.guidebook3) : nil
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
association :accessory,
|
||||||
|
blueprint: JobAccessoryBlueprint,
|
||||||
|
if: ->(_field_name, party, _options) { party.accessory_id.present? }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Created view
|
||||||
view :created do
|
view :created do
|
||||||
include_view :full
|
include_view :full
|
||||||
fields :edit_key
|
fields :edit_key
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Destroyed view
|
||||||
view :destroyed do
|
view :destroyed do
|
||||||
fields :name, :description, :created_at, :updated_at
|
fields :name, :description, :created_at, :updated_at
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ module Api
|
||||||
end
|
end
|
||||||
|
|
||||||
fields :slug, :level, :element
|
fields :slug, :level, :element
|
||||||
|
|
||||||
|
association :group, blueprint: RaidGroupBlueprint, view: :flat
|
||||||
end
|
end
|
||||||
|
|
||||||
view :full do
|
view :full do
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ module Api
|
||||||
|
|
||||||
view :full do
|
view :full do
|
||||||
include_view :flat
|
include_view :flat
|
||||||
association :raids, blueprint: RaidBlueprint, view: :full
|
association :raids, blueprint: RaidBlueprint, view: :nested
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -3,41 +3,55 @@
|
||||||
module Api
|
module Api
|
||||||
module V1
|
module V1
|
||||||
class SummonBlueprint < ApiBlueprint
|
class SummonBlueprint < ApiBlueprint
|
||||||
field :name do |w|
|
field :name do |s|
|
||||||
{
|
{
|
||||||
en: w.name_en,
|
en: s.name_en,
|
||||||
ja: w.name_jp
|
ja: s.name_jp
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
fields :granblue_id, :element, :rarity, :max_level
|
fields :granblue_id, :element, :rarity, :max_level
|
||||||
|
|
||||||
field :uncap do |w|
|
field :uncap do |s|
|
||||||
{
|
{
|
||||||
flb: w.flb,
|
flb: s.flb,
|
||||||
ulb: w.ulb,
|
ulb: s.ulb,
|
||||||
transcendence: w.transcendence
|
transcendence: s.transcendence
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
field :hp do |w|
|
view :stats do
|
||||||
{
|
field :hp do |s|
|
||||||
min_hp: w.min_hp,
|
{
|
||||||
max_hp: w.max_hp,
|
min_hp: s.min_hp,
|
||||||
max_hp_flb: w.max_hp_flb,
|
max_hp: s.max_hp,
|
||||||
max_hp_ulb: w.max_hp_ulb,
|
max_hp_flb: s.max_hp_flb,
|
||||||
max_hp_xlb: w.max_hp_xlb
|
max_hp_ulb: s.max_hp_ulb,
|
||||||
}
|
max_hp_xlb: s.max_hp_xlb
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
field :atk do |s|
|
||||||
|
{
|
||||||
|
min_atk: s.min_atk,
|
||||||
|
max_atk: s.max_atk,
|
||||||
|
max_atk_flb: s.max_atk_flb,
|
||||||
|
max_atk_ulb: s.max_atk_ulb,
|
||||||
|
max_atk_xlb: s.max_atk_xlb
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
field :atk do |w|
|
view :dates do
|
||||||
{
|
field :release_date
|
||||||
min_atk: w.min_atk,
|
field :flb_date
|
||||||
max_atk: w.max_atk,
|
field :ulb_date
|
||||||
max_atk_flb: w.max_atk_flb,
|
field :transcendence_date
|
||||||
max_atk_ulb: w.max_atk_ulb,
|
end
|
||||||
max_atk_xlb: w.max_atk_xlb
|
|
||||||
}
|
view :full do
|
||||||
|
include_view :stats
|
||||||
|
include_view :dates
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,12 @@ module Api
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Primary information
|
||||||
fields :granblue_id, :element, :proficiency,
|
fields :granblue_id, :element, :proficiency,
|
||||||
:max_level, :max_skill_level, :max_awakening_level, :limit, :rarity,
|
:max_level, :max_skill_level, :max_awakening_level, :limit, :rarity,
|
||||||
:series, :ax, :ax_type
|
:series, :ax, :ax_type
|
||||||
|
|
||||||
|
# Uncap information
|
||||||
field :uncap do |w|
|
field :uncap do |w|
|
||||||
{
|
{
|
||||||
flb: w.flb,
|
flb: w.flb,
|
||||||
|
|
@ -22,28 +24,39 @@ module Api
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
field :hp do |w|
|
view :stats do
|
||||||
{
|
field :hp do |w|
|
||||||
min_hp: w.min_hp,
|
{
|
||||||
max_hp: w.max_hp,
|
min_hp: w.min_hp,
|
||||||
max_hp_flb: w.max_hp_flb,
|
max_hp: w.max_hp,
|
||||||
max_hp_ulb: w.max_hp_ulb
|
max_hp_flb: w.max_hp_flb,
|
||||||
}
|
max_hp_ulb: w.max_hp_ulb
|
||||||
end
|
}
|
||||||
|
|
||||||
field :atk do |w|
|
|
||||||
{
|
|
||||||
min_atk: w.min_atk,
|
|
||||||
max_atk: w.max_atk,
|
|
||||||
max_atk_flb: w.max_atk_flb,
|
|
||||||
max_atk_ulb: w.max_atk_ulb
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
field :awakenings, if: ->(_fn, obj, _opt) { obj.awakenings.length.positive? } do |w|
|
|
||||||
w.awakenings.map do |a|
|
|
||||||
AwakeningBlueprint.render_as_hash(a)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
field :atk do |w|
|
||||||
|
{
|
||||||
|
min_atk: w.min_atk,
|
||||||
|
max_atk: w.max_atk,
|
||||||
|
max_atk_flb: w.max_atk_flb,
|
||||||
|
max_atk_ulb: w.max_atk_ulb
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
view :dates do
|
||||||
|
field :release_date
|
||||||
|
field :flb_date
|
||||||
|
field :ulb_date
|
||||||
|
field :transcendence_date
|
||||||
|
end
|
||||||
|
|
||||||
|
view :full do
|
||||||
|
include_view :stats
|
||||||
|
include_view :dates
|
||||||
|
association :awakenings,
|
||||||
|
blueprint: AwakeningBlueprint,
|
||||||
|
if: ->(_field_name, weapon, _options) { weapon.awakenings.any? }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ module Api
|
||||||
##### Hooks
|
##### Hooks
|
||||||
before_action :current_user
|
before_action :current_user
|
||||||
before_action :default_content_type
|
before_action :default_content_type
|
||||||
|
around_action :n_plus_one_detection, unless: -> { Rails.env.production? }
|
||||||
|
|
||||||
##### Responders
|
##### Responders
|
||||||
respond_to :json
|
respond_to :json
|
||||||
|
|
@ -104,9 +105,9 @@ module Api
|
||||||
|
|
||||||
def render_not_found_response(object)
|
def render_not_found_response(object)
|
||||||
render json: ErrorBlueprint.render(nil, error: {
|
render json: ErrorBlueprint.render(nil, error: {
|
||||||
message: "#{object.capitalize} could not be found",
|
message: "#{object.capitalize} could not be found",
|
||||||
code: 'not_found'
|
code: 'not_found'
|
||||||
}), status: :not_found
|
}), status: :not_found
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_unauthorized_response
|
def render_unauthorized_response
|
||||||
|
|
@ -119,6 +120,13 @@ module Api
|
||||||
def restrict_access
|
def restrict_access
|
||||||
raise UnauthorizedError unless current_user
|
raise UnauthorizedError unless current_user
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def n_plus_one_detection
|
||||||
|
Prosopite.scan
|
||||||
|
yield
|
||||||
|
ensure
|
||||||
|
Prosopite.finish
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -39,17 +39,22 @@ module Api
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
mastery = {}
|
permitted = character_params.to_h.deep_symbolize_keys
|
||||||
%i[ring1 ring2 ring3 ring4 earring awakening].each do |key|
|
puts "Permitted:"
|
||||||
value = character_params.to_h[key]
|
ap permitted
|
||||||
mastery[key] = value unless value.nil?
|
|
||||||
|
# For the new nested structure, assign them to the virtual attributes:
|
||||||
|
@character.new_rings = permitted[:rings] if permitted[:rings].present?
|
||||||
|
@character.new_awakening = permitted[:awakening] if permitted[:awakening].present?
|
||||||
|
|
||||||
|
# For the rest of the attributes, you can assign them normally.
|
||||||
|
@character.assign_attributes(permitted.except(:rings, :awakening))
|
||||||
|
|
||||||
|
if @character.save
|
||||||
|
render json: GridCharacterBlueprint.render(@character, view: :nested)
|
||||||
|
else
|
||||||
|
render_validation_error_response(@character)
|
||||||
end
|
end
|
||||||
|
|
||||||
@character.attributes = character_params.merge(mastery)
|
|
||||||
|
|
||||||
return render json: GridCharacterBlueprint.render(@character, view: :full) if @character.save
|
|
||||||
|
|
||||||
render_validation_error_response(@character)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def resolve
|
def resolve
|
||||||
|
|
@ -123,7 +128,7 @@ module Api
|
||||||
end
|
end
|
||||||
|
|
||||||
def set
|
def set
|
||||||
@character = GridCharacter.find(params[:id])
|
@character = GridCharacter.includes(:awakening).find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_incoming_character
|
def find_incoming_character
|
||||||
|
|
@ -143,14 +148,59 @@ module Api
|
||||||
render_unauthorized_response if unauthorized_create || unauthorized_update
|
render_unauthorized_response if unauthorized_create || unauthorized_update
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def transform_character_params(raw_params)
|
||||||
|
# Convert to a symbolized hash for convenience.
|
||||||
|
raw = raw_params.deep_symbolize_keys
|
||||||
|
|
||||||
|
# Only update keys that were provided.
|
||||||
|
transformed = raw.slice(:uncap_level, :transcendence_step, :perpetuity)
|
||||||
|
transformed[:uncap_level] = raw[:uncap_level].to_i if raw[:uncap_level].present?
|
||||||
|
transformed[:transcendence_step] = raw[:transcendence_step].to_i if raw[:transcendence_step].present?
|
||||||
|
|
||||||
|
# Process rings if provided.
|
||||||
|
transformed.merge!(transform_rings(raw[:rings])) if raw[:rings].present?
|
||||||
|
|
||||||
|
# Process earring if provided.
|
||||||
|
transformed[:earring] = raw[:earring] if raw[:earring].present?
|
||||||
|
|
||||||
|
# Process awakening if provided.
|
||||||
|
if raw[:awakening].present?
|
||||||
|
transformed[:awakening_id] = raw[:awakening][:id]
|
||||||
|
# Default to 1 if level is missing (to satisfy validations)
|
||||||
|
transformed[:awakening_level] = raw[:awakening][:level].present? ? raw[:awakening][:level].to_i : 1
|
||||||
|
end
|
||||||
|
|
||||||
|
transformed
|
||||||
|
end
|
||||||
|
|
||||||
|
def transform_rings(rings)
|
||||||
|
default_ring = { modifier: nil, strength: nil }
|
||||||
|
# Ensure rings is an array of hashes.
|
||||||
|
rings_array = Array(rings).map(&:to_h)
|
||||||
|
# Pad the array to exactly four rings if needed.
|
||||||
|
rings_array.fill(default_ring, rings_array.size...4)
|
||||||
|
{
|
||||||
|
ring1: rings_array[0],
|
||||||
|
ring2: rings_array[1],
|
||||||
|
ring3: rings_array[2],
|
||||||
|
ring4: rings_array[3]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
# Specify whitelisted properties that can be modified.
|
# Specify whitelisted properties that can be modified.
|
||||||
def character_params
|
def character_params
|
||||||
params.require(:character).permit(:id, :party_id, :character_id, :position,
|
params.require(:character).permit(
|
||||||
:uncap_level, :transcendence_step, :perpetuity,
|
:id,
|
||||||
:awakening_id, :awakening_level,
|
:party_id,
|
||||||
ring1: %i[modifier strength], ring2: %i[modifier strength],
|
:character_id,
|
||||||
ring3: %i[modifier strength], ring4: %i[modifier strength],
|
:position,
|
||||||
earring: %i[modifier strength])
|
:uncap_level,
|
||||||
|
:transcendence_step,
|
||||||
|
:perpetuity,
|
||||||
|
awakening: %i[id level],
|
||||||
|
rings: %i[modifier strength],
|
||||||
|
earring: %i[modifier strength]
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def resolve_params
|
def resolve_params
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,13 @@ module Api
|
||||||
module V1
|
module V1
|
||||||
class JobSkillsController < Api::V1::ApiController
|
class JobSkillsController < Api::V1::ApiController
|
||||||
def all
|
def all
|
||||||
render json: JobSkillBlueprint.render(JobSkill.all)
|
render json: JobSkillBlueprint.render(JobSkill.includes(:job).all)
|
||||||
end
|
end
|
||||||
|
|
||||||
def job
|
def job
|
||||||
@skills = JobSkill.where('job_id != ? AND emp = ?', params[:id], true)
|
@skills = JobSkill.includes(:job)
|
||||||
|
.where.not(job_id: params[:id])
|
||||||
|
.where(emp: true)
|
||||||
render json: JobSkillBlueprint.render(@skills)
|
render json: JobSkillBlueprint.render(@skills)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -2,40 +2,65 @@
|
||||||
|
|
||||||
module Api
|
module Api
|
||||||
module V1
|
module V1
|
||||||
|
# Controller for managing party-related operations in the API
|
||||||
|
# @api public
|
||||||
class PartiesController < Api::V1::ApiController
|
class PartiesController < Api::V1::ApiController
|
||||||
before_action :set_from_slug,
|
before_action :set_from_slug,
|
||||||
except: %w[create destroy update index favorites]
|
except: %w[create destroy update index favorites]
|
||||||
before_action :set, only: %w[update destroy]
|
before_action :set, only: %w[update destroy]
|
||||||
before_action :authorize, only: %w[update destroy]
|
before_action :authorize, only: %w[update destroy]
|
||||||
|
|
||||||
|
# == Constants
|
||||||
|
|
||||||
|
# Maximum number of characters allowed in a party
|
||||||
MAX_CHARACTERS = 5
|
MAX_CHARACTERS = 5
|
||||||
|
|
||||||
|
# Maximum number of summons allowed in a party
|
||||||
MAX_SUMMONS = 8
|
MAX_SUMMONS = 8
|
||||||
|
|
||||||
|
# Maximum number of weapons allowed in a party
|
||||||
MAX_WEAPONS = 13
|
MAX_WEAPONS = 13
|
||||||
|
|
||||||
|
# Default minimum number of characters required for filtering
|
||||||
DEFAULT_MIN_CHARACTERS = 3
|
DEFAULT_MIN_CHARACTERS = 3
|
||||||
|
|
||||||
|
# Default minimum number of summons required for filtering
|
||||||
DEFAULT_MIN_SUMMONS = 2
|
DEFAULT_MIN_SUMMONS = 2
|
||||||
|
|
||||||
|
# Default minimum number of weapons required for filtering
|
||||||
DEFAULT_MIN_WEAPONS = 5
|
DEFAULT_MIN_WEAPONS = 5
|
||||||
|
|
||||||
|
# Default maximum clear time in seconds
|
||||||
DEFAULT_MAX_CLEAR_TIME = 5400
|
DEFAULT_MAX_CLEAR_TIME = 5400
|
||||||
|
|
||||||
|
# == Primary CRUD Actions
|
||||||
|
|
||||||
|
# Creates a new party with optional user association
|
||||||
|
# @return [void]
|
||||||
def create
|
def create
|
||||||
party = Party.new
|
# Build the party with the provided parameters and assign the user
|
||||||
|
party = Party.new(party_params)
|
||||||
party.user = current_user if current_user
|
party.user = current_user if current_user
|
||||||
party.attributes = party_params if party_params
|
|
||||||
|
|
||||||
if party_params && party_params[:raid_id]
|
# If a raid_id is given, look it up and assign the extra flag from its group.
|
||||||
raid = Raid.find_by(id: party_params[:raid_id])
|
if party_params && party_params[:raid_id].present?
|
||||||
party.extra = raid.group.extra
|
if (raid = Raid.find_by(id: party_params[:raid_id]))
|
||||||
|
party.extra = raid.group.extra
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if party.save!
|
# Save and render the party, triggering preview generation if the party is ready
|
||||||
return render json: PartyBlueprint.render(party, view: :created, root: :party),
|
if party.save
|
||||||
status: :created
|
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
|
||||||
|
|
||||||
render_validation_error_response(@party)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Shows a specific party if the user has permission to view it
|
||||||
|
# @return [void]
|
||||||
def show
|
def show
|
||||||
# If a party is private, check that the user is the owner or an admin
|
# 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)
|
if (@party.private? && !current_user) || (@party.private? && not_owner && !admin_mode)
|
||||||
|
|
@ -47,6 +72,8 @@ module Api
|
||||||
render_not_found_response('project')
|
render_not_found_response('project')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Updates an existing party's attributes
|
||||||
|
# @return [void]
|
||||||
def update
|
def update
|
||||||
@party.attributes = party_params.except(:skill1_id, :skill2_id, :skill3_id)
|
@party.attributes = party_params.except(:skill1_id, :skill2_id, :skill3_id)
|
||||||
|
|
||||||
|
|
@ -62,10 +89,16 @@ module Api
|
||||||
render_validation_error_response(@party)
|
render_validation_error_response(@party)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Deletes a party if the user has permission
|
||||||
|
# @return [void]
|
||||||
def destroy
|
def destroy
|
||||||
return render json: PartyBlueprint.render(@party, view: :destroyed, root: :checkin) if @party.destroy
|
render json: PartyBlueprint.render(@party, view: :destroyed, root: :checkin) if @party.destroy
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# == Extended Party Actions
|
||||||
|
|
||||||
|
# Creates a copy of an existing party with attribution
|
||||||
|
# @return [void]
|
||||||
def remix
|
def remix
|
||||||
new_party = @party.amoeba_dup
|
new_party = @party.amoeba_dup
|
||||||
new_party.attributes = {
|
new_party.attributes = {
|
||||||
|
|
@ -78,6 +111,8 @@ module Api
|
||||||
new_party.local_id = party_params[:local_id] unless party_params.nil?
|
new_party.local_id = party_params[:local_id] unless party_params.nil?
|
||||||
|
|
||||||
if new_party.save
|
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),
|
render json: PartyBlueprint.render(new_party, view: :created, root: :party),
|
||||||
status: :created
|
status: :created
|
||||||
else
|
else
|
||||||
|
|
@ -85,37 +120,30 @@ module Api
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Lists parties based on various filter criteria
|
||||||
|
# @return [void]
|
||||||
def index
|
def index
|
||||||
conditions = build_filters
|
query = build_parties_query
|
||||||
|
@parties = query.paginate(page: params[:page], per_page: COLLECTION_PER_PAGE)
|
||||||
query = build_query(conditions)
|
render_paginated_parties(@parties)
|
||||||
query = apply_includes(query, params[:includes]) if params[:includes].present?
|
|
||||||
query = apply_excludes(query, params[:excludes]) if params[:excludes].present?
|
|
||||||
|
|
||||||
@parties = fetch_parties(query)
|
|
||||||
count = calculate_count(query)
|
|
||||||
total_pages = calculate_total_pages(count)
|
|
||||||
|
|
||||||
render_party_json(@parties, count, total_pages)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Lists parties favorited by the current user
|
||||||
|
# @return [void]
|
||||||
def favorites
|
def favorites
|
||||||
raise Api::V1::UnauthorizedError unless current_user
|
raise Api::V1::UnauthorizedError unless current_user
|
||||||
|
|
||||||
conditions = build_filters
|
query = build_parties_query(favorites: true)
|
||||||
conditions[:favorites] = { user_id: current_user.id }
|
@parties = query.paginate(page: params[:page], per_page: COLLECTION_PER_PAGE)
|
||||||
|
# Mark each party as favorited (if needed)
|
||||||
query = build_query(conditions, favorites: true)
|
@parties.each { |party| party.favorited = true }
|
||||||
query = apply_includes(query, params[:includes]) if params[:includes].present?
|
render_paginated_parties(@parties)
|
||||||
query = apply_excludes(query, params[:excludes]) if params[:excludes].present?
|
|
||||||
|
|
||||||
@parties = fetch_parties(query)
|
|
||||||
count = calculate_count(query)
|
|
||||||
total_pages = calculate_total_pages(count)
|
|
||||||
|
|
||||||
render_party_json(@parties, count, total_pages)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# == Preview Management
|
||||||
|
|
||||||
|
# Serves the party's preview image
|
||||||
|
# @return [void]
|
||||||
def preview
|
def preview
|
||||||
coordinator = PreviewService::Coordinator.new(@party)
|
coordinator = PreviewService::Coordinator.new(@party)
|
||||||
|
|
||||||
|
|
@ -154,6 +182,19 @@ module Api
|
||||||
end
|
end
|
||||||
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
|
def regenerate_preview
|
||||||
party = Party.find_by!(shortcode: params[:id])
|
party = Party.find_by!(shortcode: params[:id])
|
||||||
|
|
||||||
|
|
@ -173,12 +214,67 @@ module Api
|
||||||
|
|
||||||
private
|
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
|
def authorize
|
||||||
return unless not_owner && !admin_mode
|
return unless not_owner && !admin_mode
|
||||||
|
|
||||||
render_unauthorized_response
|
render_unauthorized_response
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Determines if the current user is not the owner of the party
|
||||||
|
# @return [Boolean]
|
||||||
def not_owner
|
def not_owner
|
||||||
if @party.user
|
if @party.user
|
||||||
# party has a user and current_user does not match
|
# party has a user and current_user does not match
|
||||||
|
|
@ -197,54 +293,139 @@ module Api
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_filters
|
# == Preview Generation
|
||||||
params = request.params
|
|
||||||
|
|
||||||
start_time = build_start_time(params['recency'])
|
# Schedules a background job to generate the party preview
|
||||||
|
# @return [void]
|
||||||
min_characters_count = build_count(params['characters_count'], DEFAULT_MIN_CHARACTERS)
|
def schedule_preview_generation
|
||||||
min_summons_count = build_count(params['summons_count'], DEFAULT_MIN_SUMMONS)
|
GeneratePartyPreviewJob.perform_later(id)
|
||||||
min_weapons_count = build_count(params['weapons_count'], DEFAULT_MIN_WEAPONS)
|
|
||||||
max_clear_time = build_max_clear_time(params['max_clear_time'])
|
|
||||||
|
|
||||||
{
|
|
||||||
element: build_element(params['element']),
|
|
||||||
raid: params['raid'],
|
|
||||||
created_at: params['recency'].present? ? start_time..DateTime.current : nil,
|
|
||||||
full_auto: build_option(params['full_auto']),
|
|
||||||
auto_guard: build_option(params['auto_guard']),
|
|
||||||
charge_attack: build_option(params['charge_attack']),
|
|
||||||
characters_count: min_characters_count..MAX_CHARACTERS,
|
|
||||||
summons_count: min_summons_count..MAX_SUMMONS,
|
|
||||||
weapons_count: min_weapons_count..MAX_WEAPONS
|
|
||||||
}.delete_if { |_k, v| v.nil? }
|
|
||||||
end
|
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)
|
def build_start_time(recency)
|
||||||
return unless recency.present?
|
return unless recency.present?
|
||||||
|
|
||||||
(DateTime.current - recency.to_i.seconds).to_datetime.beginning_of_day
|
(DateTime.current - recency.to_i.seconds).to_datetime.beginning_of_day
|
||||||
end
|
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)
|
def build_count(value, default)
|
||||||
value.blank? ? default : value.to_i
|
value.blank? ? default : value.to_i
|
||||||
end
|
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)
|
def build_max_clear_time(value)
|
||||||
value.blank? ? DEFAULT_MAX_CLEAR_TIME : value.to_i
|
value.blank? ? DEFAULT_MAX_CLEAR_TIME : value.to_i
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Processes element parameter
|
||||||
|
# @param element [String, nil] element identifier
|
||||||
|
# @return [Integer, nil] processed element value
|
||||||
def build_element(element)
|
def build_element(element)
|
||||||
element.to_i unless element.blank?
|
element.to_i unless element.blank?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Processes boolean option parameters
|
||||||
|
# @param value [String, nil] option value
|
||||||
|
# @return [Integer, nil] processed option value
|
||||||
def build_option(value)
|
def build_option(value)
|
||||||
value.to_i unless value.blank? || value.to_i == -1
|
value.to_i unless value.blank? || value.to_i == -1
|
||||||
end
|
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)
|
def build_query(conditions, favorites: false)
|
||||||
query = Party.distinct
|
query = Party.distinct
|
||||||
.joins(weapons: [:object], summons: [:object], characters: [:object])
|
# 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')
|
.group('parties.id')
|
||||||
.where(conditions)
|
.where(conditions)
|
||||||
.where(privacy(favorites: favorites))
|
.where(privacy(favorites: favorites))
|
||||||
|
|
@ -252,104 +433,158 @@ module Api
|
||||||
.where(user_quality)
|
.where(user_quality)
|
||||||
.where(original)
|
.where(original)
|
||||||
|
|
||||||
query = query.joins(:favorites) if favorites
|
query = query.includes(:favorites) if favorites
|
||||||
|
|
||||||
query
|
query
|
||||||
end
|
end
|
||||||
|
|
||||||
def includes(id)
|
# Applies the include conditions to query
|
||||||
"(\"#{id_to_table(id)}\".\"granblue_id\" = '#{id}')"
|
# @param query [ActiveRecord::Relation] base query
|
||||||
end
|
# @param includes [String] comma-separated list of IDs to include
|
||||||
|
# @return [ActiveRecord::Relation] modified query
|
||||||
def excludes(id)
|
|
||||||
"(\"#{id_to_table(id)}\".\"granblue_id\" != '#{id}')"
|
|
||||||
end
|
|
||||||
|
|
||||||
def apply_includes(query, includes)
|
def apply_includes(query, includes)
|
||||||
included = includes.split(',')
|
return query unless includes.present?
|
||||||
includes_condition = included.map { |id| includes(id) }.join(' AND ')
|
|
||||||
query.where(includes_condition)
|
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
|
end
|
||||||
|
|
||||||
def apply_excludes(query, _excludes)
|
# Applies the exclude conditions to query
|
||||||
characters_subquery = excluded_characters.select(1).arel
|
# @param query [ActiveRecord::Relation] base query
|
||||||
summons_subquery = excluded_summons.select(1).arel
|
# @return [ActiveRecord::Relation] modified query
|
||||||
weapons_subquery = excluded_weapons.select(1).arel
|
def apply_excludes(query, excludes)
|
||||||
|
return query unless excludes.present?
|
||||||
|
|
||||||
query.where(characters_subquery.exists.not)
|
excludes.split(',').each do |id|
|
||||||
.where(weapons_subquery.exists.not)
|
grid_table, object_table = grid_table_and_object_table(id)
|
||||||
.where(summons_subquery.exists.not)
|
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
|
end
|
||||||
|
|
||||||
|
# == Query Filtering Helpers
|
||||||
|
|
||||||
|
# Generates subquery for excluded characters
|
||||||
|
# @return [ActiveRecord::Relation, nil] exclusion query
|
||||||
def excluded_characters
|
def excluded_characters
|
||||||
return unless params[:excludes]
|
return unless params[:excludes]
|
||||||
|
|
||||||
excluded = params[:excludes].split(',').filter { |id| id[0] == '3' }
|
excluded = params[:excludes].split(',').filter { |id| id[0] == '3' }
|
||||||
GridCharacter.joins(:object)
|
GridCharacter.includes(:object)
|
||||||
.where(characters: { granblue_id: excluded })
|
.where(characters: { granblue_id: excluded })
|
||||||
.where('grid_characters.party_id = parties.id')
|
.where('grid_characters.party_id = parties.id')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Generates subquery for excluded summons
|
||||||
|
# @return [ActiveRecord::Relation, nil] exclusion query
|
||||||
def excluded_summons
|
def excluded_summons
|
||||||
return unless params[:excludes]
|
return unless params[:excludes]
|
||||||
|
|
||||||
excluded = params[:excludes].split(',').filter { |id| id[0] == '2' }
|
excluded = params[:excludes].split(',').filter { |id| id[0] == '2' }
|
||||||
GridSummon.joins(:object)
|
GridSummon.includes(:object)
|
||||||
.where(summons: { granblue_id: excluded })
|
.where(summons: { granblue_id: excluded })
|
||||||
.where('grid_summons.party_id = parties.id')
|
.where('grid_summons.party_id = parties.id')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Generates subquery for excluded weapons
|
||||||
|
# @return [ActiveRecord::Relation, nil] exclusion query
|
||||||
def excluded_weapons
|
def excluded_weapons
|
||||||
return unless params[:excludes]
|
return unless params[:excludes]
|
||||||
|
|
||||||
excluded = params[:excludes].split(',').filter { |id| id[0] == '1' }
|
excluded = params[:excludes].split(',').filter { |id| id[0] == '1' }
|
||||||
GridWeapon.joins(:object)
|
GridWeapon.includes(:object)
|
||||||
.where(weapons: { granblue_id: excluded })
|
.where(weapons: { granblue_id: excluded })
|
||||||
.where('grid_weapons.party_id = parties.id')
|
.where('grid_weapons.party_id = parties.id')
|
||||||
end
|
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)
|
def fetch_parties(query)
|
||||||
query.order(created_at: :desc)
|
query.order(created_at: :desc)
|
||||||
.paginate(page: request.params[:page], per_page: COLLECTION_PER_PAGE)
|
.paginate(page: params[:page], per_page: COLLECTION_PER_PAGE)
|
||||||
.each { |party| party.favorited = current_user ? party.is_favorited(current_user) : false }
|
.each { |party| party.favorited = current_user ? party.is_favorited(current_user) : false }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Calculates total count for pagination
|
||||||
|
# @param query [ActiveRecord::Relation] current query
|
||||||
|
# @return [Integer] total count
|
||||||
def calculate_count(query)
|
def calculate_count(query)
|
||||||
query.count.values.sum
|
# query.count.values.sum
|
||||||
|
query.count
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Calculates total pages for pagination
|
||||||
|
# @param count [Integer] total record count
|
||||||
|
# @return [Integer] total pages
|
||||||
def calculate_total_pages(count)
|
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 > 1 ? (count.to_f / COLLECTION_PER_PAGE).ceil : 1
|
||||||
|
(count.to_f / COLLECTION_PER_PAGE).ceil
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_party_json(parties, count, total_pages)
|
# == Include/Exclude Processing
|
||||||
render json: PartyBlueprint.render(parties,
|
|
||||||
view: :collection,
|
# Generates SQL for including specific items
|
||||||
root: :results,
|
# @param id [String] item identifier
|
||||||
meta: {
|
# @return [String] SQL condition
|
||||||
count: count,
|
def includes(id)
|
||||||
total_pages: total_pages,
|
"(\"#{id_to_table(id)}\".\"granblue_id\" = '#{id}')"
|
||||||
per_page: COLLECTION_PER_PAGE
|
|
||||||
})
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def privacy(favorites: false)
|
# Generates SQL for excluding specific items
|
||||||
return if admin_mode
|
# @param id [String] item identifier
|
||||||
|
# @return [String] SQL condition
|
||||||
if favorites
|
def excludes(id)
|
||||||
'visibility < 3'
|
"(\"#{id_to_table(id)}\".\"granblue_id\" != '#{id}')"
|
||||||
else
|
|
||||||
'visibility = 1'
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# == Filter Condition Helpers
|
||||||
|
|
||||||
|
# Generates user quality condition
|
||||||
|
# @return [String, nil] SQL condition for user quality
|
||||||
def user_quality
|
def user_quality
|
||||||
return if request.params[:user_quality].blank? || request.params[:user_quality] == 'false'
|
return if params[:user_quality].blank? || params[:user_quality] == 'false'
|
||||||
|
|
||||||
'user_id IS NOT NULL'
|
'user_id IS NOT NULL'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Generates name quality condition
|
||||||
|
# @return [String, nil] SQL condition for name quality
|
||||||
def name_quality
|
def name_quality
|
||||||
|
return if params[:name_quality].blank? || params[:name_quality] == 'false'
|
||||||
|
|
||||||
low_quality = [
|
low_quality = [
|
||||||
'Untitled',
|
'Untitled',
|
||||||
'Remix of Untitled',
|
'Remix of Untitled',
|
||||||
|
|
@ -364,33 +599,54 @@ module Api
|
||||||
'無題のリミックスのリミックスのリミックスのリミックス',
|
'無題のリミックスのリミックスのリミックスのリミックス',
|
||||||
'無題のリミックスのリミックスのリミックスのリミックスのリミックス'
|
'無題のリミックスのリミックスのリミックスのリミックスのリミックス'
|
||||||
]
|
]
|
||||||
|
|
||||||
joined_names = low_quality.map { |name| "'#{name}'" }.join(',')
|
joined_names = low_quality.map { |name| "'#{name}'" }.join(',')
|
||||||
|
|
||||||
return if request.params[:name_quality].blank? || request.params[:name_quality] == 'false'
|
|
||||||
|
|
||||||
"name NOT IN (#{joined_names})"
|
"name NOT IN (#{joined_names})"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Generates original party condition
|
||||||
|
# @return [String, nil] SQL condition for original parties
|
||||||
def original
|
def original
|
||||||
return if request.params['original'].blank? || request.params['original'] == 'false'
|
return if params['original'].blank? || params['original'] == 'false'
|
||||||
|
|
||||||
'source_party_id IS NULL'
|
'source_party_id IS NULL'
|
||||||
end
|
end
|
||||||
|
|
||||||
def id_to_table(id)
|
# == Filter Condition Helpers
|
||||||
case id[0]
|
|
||||||
when '3'
|
|
||||||
table = 'characters'
|
|
||||||
when '2'
|
|
||||||
table = 'summons'
|
|
||||||
when '1'
|
|
||||||
table = 'weapons'
|
|
||||||
end
|
|
||||||
|
|
||||||
table
|
# 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
|
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)
|
def remixed_name(name)
|
||||||
blanked_name = {
|
blanked_name = {
|
||||||
en: name.blank? ? 'Untitled team' : name,
|
en: name.blank? ? 'Untitled team' : name,
|
||||||
|
|
@ -409,19 +665,54 @@ module Api
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# == Party Loading
|
||||||
|
|
||||||
|
# Loads party by shortcode for routes using :id
|
||||||
|
# @return [void]
|
||||||
def set_from_slug
|
def set_from_slug
|
||||||
@party = Party.where('shortcode = ?', params[:id]).first
|
@party = Party.includes(
|
||||||
if @party
|
:user,
|
||||||
@party.favorited = current_user && @party ? @party.is_favorited(current_user) : false
|
:job,
|
||||||
else
|
{ raid: :group },
|
||||||
render_not_found_response('party')
|
{ characters: [:character, :awakening] },
|
||||||
end
|
{
|
||||||
|
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
|
end
|
||||||
|
|
||||||
|
# Loads party by ID for update/destroy actions
|
||||||
|
# @return [void]
|
||||||
def set
|
def set
|
||||||
@party = Party.where('id = ?', params[:id]).first
|
@party = Party.where('id = ?', params[:id]).first
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# == Parameter Sanitization
|
||||||
|
|
||||||
|
# Sanitizes and permits party parameters
|
||||||
|
# @return [Hash, nil] permitted parameters
|
||||||
def party_params
|
def party_params
|
||||||
return unless params[:party].present?
|
return unless params[:party].present?
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ module Api
|
||||||
module V1
|
module V1
|
||||||
class RaidsController < Api::V1::ApiController
|
class RaidsController < Api::V1::ApiController
|
||||||
def all
|
def all
|
||||||
render json: RaidBlueprint.render(Raid.all, view: :full)
|
render json: RaidBlueprint.render(Raid.includes(:group).all, view: :nested)
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
|
@ -13,7 +13,7 @@ module Api
|
||||||
end
|
end
|
||||||
|
|
||||||
def groups
|
def groups
|
||||||
render json: RaidGroupBlueprint.render(RaidGroup.all, view: :full)
|
render json: RaidGroupBlueprint.render(RaidGroup.includes(raids: :group).all, view: :full)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -58,17 +58,19 @@ module Api
|
||||||
conditions = build_conditions
|
conditions = build_conditions
|
||||||
conditions[:user_id] = @user.id
|
conditions[:user_id] = @user.id
|
||||||
|
|
||||||
parties = Party
|
favorites_query = "EXISTS (SELECT 1 FROM favorites WHERE favorites.party_id = parties.id AND favorites.user_id = #{current_user&.id || 'NULL'}) AS is_favorited"
|
||||||
.where(conditions)
|
parties = Party.where(conditions)
|
||||||
.where(name_quality)
|
.where(name_quality)
|
||||||
.where(user_quality)
|
.where(user_quality)
|
||||||
.where(original)
|
.where(original)
|
||||||
.where(privacy)
|
.where(privacy)
|
||||||
.order(created_at: :desc)
|
.includes(:favorites)
|
||||||
.paginate(page: request.params[:page], per_page: COLLECTION_PER_PAGE)
|
.select(Party.arel_table[Arel.star])
|
||||||
.each do |party|
|
.select(
|
||||||
party.favorited = current_user ? party.is_favorited(current_user) : false
|
Arel.sql(favorites_query)
|
||||||
end
|
)
|
||||||
|
.order(created_at: :desc)
|
||||||
|
.paginate(page: request.params[:page], per_page: COLLECTION_PER_PAGE)
|
||||||
|
|
||||||
count = Party.where(conditions).count
|
count = Party.where(conditions).count
|
||||||
|
|
||||||
|
|
@ -101,7 +103,7 @@ module Api
|
||||||
|
|
||||||
unless params['recency'].blank?
|
unless params['recency'].blank?
|
||||||
start_time = (DateTime.current - params['recency'].to_i.seconds)
|
start_time = (DateTime.current - params['recency'].to_i.seconds)
|
||||||
.to_datetime.beginning_of_day
|
.to_datetime.beginning_of_day
|
||||||
end
|
end
|
||||||
|
|
||||||
min_characters_count = params['characters_count'].blank? ? DEFAULT_MIN_CHARACTERS : params['characters_count'].to_i
|
min_characters_count = params['characters_count'].blank? ? DEFAULT_MIN_CHARACTERS : params['characters_count'].to_i
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Awakening < ApplicationRecord
|
class Awakening < ApplicationRecord
|
||||||
def weapon_awakenings
|
has_many :weapon_awakenings, foreign_key: :awakening_id
|
||||||
WeaponAwakening.where(awakening_id: id)
|
has_many :weapons, through: :weapon_awakenings
|
||||||
end
|
|
||||||
|
|
||||||
def weapons
|
|
||||||
weapon_awakenings.map(&:weapon)
|
|
||||||
end
|
|
||||||
|
|
||||||
def awakening
|
def awakening
|
||||||
AwakeningBlueprint
|
AwakeningBlueprint
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,13 @@ class Character < ApplicationRecord
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AWAKENINGS = [
|
||||||
|
{ slug: 'character-balanced', name_en: 'Balanced', name_jp: 'バランス', order: 0 },
|
||||||
|
{ slug: 'character-atk', name_en: 'Attack', name_jp: '攻撃', order: 1 },
|
||||||
|
{ slug: 'character-def', name_en: 'Defense', name_jp: '防御', order: 2 },
|
||||||
|
{ slug: 'character-multi', name_en: 'Multiattack', name_jp: '連続攻撃', order: 3 }
|
||||||
|
].freeze
|
||||||
|
|
||||||
def blueprint
|
def blueprint
|
||||||
CharacterBlueprint
|
CharacterBlueprint
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class GridCharacter < ApplicationRecord
|
class GridCharacter < ApplicationRecord
|
||||||
has_one :object, class_name: 'Character', foreign_key: :id, primary_key: :character_id
|
belongs_to :character, foreign_key: :character_id, primary_key: :id
|
||||||
|
|
||||||
belongs_to :awakening, optional: true
|
belongs_to :awakening, optional: true
|
||||||
belongs_to :party,
|
belongs_to :party,
|
||||||
|
|
@ -15,6 +15,12 @@ class GridCharacter < ApplicationRecord
|
||||||
validate :validate_aetherial_mastery_value, on: :update
|
validate :validate_aetherial_mastery_value, on: :update
|
||||||
validate :over_mastery_attack_matches_hp, on: :update
|
validate :over_mastery_attack_matches_hp, on: :update
|
||||||
|
|
||||||
|
# Virtual attribute for the new rings structure
|
||||||
|
attr_accessor :new_rings
|
||||||
|
|
||||||
|
# Virtual attribute for the new awakening structure
|
||||||
|
attr_accessor :new_awakening
|
||||||
|
|
||||||
##### Amoeba configuration
|
##### Amoeba configuration
|
||||||
amoeba do
|
amoeba do
|
||||||
set ring1: { modifier: nil, strength: nil }
|
set ring1: { modifier: nil, strength: nil }
|
||||||
|
|
@ -25,6 +31,9 @@ class GridCharacter < ApplicationRecord
|
||||||
set perpetuity: false
|
set perpetuity: false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
before_validation :apply_new_rings, if: -> { new_rings.present? }
|
||||||
|
before_validation :apply_new_awakening, if: -> { new_awakening.present? }
|
||||||
|
|
||||||
# Add awakening before the model saves
|
# Add awakening before the model saves
|
||||||
before_save :add_awakening
|
before_save :add_awakening
|
||||||
|
|
||||||
|
|
@ -78,10 +87,6 @@ class GridCharacter < ApplicationRecord
|
||||||
'aetherial_mastery')
|
'aetherial_mastery')
|
||||||
end
|
end
|
||||||
|
|
||||||
def character
|
|
||||||
Character.find(character_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
def blueprint
|
def blueprint
|
||||||
GridCharacterBlueprint
|
GridCharacterBlueprint
|
||||||
end
|
end
|
||||||
|
|
@ -94,6 +99,24 @@ class GridCharacter < ApplicationRecord
|
||||||
self.awakening = Awakening.where(slug: 'character-balanced').sole
|
self.awakening = Awakening.where(slug: 'character-balanced').sole
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def apply_new_rings
|
||||||
|
# Expect new_rings to be an array of hashes, e.g.,
|
||||||
|
# [{"modifier" => "1", "strength" => "1500"}, {"modifier" => "2", "strength" => "750"}]
|
||||||
|
default_ring = { "modifier" => nil, "strength" => nil }
|
||||||
|
rings_array = Array(new_rings).map(&:to_h)
|
||||||
|
# Pad with defaults so there are exactly four rings
|
||||||
|
rings_array.fill(default_ring, rings_array.size...4)
|
||||||
|
self.ring1 = rings_array[0]
|
||||||
|
self.ring2 = rings_array[1]
|
||||||
|
self.ring3 = rings_array[2]
|
||||||
|
self.ring4 = rings_array[3]
|
||||||
|
end
|
||||||
|
|
||||||
|
def apply_new_awakening
|
||||||
|
self.awakening_id = new_awakening[:id]
|
||||||
|
self.awakening_level = new_awakening[:level].present? ? new_awakening[:level].to_i : 1
|
||||||
|
end
|
||||||
|
|
||||||
def check_value(property, type)
|
def check_value(property, type)
|
||||||
# Input format
|
# Input format
|
||||||
# { ring1: { atk: 300 } }
|
# { ring1: { atk: 300 } }
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,16 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class GridSummon < ApplicationRecord
|
class GridSummon < ApplicationRecord
|
||||||
|
belongs_to :summon, foreign_key: :summon_id, primary_key: :id
|
||||||
|
|
||||||
belongs_to :party,
|
belongs_to :party,
|
||||||
counter_cache: :summons_count,
|
counter_cache: :summons_count,
|
||||||
inverse_of: :summons
|
inverse_of: :summons
|
||||||
validates_presence_of :party
|
validates_presence_of :party
|
||||||
has_one :object, class_name: 'Summon', foreign_key: :id, primary_key: :summon_id
|
|
||||||
|
|
||||||
validate :compatible_with_position, on: :create
|
validate :compatible_with_position, on: :create
|
||||||
validate :no_conflicts, on: :create
|
validate :no_conflicts, on: :create
|
||||||
|
|
||||||
def summon
|
|
||||||
Summon.find(summon_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
def blueprint
|
def blueprint
|
||||||
GridSummonBlueprint
|
GridSummonBlueprint
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class GridWeapon < ApplicationRecord
|
class GridWeapon < ApplicationRecord
|
||||||
|
belongs_to :weapon, foreign_key: :weapon_id, primary_key: :id
|
||||||
|
|
||||||
belongs_to :party,
|
belongs_to :party,
|
||||||
counter_cache: :weapons_count,
|
counter_cache: :weapons_count,
|
||||||
inverse_of: :weapons
|
inverse_of: :weapons
|
||||||
validates_presence_of :party
|
validates_presence_of :party
|
||||||
|
|
||||||
has_one :object, class_name: 'Weapon', foreign_key: :id, primary_key: :weapon_id
|
|
||||||
|
|
||||||
belongs_to :weapon_key1, class_name: 'WeaponKey', foreign_key: :weapon_key1_id, optional: true
|
belongs_to :weapon_key1, class_name: 'WeaponKey', foreign_key: :weapon_key1_id, optional: true
|
||||||
belongs_to :weapon_key2, class_name: 'WeaponKey', foreign_key: :weapon_key2_id, optional: true
|
belongs_to :weapon_key2, class_name: 'WeaponKey', foreign_key: :weapon_key2_id, optional: true
|
||||||
belongs_to :weapon_key3, class_name: 'WeaponKey', foreign_key: :weapon_key3_id, optional: true
|
belongs_to :weapon_key3, class_name: 'WeaponKey', foreign_key: :weapon_key3_id, optional: true
|
||||||
|
|
@ -33,10 +33,6 @@ class GridWeapon < ApplicationRecord
|
||||||
GridWeaponBlueprint
|
GridWeaponBlueprint
|
||||||
end
|
end
|
||||||
|
|
||||||
def weapon
|
|
||||||
Weapon.find(weapon_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
def weapon_keys
|
def weapon_keys
|
||||||
[weapon_key1, weapon_key2, weapon_key3].compact
|
[weapon_key1, weapon_key2, weapon_key3].compact
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,9 @@ class Party < ApplicationRecord
|
||||||
foreign_key: :source_party_id,
|
foreign_key: :source_party_id,
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
has_many :derivative_parties,
|
has_many :remixes, -> { order(created_at: :desc) },
|
||||||
class_name: 'Party',
|
class_name: 'Party',
|
||||||
foreign_key: :source_party_id,
|
foreign_key: 'source_party_id',
|
||||||
inverse_of: :source_party,
|
inverse_of: :source_party,
|
||||||
dependent: :nullify
|
dependent: :nullify
|
||||||
|
|
||||||
|
|
@ -60,18 +60,21 @@ class Party < ApplicationRecord
|
||||||
has_many :characters,
|
has_many :characters,
|
||||||
foreign_key: 'party_id',
|
foreign_key: 'party_id',
|
||||||
class_name: 'GridCharacter',
|
class_name: 'GridCharacter',
|
||||||
|
counter_cache: true,
|
||||||
dependent: :destroy,
|
dependent: :destroy,
|
||||||
inverse_of: :party
|
inverse_of: :party
|
||||||
|
|
||||||
has_many :weapons,
|
has_many :weapons,
|
||||||
foreign_key: 'party_id',
|
foreign_key: 'party_id',
|
||||||
class_name: 'GridWeapon',
|
class_name: 'GridWeapon',
|
||||||
|
counter_cache: true,
|
||||||
dependent: :destroy,
|
dependent: :destroy,
|
||||||
inverse_of: :party
|
inverse_of: :party
|
||||||
|
|
||||||
has_many :summons,
|
has_many :summons,
|
||||||
foreign_key: 'party_id',
|
foreign_key: 'party_id',
|
||||||
class_name: 'GridSummon',
|
class_name: 'GridSummon',
|
||||||
|
counter_cache: true,
|
||||||
dependent: :destroy,
|
dependent: :destroy,
|
||||||
inverse_of: :party
|
inverse_of: :party
|
||||||
|
|
||||||
|
|
@ -103,8 +106,6 @@ class Party < ApplicationRecord
|
||||||
validate :skills_are_unique
|
validate :skills_are_unique
|
||||||
validate :guidebooks_are_unique
|
validate :guidebooks_are_unique
|
||||||
|
|
||||||
attr_accessor :favorited
|
|
||||||
|
|
||||||
self.enum :preview_state, {
|
self.enum :preview_state, {
|
||||||
pending: 0,
|
pending: 0,
|
||||||
queued: 1,
|
queued: 1,
|
||||||
|
|
@ -113,11 +114,7 @@ class Party < ApplicationRecord
|
||||||
failed: 4
|
failed: 4
|
||||||
}
|
}
|
||||||
|
|
||||||
after_commit :schedule_preview_regeneration, if: :preview_relevant_changes?
|
after_commit :schedule_preview_generation, if: :should_generate_preview?
|
||||||
|
|
||||||
def is_favorited(user)
|
|
||||||
user.favorite_parties.include? self if user
|
|
||||||
end
|
|
||||||
|
|
||||||
def is_remix
|
def is_remix
|
||||||
!source_party.nil?
|
!source_party.nil?
|
||||||
|
|
@ -143,6 +140,58 @@ class Party < ApplicationRecord
|
||||||
visibility == 3
|
visibility == 3
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def is_favorited(user)
|
||||||
|
return false unless user
|
||||||
|
|
||||||
|
Rails.cache.fetch("party_#{id}_favorited_by_#{user.id}", expires_in: 1.hour) do
|
||||||
|
user.favorite_parties.include?(self)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def ready_for_preview?
|
||||||
|
return false if weapons_count < 1 # At least 1 weapon
|
||||||
|
return false if characters_count < 1 # At least 1 character
|
||||||
|
return false if summons_count < 1 # At least 1 summon
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def should_generate_preview?
|
||||||
|
return false unless ready_for_preview?
|
||||||
|
|
||||||
|
# Always generate if no preview exists
|
||||||
|
return true if preview_state.nil? || preview_state == 'pending'
|
||||||
|
|
||||||
|
# Generate if failed and enough time has passed for conditions to change
|
||||||
|
return true if preview_state == 'failed' && preview_generated_at < 5.minutes.ago
|
||||||
|
|
||||||
|
# Generate if preview is old
|
||||||
|
return true if preview_state == 'generated' && preview_expired?
|
||||||
|
|
||||||
|
# Only regenerate on content changes if the last generation was > 5 minutes ago
|
||||||
|
# This prevents rapid regeneration during party building
|
||||||
|
if preview_content_changed?
|
||||||
|
return true if preview_generated_at.nil? || preview_generated_at < 5.minutes.ago
|
||||||
|
end
|
||||||
|
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def preview_expired?
|
||||||
|
preview_generated_at.nil? ||
|
||||||
|
preview_generated_at < PreviewService::Coordinator::PREVIEW_EXPIRY.ago
|
||||||
|
end
|
||||||
|
|
||||||
|
def preview_content_changed?
|
||||||
|
saved_changes.keys.any? { |attr| preview_relevant_attributes.include?(attr) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def schedule_preview_generation
|
||||||
|
return if preview_state == 'queued' || preview_state == 'in_progress'
|
||||||
|
|
||||||
|
update_column(:preview_state, 'queued')
|
||||||
|
GeneratePartyPreviewJob.perform_later(id)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_shortcode
|
def set_shortcode
|
||||||
|
|
@ -188,17 +237,10 @@ class Party < ApplicationRecord
|
||||||
errors.add(:guidebooks, 'must be unique')
|
errors.add(:guidebooks, 'must be unique')
|
||||||
end
|
end
|
||||||
|
|
||||||
def preview_relevant_changes?
|
def preview_relevant_attributes
|
||||||
return false if preview_state == 'queued'
|
%w[
|
||||||
|
name job_id element weapons_count characters_count summons_count
|
||||||
(saved_changes.keys & %w[name job_id element weapons_count characters_count summons_count]).any?
|
full_auto auto_guard charge_attack clear_time
|
||||||
end
|
]
|
||||||
|
|
||||||
def schedule_preview_regeneration
|
|
||||||
# Cancel any pending jobs
|
|
||||||
GeneratePartyPreviewJob.cancel_scheduled_jobs(party_id: id)
|
|
||||||
|
|
||||||
# Mark as pending
|
|
||||||
update_column(:preview_state, :pending)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,4 @@
|
||||||
class WeaponAwakening < ApplicationRecord
|
class WeaponAwakening < ApplicationRecord
|
||||||
belongs_to :weapon
|
belongs_to :weapon
|
||||||
belongs_to :awakening
|
belongs_to :awakening
|
||||||
|
|
||||||
def weapon
|
|
||||||
Weapon.find(weapon_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
def awakening
|
|
||||||
Awakening.find(awakening_id)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,9 @@ module PreviewService
|
||||||
GENERATION_TIMEOUT = 5.minutes
|
GENERATION_TIMEOUT = 5.minutes
|
||||||
LOCAL_STORAGE_PATH = Rails.root.join('storage', 'party-previews')
|
LOCAL_STORAGE_PATH = Rails.root.join('storage', 'party-previews')
|
||||||
|
|
||||||
|
PREVIEW_DEBOUNCE_PERIOD = 5.minutes
|
||||||
|
PREVIEW_EXPIRY = 30.days
|
||||||
|
|
||||||
# Public Interface - Core Operations
|
# Public Interface - Core Operations
|
||||||
|
|
||||||
# Initialize the party preview service
|
# Initialize the party preview service
|
||||||
|
|
@ -40,59 +43,59 @@ module PreviewService
|
||||||
return false unless should_generate?
|
return false unless should_generate?
|
||||||
|
|
||||||
begin
|
begin
|
||||||
Rails.logger.info("Starting preview generation for party #{@party.id}")
|
Rails.logger.info("🖼️ Starting preview generation for party #{@party.id}")
|
||||||
|
|
||||||
# Update state to in_progress
|
Rails.logger.info("🖼️ Updating party state to in_progress")
|
||||||
@party.update!(preview_state: :in_progress)
|
@party.update!(preview_state: :in_progress)
|
||||||
set_generation_in_progress
|
set_generation_in_progress
|
||||||
|
|
||||||
Rails.logger.info("Checking ImageMagick installation...")
|
Rails.logger.info("🖼️ Checking ImageMagick installation...")
|
||||||
begin
|
begin
|
||||||
version = `convert -version`
|
version = `convert -version`
|
||||||
Rails.logger.info("ImageMagick version: #{version}")
|
Rails.logger.info("🖼️ ImageMagick version: #{version}")
|
||||||
rescue => e
|
rescue => e
|
||||||
Rails.logger.error("Failed to get ImageMagick version: #{e.message}")
|
Rails.logger.error("🖼️ Failed to get ImageMagick version: #{e.message}")
|
||||||
end
|
end
|
||||||
|
|
||||||
Rails.logger.info("Creating preview image...")
|
Rails.logger.info("🖼️ Creating preview image...")
|
||||||
begin
|
begin
|
||||||
image = create_preview_image
|
image = create_preview_image
|
||||||
Rails.logger.info("Preview image created successfully")
|
Rails.logger.info("🖼️ Preview image created successfully")
|
||||||
rescue => e
|
rescue => e
|
||||||
Rails.logger.error("Failed to create preview image: #{e.class} - #{e.message}")
|
Rails.logger.error("🖼️ Failed to create preview image: #{e.class} - #{e.message}")
|
||||||
Rails.logger.error(e.backtrace.join("\n"))
|
Rails.logger.error(e.backtrace.join("\n"))
|
||||||
raise e
|
raise e
|
||||||
end
|
end
|
||||||
|
|
||||||
Rails.logger.info("Saving preview...")
|
Rails.logger.info("🖼️ Saving preview...")
|
||||||
begin
|
begin
|
||||||
save_preview(image)
|
save_preview(image)
|
||||||
Rails.logger.info("Preview saved successfully")
|
Rails.logger.info("🖼️ Preview saved successfully")
|
||||||
rescue => e
|
rescue => e
|
||||||
Rails.logger.error("Failed to save preview: #{e.class} - #{e.message}")
|
Rails.logger.error("🖼️ Failed to save preview: #{e.class} - #{e.message}")
|
||||||
Rails.logger.error(e.backtrace.join("\n"))
|
Rails.logger.error(e.backtrace.join("\n"))
|
||||||
raise e
|
raise e
|
||||||
end
|
end
|
||||||
|
|
||||||
Rails.logger.info("Updating party state...")
|
Rails.logger.info("🖼️ Updating party state...")
|
||||||
@party.update!(
|
@party.update!(
|
||||||
preview_state: :generated,
|
preview_state: :generated,
|
||||||
preview_generated_at: Time.current
|
preview_generated_at: Time.current
|
||||||
)
|
)
|
||||||
Rails.logger.info("Party state updated successfully")
|
Rails.logger.info("🖼️ Party state updated successfully")
|
||||||
|
|
||||||
true
|
true
|
||||||
rescue => e
|
rescue => e
|
||||||
Rails.logger.error("Preview generation failed: #{e.class} - #{e.message}")
|
Rails.logger.error("🖼️ Preview generation failed: #{e.class} - #{e.message}")
|
||||||
Rails.logger.error("Stack trace:")
|
Rails.logger.error("🖼️ Stack trace:")
|
||||||
Rails.logger.error(e.backtrace.join("\n"))
|
Rails.logger.error(e.backtrace.join("\n"))
|
||||||
handle_preview_generation_error(e)
|
handle_preview_generation_error(e)
|
||||||
false
|
false
|
||||||
ensure
|
ensure
|
||||||
Rails.logger.info("Cleaning up resources...")
|
Rails.logger.info("🖼️ Cleaning up resources...")
|
||||||
@image_fetcher.cleanup
|
@image_fetcher.cleanup
|
||||||
clear_generation_in_progress
|
clear_generation_in_progress
|
||||||
Rails.logger.info("Cleanup completed")
|
Rails.logger.info("🖼️ Cleanup completed")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -128,32 +131,48 @@ module PreviewService
|
||||||
#
|
#
|
||||||
# @return [Boolean] True if a new preview should be generated, false otherwise
|
# @return [Boolean] True if a new preview should be generated, false otherwise
|
||||||
def should_generate?
|
def should_generate?
|
||||||
Rails.logger.info("Checking should_generate? conditions")
|
Rails.logger.info("🖼️ Checking should_generate? conditions")
|
||||||
|
|
||||||
if generation_in_progress?
|
unless @party.ready_for_preview?
|
||||||
Rails.logger.info("Generation already in progress, returning false")
|
Rails.logger.info("🖼️ Party not ready for preview (insufficient content)")
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
Rails.logger.info("Preview state: #{@party.preview_state}")
|
if generation_in_progress?
|
||||||
# Add 'queued' to the list of valid states for generation
|
Rails.logger.info("🖼️ Generation already in progress, returning false")
|
||||||
if @party.preview_state.in?(['pending', 'failed', 'queued'])
|
return false
|
||||||
Rails.logger.info("Preview state is #{@party.preview_state}, returning true")
|
|
||||||
return true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if @party.preview_state == 'generated'
|
Rails.logger.info("🖼️ Preview state: #{@party.preview_state}")
|
||||||
if @party.preview_generated_at < PREVIEW_EXPIRY.ago
|
|
||||||
Rails.logger.info("Preview is older than expiry time, returning true")
|
|
||||||
return true
|
|
||||||
else
|
|
||||||
Rails.logger.info("Preview is recent, returning false")
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
Rails.logger.info("No conditions met, returning false")
|
case @party.preview_state
|
||||||
false
|
when 'pending', 'queued'
|
||||||
|
Rails.logger.info("🖼️ State is #{@party.preview_state}, will generate")
|
||||||
|
true
|
||||||
|
when 'in_progress'
|
||||||
|
Rails.logger.info("🖼️ State is in_progress, skipping generation")
|
||||||
|
false
|
||||||
|
when 'failed'
|
||||||
|
should_retry = @party.preview_generated_at.nil? ||
|
||||||
|
@party.preview_generated_at < PREVIEW_DEBOUNCE_PERIOD.ago
|
||||||
|
Rails.logger.info("🖼️ Failed state, should retry: #{should_retry}")
|
||||||
|
should_retry
|
||||||
|
when 'generated'
|
||||||
|
expired = @party.preview_expired?
|
||||||
|
changed = @party.preview_content_changed?
|
||||||
|
debounced = @party.preview_generated_at.nil? ||
|
||||||
|
@party.preview_generated_at < PREVIEW_DEBOUNCE_PERIOD.ago
|
||||||
|
|
||||||
|
should_regenerate = expired || (changed && debounced)
|
||||||
|
|
||||||
|
Rails.logger.info("🖼️ Generated state check - expired: #{expired}, content changed: #{changed}, debounced: #{debounced}")
|
||||||
|
Rails.logger.info("🖼️ Should regenerate: #{should_regenerate}")
|
||||||
|
|
||||||
|
should_regenerate
|
||||||
|
else
|
||||||
|
Rails.logger.info("🖼️ Unknown state, will generate")
|
||||||
|
true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Checks if a preview generation is currently in progress
|
# Checks if a preview generation is currently in progress
|
||||||
|
|
@ -480,6 +499,12 @@ module PreviewService
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.cleanup_stalled_jobs
|
||||||
|
Party.where(preview_state: :in_progress)
|
||||||
|
.where('updated_at < ?', 10.minutes.ago)
|
||||||
|
.update_all(preview_state: :pending)
|
||||||
|
end
|
||||||
|
|
||||||
# Deletes local preview files
|
# Deletes local preview files
|
||||||
#
|
#
|
||||||
# @return [void]
|
# @return [void]
|
||||||
|
|
@ -498,8 +523,12 @@ module PreviewService
|
||||||
def handle_preview_generation_error(error)
|
def handle_preview_generation_error(error)
|
||||||
Rails.logger.error("Preview generation failed for party #{@party.id}")
|
Rails.logger.error("Preview generation failed for party #{@party.id}")
|
||||||
Rails.logger.error("Error: #{error.class} - #{error.message}")
|
Rails.logger.error("Error: #{error.class} - #{error.message}")
|
||||||
Rails.logger.error("Backtrace:\n#{error.backtrace.join("\n")}")
|
Rails.logger.error(error.backtrace.join("\n"))
|
||||||
@party.update!(preview_state: :failed)
|
|
||||||
|
@party.update_columns(
|
||||||
|
preview_state: 'failed',
|
||||||
|
preview_generated_at: Time.current
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,11 @@ module HenseiApi
|
||||||
config.paths["app/assets"].unshift(Rails.root.join("app", "assets").to_s)
|
config.paths["app/assets"].unshift(Rails.root.join("app", "assets").to_s)
|
||||||
config.assets.paths << Rails.root.join("app", "assets", "fonts")
|
config.assets.paths << Rails.root.join("app", "assets", "fonts")
|
||||||
|
|
||||||
|
# Enable query logging
|
||||||
|
config.active_record.query_log_tags_enabled = true
|
||||||
|
config.active_record.query_log_tags = [:application, :controller, :action, :job]
|
||||||
|
config.active_record.cache_query_log_tags = true
|
||||||
|
|
||||||
# API-only application configuration
|
# API-only application configuration
|
||||||
config.api_only = true
|
config.api_only = true
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -76,4 +76,8 @@ Rails.application.configure do
|
||||||
# This makes it easy to tag log lines with debug information like subdomain and request id -
|
# This makes it easy to tag log lines with debug information like subdomain and request id -
|
||||||
# both very helpful in debugging multi-user production applications.
|
# both very helpful in debugging multi-user production applications.
|
||||||
config.log_tags = [:request_id]
|
config.log_tags = [:request_id]
|
||||||
|
|
||||||
|
config.after_initialize do
|
||||||
|
Prosopite.rails_logger = true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class CacheFreeLogger < ActiveSupport::Logger
|
# class CacheFreeLogger < ActiveSupport::Logger
|
||||||
def add(severity, message = nil, progname = nil, &block)
|
# def add(severity, message = nil, progname = nil, &block)
|
||||||
return true if progname&.include? 'CACHE'
|
# return true if progname&.include? 'CACHE'
|
||||||
|
#
|
||||||
super
|
# super
|
||||||
end
|
# end
|
||||||
end
|
# end
|
||||||
|
#
|
||||||
ActiveRecord::Base.logger = CacheFreeLogger.new($stdout)
|
ActiveRecord::Base.logger = Logger.new(STDOUT)
|
||||||
ActiveRecord::Base.logger.level = 1
|
# ActiveRecord::Base.logger.level = 1
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
# Fetch environment variables with defaults if not set
|
redis_url = ENV.fetch('REDIS_URL', 'redis://localhost:6379/0')
|
||||||
redis_url = ENV.fetch('REDIS_URL', 'redis://localhost')
|
|
||||||
redis_port = ENV.fetch('REDISPORT', '6379')
|
|
||||||
|
|
||||||
# Combine URL and port (adjust the path/DB as needed)
|
|
||||||
full_redis_url = "#{redis_url}/0"
|
|
||||||
|
|
||||||
Sidekiq.configure_server do |config|
|
Sidekiq.configure_server do |config|
|
||||||
config.redis = { url: full_redis_url }
|
config.redis = { url: redis_url }
|
||||||
|
config.death_handlers << ->(job, ex) do
|
||||||
|
Rails.logger.error("Preview generation job #{job['jid']} failed with: #{ex.message}")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Sidekiq.configure_client do |config|
|
Sidekiq.configure_client do |config|
|
||||||
config.redis = { url: full_redis_url }
|
config.redis = { url: redis_url }
|
||||||
end
|
end
|
||||||
|
|
|
||||||
5
config/sidekiq.yml
Normal file
5
config/sidekiq.yml
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
:scheduler:
|
||||||
|
cleanup_party_previews:
|
||||||
|
cron: '0 0 * * *' # Daily at midnight
|
||||||
|
class: CleanupPartyPreviewsJob
|
||||||
|
queue: maintenance
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
class SetPreviewStateDefaultToPending < ActiveRecord::Migration[8.0]
|
||||||
|
def up
|
||||||
|
Party.where(preview_state: nil).find_each do |party|
|
||||||
|
party.update_column(:preview_state, :pending)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
12
db/migrate/20250120224953_add_missing_indexes_to_parties.rb
Normal file
12
db/migrate/20250120224953_add_missing_indexes_to_parties.rb
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
class AddMissingIndexesToParties < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
add_index :parties, :visibility
|
||||||
|
add_index :parties, :element
|
||||||
|
add_index :parties, :created_at
|
||||||
|
add_index :parties, [:weapons_count, :characters_count, :summons_count],
|
||||||
|
name: 'index_parties_on_counters'
|
||||||
|
add_index :parties, [:visibility, :created_at],
|
||||||
|
name: 'index_parties_on_visibility_created_at'
|
||||||
|
add_index :parties, :shortcode
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
class AddMissingIndexesToGridObjects < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
add_index :parties, :raid_id unless index_exists?(:parties, :raid_id)
|
||||||
|
add_index :characters, :granblue_id unless index_exists?(:characters, :granblue_id)
|
||||||
|
add_index :summons, :granblue_id unless index_exists?(:summons, :granblue_id)
|
||||||
|
add_index :weapons, :granblue_id unless index_exists?(:weapons, :granblue_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
15
db/migrate/20250201041406_create_pghero_query_stats.rb
Normal file
15
db/migrate/20250201041406_create_pghero_query_stats.rb
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
class CreatePgheroQueryStats < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
create_table :pghero_query_stats do |t|
|
||||||
|
t.text :database
|
||||||
|
t.text :user
|
||||||
|
t.text :query
|
||||||
|
t.integer :query_hash, limit: 8
|
||||||
|
t.float :total_time
|
||||||
|
t.integer :calls, limit: 8
|
||||||
|
t.timestamp :captured_at
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :pghero_query_stats, [:database, :captured_at]
|
||||||
|
end
|
||||||
|
end
|
||||||
9
db/migrate/20250201120655_enable_pg_statements.rb
Normal file
9
db/migrate/20250201120655_enable_pg_statements.rb
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
class EnablePgStatements < ActiveRecord::Migration[8.0]
|
||||||
|
def up
|
||||||
|
execute 'CREATE EXTENSION IF NOT EXISTS pg_stat_statements;'
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
execute 'DROP EXTENSION IF EXISTS pg_stat_statements;'
|
||||||
|
end
|
||||||
|
end
|
||||||
5
db/migrate/20250201120842_remove_unused_index.rb
Normal file
5
db/migrate/20250201120842_remove_unused_index.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
class RemoveUnusedIndex < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
remove_index :parties, :visibility
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
class AddOptimizedIndexesToParties < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
# Add composite index for grid positions since we order by these
|
||||||
|
add_index :grid_weapons, [:party_id, :position], name: 'index_grid_weapons_on_party_id_and_position'
|
||||||
|
add_index :grid_characters, [:party_id, :position], name: 'index_grid_characters_on_party_id_and_position'
|
||||||
|
add_index :grid_summons, [:party_id, :position], name: 'index_grid_summons_on_party_id_and_position'
|
||||||
|
end
|
||||||
|
end
|
||||||
31
db/schema.rb
31
db/schema.rb
|
|
@ -10,13 +10,13 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.0].define(version: 2025_01_19_062554) do
|
ActiveRecord::Schema[8.0].define(version: 2025_02_01_170037) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "btree_gin"
|
enable_extension "btree_gin"
|
||||||
enable_extension "pg_catalog.plpgsql"
|
enable_extension "pg_catalog.plpgsql"
|
||||||
|
enable_extension "pg_stat_statements"
|
||||||
enable_extension "pg_trgm"
|
enable_extension "pg_trgm"
|
||||||
enable_extension "pgcrypto"
|
enable_extension "pgcrypto"
|
||||||
enable_extension "uuid-ossp"
|
|
||||||
|
|
||||||
create_table "app_updates", primary_key: "updated_at", id: :datetime, force: :cascade do |t|
|
create_table "app_updates", primary_key: "updated_at", id: :datetime, force: :cascade do |t|
|
||||||
t.string "update_type", null: false
|
t.string "update_type", null: false
|
||||||
|
|
@ -67,6 +67,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_19_062554) do
|
||||||
t.string "kamigame", default: ""
|
t.string "kamigame", default: ""
|
||||||
t.string "nicknames_en", default: [], null: false, array: true
|
t.string "nicknames_en", default: [], null: false, array: true
|
||||||
t.string "nicknames_jp", default: [], null: false, array: true
|
t.string "nicknames_jp", default: [], null: false, array: true
|
||||||
|
t.index ["granblue_id"], name: "index_characters_on_granblue_id"
|
||||||
t.index ["name_en"], name: "index_characters_on_name_en", opclass: :gin_trgm_ops, using: :gin
|
t.index ["name_en"], name: "index_characters_on_name_en", opclass: :gin_trgm_ops, using: :gin
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -129,6 +130,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_19_062554) do
|
||||||
t.integer "awakening_level", default: 1
|
t.integer "awakening_level", default: 1
|
||||||
t.index ["awakening_id"], name: "index_grid_characters_on_awakening_id"
|
t.index ["awakening_id"], name: "index_grid_characters_on_awakening_id"
|
||||||
t.index ["character_id"], name: "index_grid_characters_on_character_id"
|
t.index ["character_id"], name: "index_grid_characters_on_character_id"
|
||||||
|
t.index ["party_id", "position"], name: "index_grid_characters_on_party_id_and_position"
|
||||||
t.index ["party_id"], name: "index_grid_characters_on_party_id"
|
t.index ["party_id"], name: "index_grid_characters_on_party_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -143,6 +145,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_19_062554) do
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.integer "transcendence_step", default: 0, null: false
|
t.integer "transcendence_step", default: 0, null: false
|
||||||
t.boolean "quick_summon", default: false
|
t.boolean "quick_summon", default: false
|
||||||
|
t.index ["party_id", "position"], name: "index_grid_summons_on_party_id_and_position"
|
||||||
t.index ["party_id"], name: "index_grid_summons_on_party_id"
|
t.index ["party_id"], name: "index_grid_summons_on_party_id"
|
||||||
t.index ["summon_id"], name: "index_grid_summons_on_summon_id"
|
t.index ["summon_id"], name: "index_grid_summons_on_summon_id"
|
||||||
end
|
end
|
||||||
|
|
@ -168,6 +171,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_19_062554) do
|
||||||
t.integer "transcendence_step", default: 0
|
t.integer "transcendence_step", default: 0
|
||||||
t.string "weapon_key4_id"
|
t.string "weapon_key4_id"
|
||||||
t.index ["awakening_id"], name: "index_grid_weapons_on_awakening_id"
|
t.index ["awakening_id"], name: "index_grid_weapons_on_awakening_id"
|
||||||
|
t.index ["party_id", "position"], name: "index_grid_weapons_on_party_id_and_position"
|
||||||
t.index ["party_id"], name: "index_grid_weapons_on_party_id"
|
t.index ["party_id"], name: "index_grid_weapons_on_party_id"
|
||||||
t.index ["weapon_id"], name: "index_grid_weapons_on_weapon_id"
|
t.index ["weapon_id"], name: "index_grid_weapons_on_weapon_id"
|
||||||
t.index ["weapon_key1_id"], name: "index_grid_weapons_on_weapon_key1_id"
|
t.index ["weapon_key1_id"], name: "index_grid_weapons_on_weapon_key1_id"
|
||||||
|
|
@ -304,18 +308,24 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_19_062554) do
|
||||||
t.datetime "preview_generated_at"
|
t.datetime "preview_generated_at"
|
||||||
t.string "preview_s3_key"
|
t.string "preview_s3_key"
|
||||||
t.index ["accessory_id"], name: "index_parties_on_accessory_id"
|
t.index ["accessory_id"], name: "index_parties_on_accessory_id"
|
||||||
|
t.index ["created_at"], name: "index_parties_on_created_at"
|
||||||
|
t.index ["element"], name: "index_parties_on_element"
|
||||||
t.index ["guidebook1_id"], name: "index_parties_on_guidebook1_id"
|
t.index ["guidebook1_id"], name: "index_parties_on_guidebook1_id"
|
||||||
t.index ["guidebook2_id"], name: "index_parties_on_guidebook2_id"
|
t.index ["guidebook2_id"], name: "index_parties_on_guidebook2_id"
|
||||||
t.index ["guidebook3_id"], name: "index_parties_on_guidebook3_id"
|
t.index ["guidebook3_id"], name: "index_parties_on_guidebook3_id"
|
||||||
t.index ["job_id"], name: "index_parties_on_job_id"
|
t.index ["job_id"], name: "index_parties_on_job_id"
|
||||||
t.index ["preview_generated_at"], name: "index_parties_on_preview_generated_at"
|
t.index ["preview_generated_at"], name: "index_parties_on_preview_generated_at"
|
||||||
t.index ["preview_state"], name: "index_parties_on_preview_state"
|
t.index ["preview_state"], name: "index_parties_on_preview_state"
|
||||||
|
t.index ["raid_id"], name: "index_parties_on_raid_id"
|
||||||
|
t.index ["shortcode"], name: "index_parties_on_shortcode"
|
||||||
t.index ["skill0_id"], name: "index_parties_on_skill0_id"
|
t.index ["skill0_id"], name: "index_parties_on_skill0_id"
|
||||||
t.index ["skill1_id"], name: "index_parties_on_skill1_id"
|
t.index ["skill1_id"], name: "index_parties_on_skill1_id"
|
||||||
t.index ["skill2_id"], name: "index_parties_on_skill2_id"
|
t.index ["skill2_id"], name: "index_parties_on_skill2_id"
|
||||||
t.index ["skill3_id"], name: "index_parties_on_skill3_id"
|
t.index ["skill3_id"], name: "index_parties_on_skill3_id"
|
||||||
t.index ["source_party_id"], name: "index_parties_on_source_party_id"
|
t.index ["source_party_id"], name: "index_parties_on_source_party_id"
|
||||||
t.index ["user_id"], name: "index_parties_on_user_id"
|
t.index ["user_id"], name: "index_parties_on_user_id"
|
||||||
|
t.index ["visibility", "created_at"], name: "index_parties_on_visibility_created_at"
|
||||||
|
t.index ["weapons_count", "characters_count", "summons_count"], name: "index_parties_on_counters"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "pg_search_documents", force: :cascade do |t|
|
create_table "pg_search_documents", force: :cascade do |t|
|
||||||
|
|
@ -331,6 +341,17 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_19_062554) do
|
||||||
t.index ["searchable_type", "searchable_id"], name: "index_pg_search_documents_on_searchable"
|
t.index ["searchable_type", "searchable_id"], name: "index_pg_search_documents_on_searchable"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "pghero_query_stats", force: :cascade do |t|
|
||||||
|
t.text "database"
|
||||||
|
t.text "user"
|
||||||
|
t.text "query"
|
||||||
|
t.bigint "query_hash"
|
||||||
|
t.float "total_time"
|
||||||
|
t.bigint "calls"
|
||||||
|
t.datetime "captured_at", precision: nil
|
||||||
|
t.index ["database", "captured_at"], name: "index_pghero_query_stats_on_database_and_captured_at"
|
||||||
|
end
|
||||||
|
|
||||||
create_table "raid_groups", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
create_table "raid_groups", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||||
t.string "name_en", null: false
|
t.string "name_en", null: false
|
||||||
t.string "name_jp", null: false
|
t.string "name_jp", null: false
|
||||||
|
|
@ -349,7 +370,6 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_19_062554) do
|
||||||
t.integer "element"
|
t.integer "element"
|
||||||
t.string "slug"
|
t.string "slug"
|
||||||
t.uuid "group_id"
|
t.uuid "group_id"
|
||||||
t.index ["group_id"], name: "index_raids_on_group_id"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "sparks", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
create_table "sparks", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||||
|
|
@ -400,6 +420,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_19_062554) do
|
||||||
t.date "transcendence_date"
|
t.date "transcendence_date"
|
||||||
t.string "nicknames_en", default: [], null: false, array: true
|
t.string "nicknames_en", default: [], null: false, array: true
|
||||||
t.string "nicknames_jp", default: [], null: false, array: true
|
t.string "nicknames_jp", default: [], null: false, array: true
|
||||||
|
t.index ["granblue_id"], name: "index_summons_on_granblue_id"
|
||||||
t.index ["name_en"], name: "index_summons_on_name_en", opclass: :gin_trgm_ops, using: :gin
|
t.index ["name_en"], name: "index_summons_on_name_en", opclass: :gin_trgm_ops, using: :gin
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -474,6 +495,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_19_062554) do
|
||||||
t.boolean "transcendence", default: false
|
t.boolean "transcendence", default: false
|
||||||
t.date "transcendence_date"
|
t.date "transcendence_date"
|
||||||
t.string "recruits"
|
t.string "recruits"
|
||||||
|
t.index ["granblue_id"], name: "index_weapons_on_granblue_id"
|
||||||
t.index ["name_en"], name: "index_weapons_on_name_en", opclass: :gin_trgm_ops, using: :gin
|
t.index ["name_en"], name: "index_weapons_on_name_en", opclass: :gin_trgm_ops, using: :gin
|
||||||
t.index ["recruits"], name: "index_weapons_on_recruits"
|
t.index ["recruits"], name: "index_weapons_on_recruits"
|
||||||
end
|
end
|
||||||
|
|
@ -501,9 +523,10 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_19_062554) do
|
||||||
add_foreign_key "parties", "job_skills", column: "skill2_id"
|
add_foreign_key "parties", "job_skills", column: "skill2_id"
|
||||||
add_foreign_key "parties", "job_skills", column: "skill3_id"
|
add_foreign_key "parties", "job_skills", column: "skill3_id"
|
||||||
add_foreign_key "parties", "jobs"
|
add_foreign_key "parties", "jobs"
|
||||||
|
add_foreign_key "parties", "parties", column: "source_party_id"
|
||||||
add_foreign_key "parties", "raids"
|
add_foreign_key "parties", "raids"
|
||||||
add_foreign_key "parties", "users"
|
add_foreign_key "parties", "users"
|
||||||
add_foreign_key "raids", "raid_groups", column: "group_id"
|
add_foreign_key "raids", "raid_groups", column: "group_id", name: "raids_group_id_fkey"
|
||||||
add_foreign_key "weapon_awakenings", "awakenings"
|
add_foreign_key "weapon_awakenings", "awakenings"
|
||||||
add_foreign_key "weapon_awakenings", "weapons"
|
add_foreign_key "weapon_awakenings", "weapons"
|
||||||
end
|
end
|
||||||
|
|
|
||||||
111
sig/api/v1/parties_controller.rbs
Normal file
111
sig/api/v1/parties_controller.rbs
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
module Api
|
||||||
|
module V1
|
||||||
|
class PartiesController < Api::V1::ApiController
|
||||||
|
@parties: ActiveRecord::Relation[Party]
|
||||||
|
|
||||||
|
MAX_CHARACTERS: Integer
|
||||||
|
MAX_SUMMONS: Integer
|
||||||
|
MAX_WEAPONS: Integer
|
||||||
|
DEFAULT_MIN_CHARACTERS: Integer
|
||||||
|
DEFAULT_MIN_SUMMONS: Integer
|
||||||
|
DEFAULT_MIN_WEAPONS: Integer
|
||||||
|
DEFAULT_MAX_CLEAR_TIME: Integer
|
||||||
|
|
||||||
|
def create: () -> void
|
||||||
|
|
||||||
|
def show: () -> void
|
||||||
|
|
||||||
|
def update: () -> void
|
||||||
|
|
||||||
|
def destroy: () -> void
|
||||||
|
|
||||||
|
def remix: () -> void
|
||||||
|
|
||||||
|
def index: () -> void
|
||||||
|
|
||||||
|
def favorites: () -> void
|
||||||
|
|
||||||
|
def preview: () -> void
|
||||||
|
|
||||||
|
def preview_status: () -> void
|
||||||
|
|
||||||
|
def regenerate_preview: () -> void
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def authorize: () -> void
|
||||||
|
|
||||||
|
def grid_table_and_object_table: (String) -> [String?, String?]
|
||||||
|
|
||||||
|
def not_owner: () -> bool
|
||||||
|
|
||||||
|
def schedule_preview_generation: () -> void
|
||||||
|
|
||||||
|
def apply_filters: (ActiveRecord::Relation[Party]) -> ActiveRecord::Relation[Party]
|
||||||
|
|
||||||
|
def apply_privacy_settings: (ActiveRecord::Relation[Party]) -> ActiveRecord::Relation[Party]
|
||||||
|
|
||||||
|
def apply_includes: (ActiveRecord::Relation[Party], String) -> ActiveRecord::Relation[Party]
|
||||||
|
|
||||||
|
def apply_excludes: (ActiveRecord::Relation[Party], String) -> ActiveRecord::Relation[Party]
|
||||||
|
|
||||||
|
def build_filters: () -> Hash[Symbol, untyped]
|
||||||
|
|
||||||
|
def build_date_range: () -> Range[DateTime]?
|
||||||
|
|
||||||
|
def build_start_time: (String?) -> DateTime?
|
||||||
|
|
||||||
|
def build_count: (String?, Integer) -> Integer
|
||||||
|
|
||||||
|
def build_max_clear_time: (String?) -> Integer
|
||||||
|
|
||||||
|
def build_element: (String?) -> Integer?
|
||||||
|
|
||||||
|
def build_option: (String?) -> Integer?
|
||||||
|
|
||||||
|
def build_query: (Hash[Symbol, untyped], bool) -> ActiveRecord::Relation[Party]
|
||||||
|
|
||||||
|
def fetch_parties: (ActiveRecord::Relation[Party]) -> ActiveRecord::Relation[Party]
|
||||||
|
|
||||||
|
def calculate_count: (ActiveRecord::Relation[Party]) -> Integer
|
||||||
|
|
||||||
|
def calculate_total_pages: (Integer) -> Integer
|
||||||
|
|
||||||
|
def paginate_parties: (
|
||||||
|
ActiveRecord::Relation[Party],
|
||||||
|
?page: Integer?,
|
||||||
|
?per_page: Integer
|
||||||
|
) -> ActiveRecord::Relation[Party]
|
||||||
|
|
||||||
|
def excluded_characters: () -> ActiveRecord::Relation[GridCharacter]?
|
||||||
|
|
||||||
|
def excluded_summons: () -> ActiveRecord::Relation[GridSummon]?
|
||||||
|
|
||||||
|
def excluded_weapons: () -> ActiveRecord::Relation[GridWeapon]?
|
||||||
|
|
||||||
|
def render_party_json: (ActiveRecord::Relation[Party]) -> void
|
||||||
|
|
||||||
|
def privacy: (?favorites: bool) -> String?
|
||||||
|
|
||||||
|
def user_quality: () -> String?
|
||||||
|
|
||||||
|
def name_quality: () -> String?
|
||||||
|
|
||||||
|
def original: () -> String?
|
||||||
|
|
||||||
|
def includes: (String) -> String
|
||||||
|
|
||||||
|
def excludes: (String) -> String
|
||||||
|
|
||||||
|
def id_to_table: (String) -> String
|
||||||
|
|
||||||
|
def remixed_name: (String) -> String
|
||||||
|
|
||||||
|
def set_from_slug: () -> void
|
||||||
|
|
||||||
|
def set: () -> void
|
||||||
|
|
||||||
|
def party_params: () -> Hash[Symbol, untyped]?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Reference in a new issue