Add Item.is{_not}_modeled scopes, for use in search later

We're now caching `predicted_fully_modeled?` on the database record, so
we can query by it in the database!

I'm moving on from the model I did in Impress 2020, of writing really
big fancy single-source-of-truth queries based on the assets themselves.

I see the merit of that in terms of theoretical reliability, but in
practice I think it will be *more* reliable to have one *in-code*
definition of modeling status (which we need anyway for generating the
homepage modeling requests), and just save that in a queryable way.
This commit is contained in:
Emi Matchu 2024-11-19 15:52:52 -08:00
parent 39bed6b157
commit f6f618c9d5
5 changed files with 45 additions and 13 deletions

View file

@ -52,7 +52,8 @@ class OutfitsController < ApplicationController
newest_items = Item.newest. newest_items = Item.newest.
select(:id, :name, :updated_at, :thumbnail_url, :rarity_index, select(:id, :name, :updated_at, :thumbnail_url, :rarity_index,
:is_manually_nc, :cached_compatible_body_ids) :is_manually_nc, :cached_compatible_body_ids,
:cached_predicted_fully_modeled)
.limit(18) .limit(18)
@newest_modeled_items, @newest_unmodeled_items = @newest_modeled_items, @newest_unmodeled_items =
newest_items.partition(&:predicted_fully_modeled?) newest_items.partition(&:predicted_fully_modeled?)

View file

@ -65,6 +65,12 @@ class Item < ApplicationRecord
where('description NOT LIKE ?', where('description NOT LIKE ?',
'%' + sanitize_sql_like(PAINTBRUSH_SET_DESCRIPTION) + '%') '%' + sanitize_sql_like(PAINTBRUSH_SET_DESCRIPTION) + '%')
} }
scope :is_modeled, -> {
where(cached_predicted_fully_modeled: true)
}
scope :is_not_modeled, -> {
where(cached_predicted_fully_modeled: false)
}
scope :occupies, ->(zone_label) { scope :occupies, ->(zone_label) {
Zone.matching_label(zone_label). Zone.matching_label(zone_label).
map { |z| occupies_zone_id(z.id) }.reduce(none, &:or) map { |z| occupies_zone_id(z.id) }.reduce(none, &:or)
@ -263,12 +269,19 @@ class Item < ApplicationRecord
end end
def update_cached_fields def update_cached_fields
# Reload our associations, so they include any new records. # First, clear out some cached instance variables we use for performance,
# to ensure we recompute the latest values.
@predicted_body_ids = nil
@predicted_missing_body_ids = nil
# We also need to reload our associations, so they include any new records.
swf_assets.reload swf_assets.reload
# Then, compute and save our cached fields. # Finally, compute and save our cached fields.
self.cached_occupied_zone_ids = occupied_zone_ids self.cached_occupied_zone_ids = occupied_zone_ids
self.cached_compatible_body_ids = compatible_body_ids(use_cached: false) self.cached_compatible_body_ids = compatible_body_ids(use_cached: false)
self.cached_predicted_fully_modeled =
predicted_fully_modeled?(use_cached: false)
self.save! self.save!
end end
@ -387,7 +400,8 @@ class Item < ApplicationRecord
body_ids_by_species_by_color body_ids_by_species_by_color
end end
def predicted_fully_modeled? def predicted_fully_modeled?(use_cached: true)
return cached_predicted_fully_modeled? if use_cached
predicted_missing_body_ids.empty? predicted_missing_body_ids.empty?
end end

View file

@ -0,0 +1,16 @@
class AddCachedPredictedFullyModeledToItems < ActiveRecord::Migration[7.2]
def change
add_column :items, :cached_predicted_fully_modeled, :boolean,
default: false, null: false
reversible do |direction|
direction.up do
puts "Updating cached item fields for all items…"
Item.includes(:swf_assets).find_in_batches.with_index do |items, batch|
puts "Updating item batch ##{batch+1}"
items.each(&:update_cached_fields)
end
end
end
end
end

View file

@ -10,7 +10,7 @@
# #
# 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[7.2].define(version: 2024_11_16_041926) do ActiveRecord::Schema[7.2].define(version: 2024_11_19_214543) do
create_table "alt_styles", charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t| create_table "alt_styles", charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
t.integer "species_id", null: false t.integer "species_id", null: false
t.integer "color_id", null: false t.integer "color_id", null: false
@ -139,6 +139,7 @@ ActiveRecord::Schema[7.2].define(version: 2024_11_16_041926) do
t.integer "dyeworks_base_item_id" t.integer "dyeworks_base_item_id"
t.string "cached_occupied_zone_ids", default: "" t.string "cached_occupied_zone_ids", default: ""
t.text "cached_compatible_body_ids", default: "" t.text "cached_compatible_body_ids", default: ""
t.boolean "cached_predicted_fully_modeled", default: false, null: false
t.index ["dyeworks_base_item_id"], name: "index_items_on_dyeworks_base_item_id" t.index ["dyeworks_base_item_id"], name: "index_items_on_dyeworks_base_item_id"
t.index ["modeling_status_hint", "created_at", "id"], name: "items_modeling_status_hint_and_created_at_and_id" t.index ["modeling_status_hint", "created_at", "id"], name: "items_modeling_status_hint_and_created_at_and_id"
t.index ["modeling_status_hint", "created_at"], name: "items_modeling_status_hint_and_created_at" t.index ["modeling_status_hint", "created_at"], name: "items_modeling_status_hint_and_created_at"

View file

@ -41,21 +41,21 @@ RSpec.describe Item do
it("predicts no more compatible bodies") do it("predicts no more compatible bodies") do
expect(item.predicted_missing_body_ids).to be_empty expect(item.predicted_missing_body_ids).to be_empty
end end
pending("appears in Item.is_modeled") do it("appears in Item.is_modeled") do
expect(Item.is_modeled.find(item.id)).to be_present expect(Item.is_modeled.find_by_id(item.id)).to be_present
end end
pending("does not appear in Item.is_not_modeled") do it("does not appear in Item.is_not_modeled") do
expect(Item.is_not_modeled.find(item.id)).to be_nil expect(Item.is_not_modeled.find_by_id(item.id)).to be_nil
end end
end end
shared_examples "a not-fully-modeled item" do shared_examples "a not-fully-modeled item" do
it("is not fully modeled") { should_not be_predicted_fully_modeled } it("is not fully modeled") { should_not be_predicted_fully_modeled }
pending("does not appear in Item.is_modeled") do it("does not appear in Item.is_modeled") do
expect(Item.is_modeled.find(item.id)).to be_nil expect(Item.is_modeled.find_by_id(item.id)).to be_nil
end end
pending("appears in in Item.is_not_modeled") do it("appears in Item.is_not_modeled") do
expect(Item.is_not_modeled.find(item.id)).to be_present expect(Item.is_not_modeled.find_by_id(item.id)).to be_present
end end
end end