Add support for inclusions/exclusions (#124)

* Remove ap call

* Fix remix render method

* 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.

* Remove ap call and unused code

* Add granblue.team to cors

This works now!

* 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

* Update grid_summons_controller.rb

Set the proper uncap level for transcended summons

* Search is broken in Japanese!

* Grid model object updates

* Adds has_one association to canonical objects
* GridWeapon is_mainhand refactored
* GridCharacter add_awakening refactored

* (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.

* Update multisearch for exclusions

* Add nicknames to the index for multisearchable
This commit is contained in:
Justin Edmund 2023-08-21 20:01:15 -07:00 committed by GitHub
parent 4ee7a3a644
commit d2c5455120
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 680 additions and 581 deletions

View file

@ -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)

View file

@ -30,20 +30,26 @@ module Api
end
def search_all_en
PgSearch.multisearch_options = { using: TRIGRAM }
results = PgSearch.multisearch(search_params[:query]).limit(10)
query = search_params[:query]
exclude = search_params[:exclude]
if (results.length < 5) && (search_params[:query].length >= 2)
PgSearch.multisearch_options = { using: TRIGRAM }
results = PgSearch.multisearch(query).where.not(granblue_id: exclude).limit(10)
if (results.length < 5) && (query.length >= 2)
PgSearch.multisearch_options = { using: TSEARCH_WITH_PREFIX }
results = PgSearch.multisearch(search_params[:query]).limit(10)
results = PgSearch.multisearch(query).where.not(granblue_id: exclude).limit(10)
end
results
end
def search_all_ja
query = search_params[:query]
exclude = search_params[:exclude]
PgSearch.multisearch_options = { using: TSEARCH_WITH_PREFIX }
PgSearch.multisearch(search_params[:query]).limit(10)
PgSearch.multisearch(query).where.not(granblue_id: exclude).limit(10)
end
def characters

View file

@ -8,6 +8,8 @@ class Character < ApplicationRecord
{
name_en: character.name_en,
name_jp: character.name_jp,
nicknames_en: character.nicknames_en,
nicknames_jp: character.nicknames_jp,
granblue_id: character.granblue_id,
element: character.element
}

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -8,6 +8,8 @@ class Summon < ApplicationRecord
{
name_en: summon.name_en,
name_jp: summon.name_jp,
nicknames_en: summon.nicknames_en,
nicknames_jp: summon.nicknames_jp,
granblue_id: summon.granblue_id,
element: summon.element
}

View file

@ -8,6 +8,8 @@ class Weapon < ApplicationRecord
{
name_en: weapon.name_en,
name_jp: weapon.name_jp,
nicknames_en: weapon.nicknames_en,
nicknames_jp: weapon.nicknames_jp,
granblue_id: weapon.granblue_id,
element: weapon.element
}

File diff suppressed because it is too large Load diff