From f154e898bc70d450168eacd6909092caca2e773e Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Tue, 4 Jul 2023 02:53:09 -0700 Subject: [PATCH 01/12] Remove ap call --- app/errors/api/v1/incompatible_skill_error.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/errors/api/v1/incompatible_skill_error.rb b/app/errors/api/v1/incompatible_skill_error.rb index 6168feb..6bd23dc 100644 --- a/app/errors/api/v1/incompatible_skill_error.rb +++ b/app/errors/api/v1/incompatible_skill_error.rb @@ -20,7 +20,6 @@ module Api end def to_hash - ap @data { message: message, code: code, From 197aad8a8d708de2758551a38ee1d16c601a5f2d Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Tue, 4 Jul 2023 02:53:24 -0700 Subject: [PATCH 02/12] Fix remix render method --- app/controllers/api/v1/parties_controller.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/v1/parties_controller.rb b/app/controllers/api/v1/parties_controller.rb index aa78acd..c841a26 100644 --- a/app/controllers/api/v1/parties_controller.rb +++ b/app/controllers/api/v1/parties_controller.rb @@ -22,6 +22,7 @@ module Api party = Party.new party.user = current_user if current_user party.attributes = party_params if party_params + ap party # unless party_params.empty? # party.attributes = party_params @@ -76,8 +77,8 @@ module Api new_party.local_id = party_params[:local_id] if !party_params.nil? if new_party.save - render json: PartyBlueprint.render(new_party, view: :created, root: :party, - meta: { remix: true }) + render json: PartyBlueprint.render(new_party, view: :created, root: :party), + status: :created else render_validation_error_response(new_party) end From 3ff89796d4e5e5fe2b907fbd56fac240f05c0b7f Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Tue, 4 Jul 2023 03:18:59 -0700 Subject: [PATCH 03/12] Downcase username on db end There was a bug where users with capital letters in their name could not access their profiles after we tried to make things case insensitive. --- app/controllers/api/v1/users_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/v1/users_controller.rb b/app/controllers/api/v1/users_controller.rb index 2f658a1..b1afff2 100644 --- a/app/controllers/api/v1/users_controller.rb +++ b/app/controllers/api/v1/users_controller.rb @@ -167,11 +167,11 @@ module Api # Specify whitelisted properties that can be modified. def set - @user = User.where('username = ?', params[:id].downcase).first + @user = User.find_by('lower(username) = ?', params[:id].downcase) end def set_by_id - @user = User.where('id = ?', params[:id]).first + @user = User.find_by('id = ?', params[:id]) end def user_params From 56bb2ccf03ce63269fedadd501b2130934025fc1 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Tue, 4 Jul 2023 03:19:05 -0700 Subject: [PATCH 04/12] Remove ap call and unused code --- app/controllers/api/v1/parties_controller.rb | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/app/controllers/api/v1/parties_controller.rb b/app/controllers/api/v1/parties_controller.rb index c841a26..1cc1965 100644 --- a/app/controllers/api/v1/parties_controller.rb +++ b/app/controllers/api/v1/parties_controller.rb @@ -22,20 +22,6 @@ module Api party = Party.new party.user = current_user if current_user party.attributes = party_params if party_params - ap party - - # unless party_params.empty? - # party.attributes = party_params - # - # # TODO: Extract this into a different method - # job = Job.find(party_params['job_id']) if party_params['job_id'].present? - # if job - # job_skills = JobSkill.where(job: job.id, main: true) - # job_skills.each_with_index do |skill, index| - # party["skill#{index}_id"] = skill.id - # end - # end - # end if party.save! return render json: PartyBlueprint.render(party, view: :created, root: :party), From 193b1b7b2dbedd4742844b0222588e91791c921d Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Wed, 5 Jul 2023 13:06:42 -0700 Subject: [PATCH 05/12] Add granblue.team to cors This works now! --- config/initializers/cors.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/initializers/cors.rb b/config/initializers/cors.rb index 7d7e544..339a4e9 100644 --- a/config/initializers/cors.rb +++ b/config/initializers/cors.rb @@ -8,13 +8,13 @@ Rails.application.config.middleware.insert_before 0, Rack::Cors do allow do if Rails.env.production? - origins %w[app.granblue.team hensei-web-production.up.railway.app] + origins %w[granblue.team app.granblue.team hensei-web-production.up.railway.app] else origins %w[staging.granblue.team 127.0.0.1:1234] end - resource "*", + resource '*', headers: :any, - methods: [:get, :post, :put, :patch, :delete, :options, :head] + methods: %i[get post put patch delete options head] end end From c0b2c9502fcf0b5cc1643ea6e394c1e297e9e977 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Wed, 5 Jul 2023 21:19:48 -0700 Subject: [PATCH 06/12] Implement all-entity search to support tagging objects (#117) * Add table for multisearch * Add new route for searching all entities * Make models multisearchable We're going to start with Character, Summon, Weapon and Jobs * Add method to Search controller This will search with trigram first, and then if there aren't enough results, search with prefixed text search * Add support for Japanese all-entity search --- app/blueprints/api/v1/search_blueprint.rb | 10 +++++ app/controllers/api/v1/search_controller.rb | 43 +++++++++++++++++++ app/models/character.rb | 10 +++++ app/models/job.rb | 12 ++++++ app/models/summon.rb | 10 +++++ app/models/weapon.rb | 10 +++++ config/routes.rb | 1 + ...230705065015_create_pg_search_documents.rb | 21 +++++++++ db/schema.rb | 15 ++++++- 9 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 app/blueprints/api/v1/search_blueprint.rb create mode 100644 db/migrate/20230705065015_create_pg_search_documents.rb diff --git a/app/blueprints/api/v1/search_blueprint.rb b/app/blueprints/api/v1/search_blueprint.rb new file mode 100644 index 0000000..f80531b --- /dev/null +++ b/app/blueprints/api/v1/search_blueprint.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Api + module V1 + class SearchBlueprint < Blueprinter::Base + identifier :searchable_id + fields :searchable_type, :granblue_id, :name_en, :name_jp, :element + end + end +end diff --git a/app/controllers/api/v1/search_controller.rb b/app/controllers/api/v1/search_controller.rb index b60c3d0..842863b 100644 --- a/app/controllers/api/v1/search_controller.rb +++ b/app/controllers/api/v1/search_controller.rb @@ -3,6 +3,49 @@ module Api module V1 class SearchController < Api::V1::ApiController + TRIGRAM = { + trigram: { + threshold: 0.3 + } + }.freeze + + TSEARCH_WITH_PREFIX = { + tsearch: { + prefix: true, + dictionary: 'simple' + } + }.freeze + + def all + locale = search_params[:locale] || 'en' + + case locale + when 'en' + results = search_all_en + when 'ja' + results = search_all_ja + end + + render json: SearchBlueprint.render(results, root: :results) + end + + def search_all_en + PgSearch.multisearch_options = { using: TRIGRAM } + results = PgSearch.multisearch(search_params[:query]).limit(10) + + if (results.length < 5) && (search_params[:query].length >= 2) + PgSearch.multisearch_options = { using: TSEARCH_WITH_PREFIX } + results = PgSearch.multisearch(search_params[:query]).limit(10) + end + + results + end + + def search_all_ja + PgSearch.multisearch_options = { using: TSEARCH_WITH_PREFIX } + PgSearch.multisearch(search_params[:query]).limit(10) + end + def characters filters = search_params[:filters] locale = search_params[:locale] || 'en' diff --git a/app/models/character.rb b/app/models/character.rb index 6bb30a6..b89d3a1 100644 --- a/app/models/character.rb +++ b/app/models/character.rb @@ -3,6 +3,16 @@ class Character < ApplicationRecord include PgSearch::Model + multisearchable against: %i[name_en name_jp], + additional_attributes: lambda { |character| + { + name_en: character.name_en, + name_jp: character.name_jp, + granblue_id: character.granblue_id, + element: character.element + } + } + pg_search_scope :en_search, against: :name_en, using: { diff --git a/app/models/job.rb b/app/models/job.rb index 7cb8d6b..52ce307 100644 --- a/app/models/job.rb +++ b/app/models/job.rb @@ -1,9 +1,21 @@ # frozen_string_literal: true class Job < ApplicationRecord + include PgSearch::Model + belongs_to :party has_many :skills, class_name: 'JobSkill' + multisearchable against: %i[name_en name_jp], + additional_attributes: lambda { |job| + { + name_en: job.name_en, + name_jp: job.name_jp, + granblue_id: job.granblue_id, + element: 0 + } + } + belongs_to :base_job, foreign_key: 'base_job_id', class_name: 'Job', diff --git a/app/models/summon.rb b/app/models/summon.rb index cd9245a..59ce773 100644 --- a/app/models/summon.rb +++ b/app/models/summon.rb @@ -3,6 +3,16 @@ class Summon < ApplicationRecord include PgSearch::Model + multisearchable against: %i[name_en name_jp], + additional_attributes: lambda { |summon| + { + name_en: summon.name_en, + name_jp: summon.name_jp, + granblue_id: summon.granblue_id, + element: summon.element + } + } + pg_search_scope :en_search, against: :name_en, using: { diff --git a/app/models/weapon.rb b/app/models/weapon.rb index 9aedca3..fde26d2 100644 --- a/app/models/weapon.rb +++ b/app/models/weapon.rb @@ -3,6 +3,16 @@ class Weapon < ApplicationRecord include PgSearch::Model + multisearchable against: %i[name_en name_jp], + additional_attributes: lambda { |weapon| + { + name_en: weapon.name_en, + name_jp: weapon.name_jp, + granblue_id: weapon.granblue_id, + element: weapon.element + } + } + pg_search_scope :en_search, against: :name_en, using: { diff --git a/config/routes.rb b/config/routes.rb index 5864e34..9e13ee8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -31,6 +31,7 @@ Rails.application.routes.draw do post 'check/email', to: 'users#check_email' post 'check/username', to: 'users#check_username' + post 'search', to: 'search#all' post 'search/characters', to: 'search#characters' post 'search/weapons', to: 'search#weapons' post 'search/summons', to: 'search#summons' diff --git a/db/migrate/20230705065015_create_pg_search_documents.rb b/db/migrate/20230705065015_create_pg_search_documents.rb new file mode 100644 index 0000000..ce81c4f --- /dev/null +++ b/db/migrate/20230705065015_create_pg_search_documents.rb @@ -0,0 +1,21 @@ +class CreatePgSearchDocuments < ActiveRecord::Migration[7.0] + def up + say_with_time('Creating table for pg_search multisearch') do + create_table :pg_search_documents do |t| + t.text :content + t.string :granblue_id + t.string :name_en + t.string :name_jp + t.integer :element + t.belongs_to :searchable, type: :uuid, polymorphic: true, index: true + t.timestamps null: false + end + end + end + + def down + say_with_time('Dropping table for pg_search multisearch') do + drop_table :pg_search_documents + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 0dd39f7..6abca66 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_07_02_035508) do +ActiveRecord::Schema[7.0].define(version: 2023_07_05_065015) do # These are extensions that must be enabled in order to support this database enable_extension "btree_gin" enable_extension "pg_trgm" @@ -361,6 +361,19 @@ ActiveRecord::Schema[7.0].define(version: 2023_07_02_035508) do t.index ["user_id"], name: "index_parties_on_user_id" end + create_table "pg_search_documents", force: :cascade do |t| + t.text "content" + t.string "granblue_id" + t.string "name_en" + t.string "name_jp" + t.integer "element" + t.string "searchable_type" + t.uuid "searchable_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["searchable_type", "searchable_id"], name: "index_pg_search_documents_on_searchable" + end + create_table "raid_groups", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.string "name_en", null: false t.string "name_jp", null: false From 645fc07327d93bf02af2b1f4cfbabce9ceb90a3f Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Thu, 6 Jul 2023 15:54:29 -0700 Subject: [PATCH 07/12] Update grid_summons_controller.rb Set the proper uncap level for transcended summons --- app/controllers/api/v1/grid_summons_controller.rb | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/controllers/api/v1/grid_summons_controller.rb b/app/controllers/api/v1/grid_summons_controller.rb index 156fcd6..d4a6416 100644 --- a/app/controllers/api/v1/grid_summons_controller.rb +++ b/app/controllers/api/v1/grid_summons_controller.rb @@ -32,16 +32,18 @@ module Api def update_uncap_level summon = @summon.summon - max_uncap_level = if summon.flb && !summon.ulb + max_uncap_level = if summon.flb && !summon.ulb && !summon.xlb 4 - elsif summon.ulb + elsif summon.ulb && !summon.xlb 5 + elsif summon.xlb + 6 else 3 end greater_than_max_uncap = summon_params[:uncap_level].to_i > max_uncap_level - can_be_transcended = summon.xlb && summon_params[:transcendence_step] && summon_params[:transcendence_step]&.to_i.positive? + can_be_transcended = summon.xlb && summon_params[:transcendence_step] && summon_params[:transcendence_step]&.to_i&.positive? uncap_level = if greater_than_max_uncap || can_be_transcended max_uncap_level @@ -130,8 +132,8 @@ module Api def render_grid_summon_view(grid_summon, conflict_position = nil) GridSummonBlueprint.render(grid_summon, view: :nested, - root: :grid_summon, - meta: { replaced: conflict_position }) + root: :grid_summon, + meta: { replaced: conflict_position }) end def authorize From a82e1512c24bbe1469862de7da66fd6166c5da6f Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Thu, 6 Jul 2023 18:07:16 -0700 Subject: [PATCH 08/12] Search is broken in Japanese! --- app/controllers/api/v1/search_controller.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/v1/search_controller.rb b/app/controllers/api/v1/search_controller.rb index 842863b..d18b69d 100644 --- a/app/controllers/api/v1/search_controller.rb +++ b/app/controllers/api/v1/search_controller.rb @@ -67,7 +67,7 @@ module Api characters = if search_params[:query].present? && search_params[:query].length >= 2 if locale == 'ja' - Character.jp_search(search_params[:query]).where(conditions) + Character.ja_search(search_params[:query]).where(conditions) else Character.en_search(search_params[:query]).where(conditions) end @@ -105,7 +105,7 @@ module Api weapons = if search_params[:query].present? && search_params[:query].length >= 2 if locale == 'ja' - Weapon.jp_search(search_params[:query]).where(conditions) + Weapon.ja_search(search_params[:query]).where(conditions) else Weapon.en_search(search_params[:query]).where(conditions) end @@ -138,7 +138,7 @@ module Api summons = if search_params[:query].present? && search_params[:query].length >= 2 if locale == 'ja' - Summon.jp_search(search_params[:query]).where(conditions) + Summon.ja_search(search_params[:query]).where(conditions) else Summon.en_search(search_params[:query]).where(conditions) end From 70e820b781884792b0f3d9b485a0d01d24e485a1 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Sun, 9 Jul 2023 22:38:01 -0700 Subject: [PATCH 09/12] Grid model object updates * Adds has_one association to canonical objects * GridWeapon is_mainhand refactored * GridCharacter add_awakening refactored --- app/models/grid_character.rb | 8 +++++--- app/models/grid_summon.rb | 2 ++ app/models/grid_weapon.rb | 8 +++----- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/app/models/grid_character.rb b/app/models/grid_character.rb index 09d6e8d..6fc5783 100644 --- a/app/models/grid_character.rb +++ b/app/models/grid_character.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class GridCharacter < ApplicationRecord + has_one :object, class_name: 'Character', foreign_key: :id, primary_key: :character_id + belongs_to :awakening, optional: true belongs_to :party, counter_cache: :characters_count, @@ -87,9 +89,9 @@ class GridCharacter < ApplicationRecord private def add_awakening - if self.awakening.nil? - self.awakening = Awakening.where(slug: "character-balanced").sole - end + return unless awakening.nil? + + self.awakening = Awakening.where(slug: 'character-balanced').sole end def check_value(property, type) diff --git a/app/models/grid_summon.rb b/app/models/grid_summon.rb index c62a33b..f2a1afc 100644 --- a/app/models/grid_summon.rb +++ b/app/models/grid_summon.rb @@ -5,6 +5,7 @@ class GridSummon < ApplicationRecord counter_cache: :summons_count, inverse_of: :summons validates_presence_of :party + has_one :object, class_name: 'Summon', foreign_key: :id, primary_key: :summon_id validate :compatible_with_position, on: :create validate :no_conflicts, on: :create @@ -23,6 +24,7 @@ class GridSummon < ApplicationRecord party.summons.find do |grid_summon| return unless grid_summon.id + grid_summon if summon.id == grid_summon.summon.id end end diff --git a/app/models/grid_weapon.rb b/app/models/grid_weapon.rb index 459e450..ab8e3fd 100644 --- a/app/models/grid_weapon.rb +++ b/app/models/grid_weapon.rb @@ -6,6 +6,8 @@ class GridWeapon < ApplicationRecord inverse_of: :weapons 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_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 @@ -78,10 +80,6 @@ class GridWeapon < ApplicationRecord # Checks if the weapon should be a mainhand before saving the model def is_mainhand - if self.position == -1 - self.mainhand = true - else - self.mainhand = false - end + self.mainhand = position == -1 end end From b5f9889c00a4d99951386af07d44a973cc39980f Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Sun, 9 Jul 2023 22:39:10 -0700 Subject: [PATCH 10/12] (WIP) Add support for inclusion/exclusion + refactor This commit adds basic support for including/excluding objects from collection filters. There is also a refactor of the filter logic as a whole. It is only implemented in `teams` for now and is a work in progress. --- app/controllers/api/v1/parties_controller.rb | 261 ++++++++++++------- 1 file changed, 173 insertions(+), 88 deletions(-) diff --git a/app/controllers/api/v1/parties_controller.rb b/app/controllers/api/v1/parties_controller.rb index 1cc1965..ef0b048 100644 --- a/app/controllers/api/v1/parties_controller.rb +++ b/app/controllers/api/v1/parties_controller.rb @@ -60,7 +60,7 @@ module Api remix: true } - new_party.local_id = party_params[:local_id] if !party_params.nil? + new_party.local_id = party_params[:local_id] unless party_params.nil? if new_party.save render json: PartyBlueprint.render(new_party, view: :created, root: :party), @@ -71,58 +71,34 @@ module Api end def index - conditions = build_conditions + conditions = build_filters - @parties = Party.joins(:weapons) - .group('parties.id') - .having('count(distinct grid_weapons.weapon_id) > 2') - .where(conditions) - .where(name_quality) - .where(user_quality) - .where(original) - .order(created_at: :desc) - .paginate(page: request.params[:page], per_page: COLLECTION_PER_PAGE) - .each { |party| party.favorited = current_user ? party.is_favorited(current_user) : false } + query = build_query(conditions) + query = apply_includes(query, params[:includes]) if params[:includes].present? + query = apply_excludes(query, params[:excludes]) if params[:excludes].present? - count = Party.where(conditions).count - total_pages = count.to_f / COLLECTION_PER_PAGE > 1 ? (count.to_f / COLLECTION_PER_PAGE).ceil : 1 + @parties = fetch_parties(query) + count = calculate_count(query) + total_pages = calculate_total_pages(count) - render json: PartyBlueprint.render(@parties, - view: :collection, - root: :results, - meta: { - count: count, - total_pages: total_pages, - per_page: COLLECTION_PER_PAGE - }) + render_party_json(@parties, count, total_pages) end def favorites raise Api::V1::UnauthorizedError unless current_user - conditions = build_conditions + conditions = build_filters conditions[:favorites] = { user_id: current_user.id } - @parties = Party.joins(:favorites) - .where(conditions) - .where(name_quality) - .where(user_quality) - .where(original) - .order('favorites.created_at DESC') - .paginate(page: request.params[:page], per_page: COLLECTION_PER_PAGE) - .each { |party| party.favorited = party.is_favorited(current_user) } + query = build_query(conditions) + query = apply_includes(query, params[:includes]) if params[:includes].present? + query = apply_excludes(query, params[:excludes]) if params[:excludes].present? - count = Party.joins(:favorites).where(conditions).count - total_pages = count.to_f / COLLECTION_PER_PAGE > 1 ? (count.to_f / COLLECTION_PER_PAGE).ceil : 1 + @parties = fetch_parties(query) + count = calculate_count(query) + total_pages = calculate_total_pages(count) - render json: PartyBlueprint.render(@parties, - view: :collection, - root: :results, - meta: { - count: count, - total_pages: total_pages, - per_page: COLLECTION_PER_PAGE - }) + render_party_json(@parties, count, total_pages) end private @@ -131,70 +107,179 @@ module Api render_unauthorized_response if @party.user != current_user || @party.edit_key != edit_key end - def build_conditions + def build_filters params = request.params - unless params['recency'].blank? - start_time = (DateTime.current - params['recency'].to_i.seconds) - .to_datetime.beginning_of_day - end + start_time = build_start_time(params['recency']) - min_characters_count = params['characters_count'].blank? ? DEFAULT_MIN_CHARACTERS : params['characters_count'].to_i - min_summons_count = params['summons_count'].blank? ? DEFAULT_MIN_SUMMONS : params['summons_count'].to_i - min_weapons_count = params['weapons_count'].blank? ? DEFAULT_MIN_WEAPONS : params['weapons_count'].to_i - max_clear_time = params['max_clear_time'].blank? ? DEFAULT_MAX_CLEAR_TIME : params['max_clear_time'].to_i + min_characters_count = build_count(params['characters_count'], DEFAULT_MIN_CHARACTERS) + min_summons_count = build_count(params['summons_count'], DEFAULT_MIN_SUMMONS) + min_weapons_count = build_count(params['weapons_count'], DEFAULT_MIN_WEAPONS) + max_clear_time = build_max_clear_time(params['max_clear_time']) - {}.tap do |hash| - # Basic filters - hash[:element] = params['element'].to_i unless params['element'].blank? - hash[:raid] = params['raid'] unless params['raid'].blank? - hash[:created_at] = start_time..DateTime.current unless params['recency'].blank? - - # Advanced filters: Team parameters - hash[:full_auto] = params['full_auto'].to_i unless params['full_auto'].blank? || params['full_auto'].to_i == -1 - hash[:auto_guard] = params['auto_guard'].to_i unless params['auto_guard'].blank? || params['auto_guard'].to_i == -1 - hash[:charge_attack] = params['charge_attack'].to_i unless params['charge_attack'].blank? || params['charge_attack'].to_i == -1 - - # Turn count of 0 will not be displayed, so disallow on the frontend or set default to 1 - # How do we do the same for button count since that can reasonably be 1? - # hash[:turn_count] = params['turn_count'].to_i unless params['turn_count'].blank? || params['turn_count'].to_i <= 0 - # hash[:button_count] = params['button_count'].to_i unless params['button_count'].blank? - # hash[:clear_time] = 0..max_clear_time - - # Advanced filters: Object counts - hash[:characters_count] = min_characters_count..MAX_CHARACTERS - hash[:summons_count] = min_summons_count..MAX_SUMMONS - hash[:weapons_count] = min_weapons_count..MAX_WEAPONS - end + { + 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 - def original - "source_party_id IS NULL" unless request.params['original'].blank? || request.params['original'] == "false" + def build_start_time(recency) + return unless recency.present? + + (DateTime.current - recency.to_i.seconds).to_datetime.beginning_of_day + end + + def build_count(value, default) + value.blank? ? default : value.to_i + end + + def build_max_clear_time(value) + value.blank? ? DEFAULT_MAX_CLEAR_TIME : value.to_i + end + + def build_element(element) + element.to_i unless element.blank? + end + + def build_option(value) + value.to_i unless value.blank? || value.to_i == -1 + end + + def build_query(conditions) + Party.distinct + .joins(weapons: [:object], summons: [:object], characters: [:object]) + .group('parties.id') + .where(conditions) + .where(name_quality) + .where(user_quality) + .where(original) + end + + def includes(id) + "(\"#{id_to_table(id)}\".\"granblue_id\" = '#{id}')" + end + + def excludes(id) + "(\"#{id_to_table(id)}\".\"granblue_id\" != '#{id}')" + end + + def apply_includes(query, includes) + included = includes.split(',') + includes_condition = included.map { |id| includes(id) }.join(' AND ') + query.where(includes_condition) + end + + def apply_excludes(query, _excludes) + characters_subquery = excluded_characters.select(1).arel + summons_subquery = excluded_summons.select(1).arel + weapons_subquery = excluded_weapons.select(1).arel + + query.where(characters_subquery.exists.not) + .where(weapons_subquery.exists.not) + .where(summons_subquery.exists.not) + end + + def excluded_characters + return unless params[:excludes] + + excluded = params[:excludes].split(',').filter { |id| id[0] == '3' } + GridCharacter.joins(:object) + .where(characters: { granblue_id: excluded }) + .where('grid_characters.party_id = parties.id') + end + + def excluded_summons + return unless params[:excludes] + + excluded = params[:excludes].split(',').filter { |id| id[0] == '2' } + GridSummon.joins(:object) + .where(summons: { granblue_id: excluded }) + .where('grid_summons.party_id = parties.id') + end + + def excluded_weapons + return unless params[:excludes] + + excluded = params[:excludes].split(',').filter { |id| id[0] == '1' } + GridWeapon.joins(:object) + .where(weapons: { granblue_id: excluded }) + .where('grid_weapons.party_id = parties.id') + end + + def fetch_parties(query) + query.order(created_at: :desc) + .paginate(page: request.params[:page], per_page: COLLECTION_PER_PAGE) + .each { |party| party.favorited = current_user ? party.is_favorited(current_user) : false } + end + + def calculate_count(query) + query.count.values.sum + end + + def calculate_total_pages(count) + count.to_f / COLLECTION_PER_PAGE > 1 ? (count.to_f / COLLECTION_PER_PAGE).ceil : 1 + end + + def render_party_json(parties, count, total_pages) + render json: PartyBlueprint.render(parties, + view: :collection, + root: :results, + meta: { + count: count, + total_pages: total_pages, + per_page: COLLECTION_PER_PAGE + }) end def user_quality - "user_id IS NOT NULL" unless request.params[:user_quality].blank? || request.params[:user_quality] == "false" + 'user_id IS NOT NULL' unless request.params[:user_quality].blank? || request.params[:user_quality] == 'false' end def name_quality low_quality = [ - "Untitled", - "Remix of Untitled", - "Remix of Remix of Untitled", - "Remix of Remix of Remix of Untitled", - "Remix of Remix of Remix of Remix of Untitled", - "Remix of Remix of Remix of Remix of Remix of Untitled", - "無題", - "無題のリミックス", - "無題のリミックスのリミックス", - "無題のリミックスのリミックスのリミックス", - "無題のリミックスのリミックスのリミックスのリミックス", - "無題のリミックスのリミックスのリミックスのリミックスのリミックス" + 'Untitled', + 'Remix of Untitled', + 'Remix of Remix of Untitled', + 'Remix of Remix of Remix of Untitled', + 'Remix of Remix of Remix of Remix of Untitled', + 'Remix of Remix of Remix of Remix of Remix of Untitled', + '無題', + '無題のリミックス', + '無題のリミックスのリミックス', + '無題のリミックスのリミックスのリミックス', + '無題のリミックスのリミックスのリミックスのリミックス', + '無題のリミックスのリミックスのリミックスのリミックスのリミックス' ] joined_names = low_quality.map { |name| "'#{name}'" }.join(',') - "name NOT IN (#{joined_names})" unless request.params[:name_quality].blank? || request.params[:name_quality] == "false" + return if request.params[:name_quality].blank? || request.params[:name_quality] == 'false' + + "name NOT IN (#{joined_names})" + end + + def original + 'source_party_id IS NULL' unless request.params['original'].blank? || request.params['original'] == 'false' + end + + def id_to_table(id) + case id[0] + when '3' + table = 'characters' + when '2' + table = 'summons' + when '1' + table = 'weapons' + end + + table end def remixed_name(name) From 3f734a03897deb16fe4a661b575a45306535ff95 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Sun, 9 Jul 2023 22:39:59 -0700 Subject: [PATCH 11/12] Revert "Grid model object updates" This reverts commit 70e820b781884792b0f3d9b485a0d01d24e485a1. --- app/models/grid_character.rb | 8 +++----- app/models/grid_summon.rb | 2 -- app/models/grid_weapon.rb | 8 +++++--- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/app/models/grid_character.rb b/app/models/grid_character.rb index 6fc5783..09d6e8d 100644 --- a/app/models/grid_character.rb +++ b/app/models/grid_character.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class GridCharacter < ApplicationRecord - has_one :object, class_name: 'Character', foreign_key: :id, primary_key: :character_id - belongs_to :awakening, optional: true belongs_to :party, counter_cache: :characters_count, @@ -89,9 +87,9 @@ class GridCharacter < ApplicationRecord private def add_awakening - return unless awakening.nil? - - self.awakening = Awakening.where(slug: 'character-balanced').sole + if self.awakening.nil? + self.awakening = Awakening.where(slug: "character-balanced").sole + end end def check_value(property, type) diff --git a/app/models/grid_summon.rb b/app/models/grid_summon.rb index f2a1afc..c62a33b 100644 --- a/app/models/grid_summon.rb +++ b/app/models/grid_summon.rb @@ -5,7 +5,6 @@ class GridSummon < ApplicationRecord counter_cache: :summons_count, inverse_of: :summons validates_presence_of :party - has_one :object, class_name: 'Summon', foreign_key: :id, primary_key: :summon_id validate :compatible_with_position, on: :create validate :no_conflicts, on: :create @@ -24,7 +23,6 @@ class GridSummon < ApplicationRecord party.summons.find do |grid_summon| return unless grid_summon.id - grid_summon if summon.id == grid_summon.summon.id end end diff --git a/app/models/grid_weapon.rb b/app/models/grid_weapon.rb index ab8e3fd..459e450 100644 --- a/app/models/grid_weapon.rb +++ b/app/models/grid_weapon.rb @@ -6,8 +6,6 @@ class GridWeapon < ApplicationRecord inverse_of: :weapons 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_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 @@ -80,6 +78,10 @@ class GridWeapon < ApplicationRecord # Checks if the weapon should be a mainhand before saving the model def is_mainhand - self.mainhand = position == -1 + if self.position == -1 + self.mainhand = true + else + self.mainhand = false + end end end From 12544bd8ad8927b15d2e9c3d381d925f6349452a Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Sun, 9 Jul 2023 22:40:01 -0700 Subject: [PATCH 12/12] Revert "(WIP) Add support for inclusion/exclusion + refactor" This reverts commit b5f9889c00a4d99951386af07d44a973cc39980f. --- app/controllers/api/v1/parties_controller.rb | 293 +++++++------------ 1 file changed, 104 insertions(+), 189 deletions(-) diff --git a/app/controllers/api/v1/parties_controller.rb b/app/controllers/api/v1/parties_controller.rb index ef0b048..1cc1965 100644 --- a/app/controllers/api/v1/parties_controller.rb +++ b/app/controllers/api/v1/parties_controller.rb @@ -60,7 +60,7 @@ module Api remix: true } - new_party.local_id = party_params[:local_id] unless party_params.nil? + new_party.local_id = party_params[:local_id] if !party_params.nil? if new_party.save render json: PartyBlueprint.render(new_party, view: :created, root: :party), @@ -71,164 +71,23 @@ module Api end def index - conditions = build_filters + conditions = build_conditions - query = build_query(conditions) - query = apply_includes(query, params[:includes]) if params[:includes].present? - query = apply_excludes(query, params[:excludes]) if params[:excludes].present? + @parties = Party.joins(:weapons) + .group('parties.id') + .having('count(distinct grid_weapons.weapon_id) > 2') + .where(conditions) + .where(name_quality) + .where(user_quality) + .where(original) + .order(created_at: :desc) + .paginate(page: request.params[:page], per_page: COLLECTION_PER_PAGE) + .each { |party| party.favorited = current_user ? party.is_favorited(current_user) : false } - @parties = fetch_parties(query) - count = calculate_count(query) - total_pages = calculate_total_pages(count) + count = Party.where(conditions).count + total_pages = count.to_f / COLLECTION_PER_PAGE > 1 ? (count.to_f / COLLECTION_PER_PAGE).ceil : 1 - render_party_json(@parties, count, total_pages) - end - - def favorites - raise Api::V1::UnauthorizedError unless current_user - - conditions = build_filters - conditions[:favorites] = { user_id: current_user.id } - - query = build_query(conditions) - 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 - - private - - def authorize - render_unauthorized_response if @party.user != current_user || @party.edit_key != edit_key - end - - def build_filters - params = request.params - - start_time = build_start_time(params['recency']) - - min_characters_count = build_count(params['characters_count'], DEFAULT_MIN_CHARACTERS) - min_summons_count = build_count(params['summons_count'], DEFAULT_MIN_SUMMONS) - 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 - - def build_start_time(recency) - return unless recency.present? - - (DateTime.current - recency.to_i.seconds).to_datetime.beginning_of_day - end - - def build_count(value, default) - value.blank? ? default : value.to_i - end - - def build_max_clear_time(value) - value.blank? ? DEFAULT_MAX_CLEAR_TIME : value.to_i - end - - def build_element(element) - element.to_i unless element.blank? - end - - def build_option(value) - value.to_i unless value.blank? || value.to_i == -1 - end - - def build_query(conditions) - Party.distinct - .joins(weapons: [:object], summons: [:object], characters: [:object]) - .group('parties.id') - .where(conditions) - .where(name_quality) - .where(user_quality) - .where(original) - end - - def includes(id) - "(\"#{id_to_table(id)}\".\"granblue_id\" = '#{id}')" - end - - def excludes(id) - "(\"#{id_to_table(id)}\".\"granblue_id\" != '#{id}')" - end - - def apply_includes(query, includes) - included = includes.split(',') - includes_condition = included.map { |id| includes(id) }.join(' AND ') - query.where(includes_condition) - end - - def apply_excludes(query, _excludes) - characters_subquery = excluded_characters.select(1).arel - summons_subquery = excluded_summons.select(1).arel - weapons_subquery = excluded_weapons.select(1).arel - - query.where(characters_subquery.exists.not) - .where(weapons_subquery.exists.not) - .where(summons_subquery.exists.not) - end - - def excluded_characters - return unless params[:excludes] - - excluded = params[:excludes].split(',').filter { |id| id[0] == '3' } - GridCharacter.joins(:object) - .where(characters: { granblue_id: excluded }) - .where('grid_characters.party_id = parties.id') - end - - def excluded_summons - return unless params[:excludes] - - excluded = params[:excludes].split(',').filter { |id| id[0] == '2' } - GridSummon.joins(:object) - .where(summons: { granblue_id: excluded }) - .where('grid_summons.party_id = parties.id') - end - - def excluded_weapons - return unless params[:excludes] - - excluded = params[:excludes].split(',').filter { |id| id[0] == '1' } - GridWeapon.joins(:object) - .where(weapons: { granblue_id: excluded }) - .where('grid_weapons.party_id = parties.id') - end - - def fetch_parties(query) - query.order(created_at: :desc) - .paginate(page: request.params[:page], per_page: COLLECTION_PER_PAGE) - .each { |party| party.favorited = current_user ? party.is_favorited(current_user) : false } - end - - def calculate_count(query) - query.count.values.sum - end - - def calculate_total_pages(count) - count.to_f / COLLECTION_PER_PAGE > 1 ? (count.to_f / COLLECTION_PER_PAGE).ceil : 1 - end - - def render_party_json(parties, count, total_pages) - render json: PartyBlueprint.render(parties, + render json: PartyBlueprint.render(@parties, view: :collection, root: :results, meta: { @@ -238,48 +97,104 @@ module Api }) end + def favorites + raise Api::V1::UnauthorizedError unless current_user + + conditions = build_conditions + conditions[:favorites] = { user_id: current_user.id } + + @parties = Party.joins(:favorites) + .where(conditions) + .where(name_quality) + .where(user_quality) + .where(original) + .order('favorites.created_at DESC') + .paginate(page: request.params[:page], per_page: COLLECTION_PER_PAGE) + .each { |party| party.favorited = party.is_favorited(current_user) } + + count = Party.joins(:favorites).where(conditions).count + total_pages = count.to_f / COLLECTION_PER_PAGE > 1 ? (count.to_f / COLLECTION_PER_PAGE).ceil : 1 + + render json: PartyBlueprint.render(@parties, + view: :collection, + root: :results, + meta: { + count: count, + total_pages: total_pages, + per_page: COLLECTION_PER_PAGE + }) + end + + private + + def authorize + render_unauthorized_response if @party.user != current_user || @party.edit_key != edit_key + end + + def build_conditions + params = request.params + + unless params['recency'].blank? + start_time = (DateTime.current - params['recency'].to_i.seconds) + .to_datetime.beginning_of_day + end + + min_characters_count = params['characters_count'].blank? ? DEFAULT_MIN_CHARACTERS : params['characters_count'].to_i + min_summons_count = params['summons_count'].blank? ? DEFAULT_MIN_SUMMONS : params['summons_count'].to_i + min_weapons_count = params['weapons_count'].blank? ? DEFAULT_MIN_WEAPONS : params['weapons_count'].to_i + max_clear_time = params['max_clear_time'].blank? ? DEFAULT_MAX_CLEAR_TIME : params['max_clear_time'].to_i + + {}.tap do |hash| + # Basic filters + hash[:element] = params['element'].to_i unless params['element'].blank? + hash[:raid] = params['raid'] unless params['raid'].blank? + hash[:created_at] = start_time..DateTime.current unless params['recency'].blank? + + # Advanced filters: Team parameters + hash[:full_auto] = params['full_auto'].to_i unless params['full_auto'].blank? || params['full_auto'].to_i == -1 + hash[:auto_guard] = params['auto_guard'].to_i unless params['auto_guard'].blank? || params['auto_guard'].to_i == -1 + hash[:charge_attack] = params['charge_attack'].to_i unless params['charge_attack'].blank? || params['charge_attack'].to_i == -1 + + # Turn count of 0 will not be displayed, so disallow on the frontend or set default to 1 + # How do we do the same for button count since that can reasonably be 1? + # hash[:turn_count] = params['turn_count'].to_i unless params['turn_count'].blank? || params['turn_count'].to_i <= 0 + # hash[:button_count] = params['button_count'].to_i unless params['button_count'].blank? + # hash[:clear_time] = 0..max_clear_time + + # Advanced filters: Object counts + hash[:characters_count] = min_characters_count..MAX_CHARACTERS + hash[:summons_count] = min_summons_count..MAX_SUMMONS + hash[:weapons_count] = min_weapons_count..MAX_WEAPONS + end + end + + def original + "source_party_id IS NULL" unless request.params['original'].blank? || request.params['original'] == "false" + end + def user_quality - 'user_id IS NOT NULL' unless request.params[:user_quality].blank? || request.params[:user_quality] == 'false' + "user_id IS NOT NULL" unless request.params[:user_quality].blank? || request.params[:user_quality] == "false" end def name_quality low_quality = [ - 'Untitled', - 'Remix of Untitled', - 'Remix of Remix of Untitled', - 'Remix of Remix of Remix of Untitled', - 'Remix of Remix of Remix of Remix of Untitled', - 'Remix of Remix of Remix of Remix of Remix of Untitled', - '無題', - '無題のリミックス', - '無題のリミックスのリミックス', - '無題のリミックスのリミックスのリミックス', - '無題のリミックスのリミックスのリミックスのリミックス', - '無題のリミックスのリミックスのリミックスのリミックスのリミックス' + "Untitled", + "Remix of Untitled", + "Remix of Remix of Untitled", + "Remix of Remix of Remix of Untitled", + "Remix of Remix of Remix of Remix of Untitled", + "Remix of Remix of Remix of Remix of Remix of Untitled", + "無題", + "無題のリミックス", + "無題のリミックスのリミックス", + "無題のリミックスのリミックスのリミックス", + "無題のリミックスのリミックスのリミックスのリミックス", + "無題のリミックスのリミックスのリミックスのリミックスのリミックス" ] joined_names = low_quality.map { |name| "'#{name}'" }.join(',') - return if request.params[:name_quality].blank? || request.params[:name_quality] == 'false' - - "name NOT IN (#{joined_names})" - end - - def original - 'source_party_id IS NULL' unless request.params['original'].blank? || request.params['original'] == 'false' - end - - def id_to_table(id) - case id[0] - when '3' - table = 'characters' - when '2' - table = 'summons' - when '1' - table = 'weapons' - end - - table + "name NOT IN (#{joined_names})" unless request.params[:name_quality].blank? || request.params[:name_quality] == "false" end def remixed_name(name)