merge next-main, keep both modifier associations and orphan handling
This commit is contained in:
commit
4430331da9
19 changed files with 465 additions and 21 deletions
|
|
@ -3,7 +3,7 @@
|
||||||
module Api
|
module Api
|
||||||
module V1
|
module V1
|
||||||
class GridArtifactBlueprint < ApiBlueprint
|
class GridArtifactBlueprint < ApiBlueprint
|
||||||
fields :level, :reroll_slot
|
fields :level, :reroll_slot, :orphaned
|
||||||
|
|
||||||
field :collection_artifact_id
|
field :collection_artifact_id
|
||||||
field :out_of_sync, if: ->(_field, ga, _options) { ga.collection_artifact_id.present? } do |ga|
|
field :out_of_sync, if: ->(_field, ga, _options) { ga.collection_artifact_id.present? } do |ga|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
module Api
|
module Api
|
||||||
module V1
|
module V1
|
||||||
class GridSummonBlueprint < ApiBlueprint
|
class GridSummonBlueprint < ApiBlueprint
|
||||||
fields :main, :friend, :position, :quick_summon, :uncap_level, :transcendence_step
|
fields :main, :friend, :position, :quick_summon, :uncap_level, :transcendence_step, :orphaned
|
||||||
|
|
||||||
field :collection_summon_id
|
field :collection_summon_id
|
||||||
field :out_of_sync, if: ->(_field, gs, _options) { gs.collection_summon_id.present? } do |gs|
|
field :out_of_sync, if: ->(_field, gs, _options) { gs.collection_summon_id.present? } do |gs|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
module Api
|
module Api
|
||||||
module V1
|
module V1
|
||||||
class GridWeaponBlueprint < ApiBlueprint
|
class GridWeaponBlueprint < ApiBlueprint
|
||||||
fields :mainhand, :position, :uncap_level, :transcendence_step, :element
|
fields :mainhand, :position, :uncap_level, :transcendence_step, :element, :orphaned
|
||||||
|
|
||||||
field :collection_weapon_id
|
field :collection_weapon_id
|
||||||
field :out_of_sync, if: ->(_field, gw, _options) { gw.collection_weapon_id.present? } do |gw|
|
field :out_of_sync, if: ->(_field, gw, _options) { gw.collection_weapon_id.present? } do |gw|
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,10 @@ module Api
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
field :has_orphaned_items do |party|
|
||||||
|
party.has_orphaned_items?
|
||||||
|
end
|
||||||
|
|
||||||
# For collection views
|
# For collection views
|
||||||
view :preview do
|
view :preview do
|
||||||
include_view :preview_objects # Characters, Weapons, Summons
|
include_view :preview_objects # Characters, Weapons, Summons
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ module Api
|
||||||
before_action :set_collection_artifact_for_read, only: %i[show]
|
before_action :set_collection_artifact_for_read, only: %i[show]
|
||||||
|
|
||||||
# Write actions: require auth, use current_user
|
# Write actions: require auth, use current_user
|
||||||
before_action :restrict_access, only: %i[create update destroy batch batch_destroy import]
|
before_action :restrict_access, only: %i[create update destroy batch batch_destroy import preview_sync]
|
||||||
before_action :set_collection_artifact_for_write, only: %i[update destroy]
|
before_action :set_collection_artifact_for_write, only: %i[update destroy]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
|
@ -109,6 +109,8 @@ module Api
|
||||||
#
|
#
|
||||||
# @param data [Hash] Game data containing artifact list
|
# @param data [Hash] Game data containing artifact list
|
||||||
# @param update_existing [Boolean] Whether to update existing artifacts (default: false)
|
# @param update_existing [Boolean] Whether to update existing artifacts (default: false)
|
||||||
|
# @param is_full_inventory [Boolean] Whether this represents the user's complete inventory (default: false)
|
||||||
|
# @param reconcile_deletions [Boolean] Whether to delete items not in the import (default: false)
|
||||||
def import
|
def import
|
||||||
game_data = import_params[:data]
|
game_data = import_params[:data]
|
||||||
|
|
||||||
|
|
@ -119,7 +121,9 @@ module Api
|
||||||
service = ArtifactImportService.new(
|
service = ArtifactImportService.new(
|
||||||
current_user,
|
current_user,
|
||||||
game_data,
|
game_data,
|
||||||
update_existing: import_params[:update_existing] == true
|
update_existing: import_params[:update_existing] == true,
|
||||||
|
is_full_inventory: import_params[:is_full_inventory] == true,
|
||||||
|
reconcile_deletions: import_params[:reconcile_deletions] == true
|
||||||
)
|
)
|
||||||
|
|
||||||
result = service.import
|
result = service.import
|
||||||
|
|
@ -131,10 +135,41 @@ module Api
|
||||||
created: result.created&.size || 0,
|
created: result.created&.size || 0,
|
||||||
updated: result.updated&.size || 0,
|
updated: result.updated&.size || 0,
|
||||||
skipped: result.skipped&.size || 0,
|
skipped: result.skipped&.size || 0,
|
||||||
errors: result.errors || []
|
errors: result.errors || [],
|
||||||
|
reconciliation: result.reconciliation
|
||||||
}, status: status
|
}, status: status
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# POST /collection/artifacts/preview_sync
|
||||||
|
# Previews what would be deleted in a full sync operation
|
||||||
|
#
|
||||||
|
# @param data [Hash] Game data containing artifact list
|
||||||
|
# @return [JSON] List of items that would be deleted
|
||||||
|
def preview_sync
|
||||||
|
game_data = import_params[:data]
|
||||||
|
|
||||||
|
unless game_data.present?
|
||||||
|
return render json: { error: 'No data provided' }, status: :bad_request
|
||||||
|
end
|
||||||
|
|
||||||
|
service = ArtifactImportService.new(current_user, game_data)
|
||||||
|
items_to_delete = service.preview_deletions
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
will_delete: items_to_delete.map do |ca|
|
||||||
|
{
|
||||||
|
id: ca.id,
|
||||||
|
game_id: ca.game_id,
|
||||||
|
name: ca.artifact&.name_en,
|
||||||
|
granblue_id: ca.artifact&.granblue_id,
|
||||||
|
element: ca.element,
|
||||||
|
level: ca.level
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
count: items_to_delete.size
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
# DELETE /collection/artifacts/batch_destroy
|
# DELETE /collection/artifacts/batch_destroy
|
||||||
# Deletes multiple collection artifacts in a single request
|
# Deletes multiple collection artifacts in a single request
|
||||||
def batch_destroy
|
def batch_destroy
|
||||||
|
|
@ -197,6 +232,8 @@ module Api
|
||||||
def import_params
|
def import_params
|
||||||
{
|
{
|
||||||
update_existing: params[:update_existing],
|
update_existing: params[:update_existing],
|
||||||
|
is_full_inventory: params[:is_full_inventory],
|
||||||
|
reconcile_deletions: params[:reconcile_deletions],
|
||||||
data: params[:data]&.to_unsafe_h
|
data: params[:data]&.to_unsafe_h
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ module Api
|
||||||
before_action :set_collection_summon_for_read, only: %i[show]
|
before_action :set_collection_summon_for_read, only: %i[show]
|
||||||
|
|
||||||
# Write actions: require auth, use current_user
|
# Write actions: require auth, use current_user
|
||||||
before_action :restrict_access, only: %i[create update destroy batch batch_destroy import]
|
before_action :restrict_access, only: %i[create update destroy batch batch_destroy import preview_sync]
|
||||||
before_action :set_collection_summon_for_write, only: %i[update destroy]
|
before_action :set_collection_summon_for_write, only: %i[update destroy]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
|
@ -113,6 +113,8 @@ module Api
|
||||||
#
|
#
|
||||||
# @param data [Hash] Game data containing summon list
|
# @param data [Hash] Game data containing summon list
|
||||||
# @param update_existing [Boolean] Whether to update existing summons (default: false)
|
# @param update_existing [Boolean] Whether to update existing summons (default: false)
|
||||||
|
# @param is_full_inventory [Boolean] Whether this represents the user's complete inventory (default: false)
|
||||||
|
# @param reconcile_deletions [Boolean] Whether to delete items not in the import (default: false)
|
||||||
def import
|
def import
|
||||||
game_data = import_params[:data]
|
game_data = import_params[:data]
|
||||||
|
|
||||||
|
|
@ -123,7 +125,9 @@ module Api
|
||||||
service = SummonImportService.new(
|
service = SummonImportService.new(
|
||||||
current_user,
|
current_user,
|
||||||
game_data,
|
game_data,
|
||||||
update_existing: import_params[:update_existing] == true
|
update_existing: import_params[:update_existing] == true,
|
||||||
|
is_full_inventory: import_params[:is_full_inventory] == true,
|
||||||
|
reconcile_deletions: import_params[:reconcile_deletions] == true
|
||||||
)
|
)
|
||||||
|
|
||||||
result = service.import
|
result = service.import
|
||||||
|
|
@ -135,10 +139,41 @@ module Api
|
||||||
created: result.created.size,
|
created: result.created.size,
|
||||||
updated: result.updated.size,
|
updated: result.updated.size,
|
||||||
skipped: result.skipped.size,
|
skipped: result.skipped.size,
|
||||||
errors: result.errors
|
errors: result.errors,
|
||||||
|
reconciliation: result.reconciliation
|
||||||
}, status: status
|
}, status: status
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# POST /collection/summons/preview_sync
|
||||||
|
# Previews what would be deleted in a full sync operation
|
||||||
|
#
|
||||||
|
# @param data [Hash] Game data containing summon list
|
||||||
|
# @return [JSON] List of items that would be deleted
|
||||||
|
def preview_sync
|
||||||
|
game_data = import_params[:data]
|
||||||
|
|
||||||
|
unless game_data.present?
|
||||||
|
return render json: { error: 'No data provided' }, status: :bad_request
|
||||||
|
end
|
||||||
|
|
||||||
|
service = SummonImportService.new(current_user, game_data)
|
||||||
|
items_to_delete = service.preview_deletions
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
will_delete: items_to_delete.map do |cs|
|
||||||
|
{
|
||||||
|
id: cs.id,
|
||||||
|
game_id: cs.game_id,
|
||||||
|
name: cs.summon&.name_en,
|
||||||
|
granblue_id: cs.summon&.granblue_id,
|
||||||
|
uncap_level: cs.uncap_level,
|
||||||
|
transcendence_step: cs.transcendence_step
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
count: items_to_delete.size
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_target_user
|
def set_target_user
|
||||||
|
|
@ -181,6 +216,8 @@ module Api
|
||||||
def import_params
|
def import_params
|
||||||
{
|
{
|
||||||
update_existing: params[:update_existing],
|
update_existing: params[:update_existing],
|
||||||
|
is_full_inventory: params[:is_full_inventory],
|
||||||
|
reconcile_deletions: params[:reconcile_deletions],
|
||||||
data: params[:data]&.to_unsafe_h
|
data: params[:data]&.to_unsafe_h
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ module Api
|
||||||
before_action :set_collection_weapon_for_read, only: %i[show]
|
before_action :set_collection_weapon_for_read, only: %i[show]
|
||||||
|
|
||||||
# Write actions: require auth, use current_user
|
# Write actions: require auth, use current_user
|
||||||
before_action :restrict_access, only: %i[create update destroy batch batch_destroy import]
|
before_action :restrict_access, only: %i[create update destroy batch batch_destroy import preview_sync]
|
||||||
before_action :set_collection_weapon_for_write, only: %i[update destroy]
|
before_action :set_collection_weapon_for_write, only: %i[update destroy]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
|
@ -121,6 +121,8 @@ module Api
|
||||||
#
|
#
|
||||||
# @param data [Hash] Game data containing weapon list
|
# @param data [Hash] Game data containing weapon list
|
||||||
# @param update_existing [Boolean] Whether to update existing weapons (default: false)
|
# @param update_existing [Boolean] Whether to update existing weapons (default: false)
|
||||||
|
# @param is_full_inventory [Boolean] Whether this represents the user's complete inventory (default: false)
|
||||||
|
# @param reconcile_deletions [Boolean] Whether to delete items not in the import (default: false)
|
||||||
def import
|
def import
|
||||||
game_data = import_params[:data]
|
game_data = import_params[:data]
|
||||||
|
|
||||||
|
|
@ -131,7 +133,9 @@ module Api
|
||||||
service = WeaponImportService.new(
|
service = WeaponImportService.new(
|
||||||
current_user,
|
current_user,
|
||||||
game_data,
|
game_data,
|
||||||
update_existing: import_params[:update_existing] == true
|
update_existing: import_params[:update_existing] == true,
|
||||||
|
is_full_inventory: import_params[:is_full_inventory] == true,
|
||||||
|
reconcile_deletions: import_params[:reconcile_deletions] == true
|
||||||
)
|
)
|
||||||
|
|
||||||
result = service.import
|
result = service.import
|
||||||
|
|
@ -143,10 +147,41 @@ module Api
|
||||||
created: result.created.size,
|
created: result.created.size,
|
||||||
updated: result.updated.size,
|
updated: result.updated.size,
|
||||||
skipped: result.skipped.size,
|
skipped: result.skipped.size,
|
||||||
errors: result.errors
|
errors: result.errors,
|
||||||
|
reconciliation: result.reconciliation
|
||||||
}, status: status
|
}, status: status
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# POST /collection/weapons/preview_sync
|
||||||
|
# Previews what would be deleted in a full sync operation
|
||||||
|
#
|
||||||
|
# @param data [Hash] Game data containing weapon list
|
||||||
|
# @return [JSON] List of items that would be deleted
|
||||||
|
def preview_sync
|
||||||
|
game_data = import_params[:data]
|
||||||
|
|
||||||
|
unless game_data.present?
|
||||||
|
return render json: { error: 'No data provided' }, status: :bad_request
|
||||||
|
end
|
||||||
|
|
||||||
|
service = WeaponImportService.new(current_user, game_data)
|
||||||
|
items_to_delete = service.preview_deletions
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
will_delete: items_to_delete.map do |cw|
|
||||||
|
{
|
||||||
|
id: cw.id,
|
||||||
|
game_id: cw.game_id,
|
||||||
|
name: cw.weapon&.name_en,
|
||||||
|
granblue_id: cw.weapon&.granblue_id,
|
||||||
|
uncap_level: cw.uncap_level,
|
||||||
|
transcendence_step: cw.transcendence_step
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
count: items_to_delete.size
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_target_user
|
def set_target_user
|
||||||
|
|
@ -199,6 +234,8 @@ module Api
|
||||||
def import_params
|
def import_params
|
||||||
{
|
{
|
||||||
update_existing: params[:update_existing],
|
update_existing: params[:update_existing],
|
||||||
|
is_full_inventory: params[:is_full_inventory],
|
||||||
|
reconcile_deletions: params[:reconcile_deletions],
|
||||||
data: params[:data]&.to_unsafe_h
|
data: params[:data]&.to_unsafe_h
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,10 @@ class CollectionArtifact < ApplicationRecord
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
belongs_to :artifact
|
belongs_to :artifact
|
||||||
|
|
||||||
|
has_many :grid_artifacts, dependent: :nullify
|
||||||
|
|
||||||
|
before_destroy :orphan_grid_items
|
||||||
|
|
||||||
# Enums - using GranblueEnums::ELEMENTS values (excluding Null)
|
# Enums - using GranblueEnums::ELEMENTS values (excluding Null)
|
||||||
# Wind: 1, Fire: 2, Water: 3, Earth: 4, Dark: 5, Light: 6
|
# Wind: 1, Fire: 2, Water: 3, Earth: 4, Dark: 5, Light: 6
|
||||||
enum :element, {
|
enum :element, {
|
||||||
|
|
@ -77,4 +81,12 @@ class CollectionArtifact < ApplicationRecord
|
||||||
def quirk_artifact?
|
def quirk_artifact?
|
||||||
artifact&.quirk?
|
artifact&.quirk?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Marks all linked grid artifacts as orphaned before destroying this collection artifact.
|
||||||
|
#
|
||||||
|
# @return [void]
|
||||||
|
def orphan_grid_items
|
||||||
|
grid_artifacts.update_all(orphaned: true, collection_artifact_id: nil)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@ class CollectionSummon < ApplicationRecord
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
belongs_to :summon
|
belongs_to :summon
|
||||||
|
|
||||||
|
has_many :grid_summons, dependent: :nullify
|
||||||
|
|
||||||
|
before_destroy :orphan_grid_items
|
||||||
|
|
||||||
validates :uncap_level, inclusion: { in: 0..5 }
|
validates :uncap_level, inclusion: { in: 0..5 }
|
||||||
validates :transcendence_step, inclusion: { in: 0..10 }
|
validates :transcendence_step, inclusion: { in: 0..10 }
|
||||||
|
|
||||||
|
|
@ -31,4 +35,12 @@ class CollectionSummon < ApplicationRecord
|
||||||
errors.add(:transcendence_step, "not available for this summon")
|
errors.add(:transcendence_step, "not available for this summon")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Marks all linked grid summons as orphaned before destroying this collection summon.
|
||||||
|
#
|
||||||
|
# @return [void]
|
||||||
|
def orphan_grid_items
|
||||||
|
grid_summons.update_all(orphaned: true, collection_summon_id: nil)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -12,6 +12,10 @@ class CollectionWeapon < ApplicationRecord
|
||||||
belongs_to :ax_modifier2, class_name: 'WeaponStatModifier', optional: true
|
belongs_to :ax_modifier2, class_name: 'WeaponStatModifier', optional: true
|
||||||
belongs_to :befoulment_modifier, class_name: 'WeaponStatModifier', optional: true
|
belongs_to :befoulment_modifier, class_name: 'WeaponStatModifier', optional: true
|
||||||
|
|
||||||
|
has_many :grid_weapons, dependent: :nullify
|
||||||
|
|
||||||
|
before_destroy :orphan_grid_items
|
||||||
|
|
||||||
# Set defaults before validation so database defaults don't cause validation failures
|
# Set defaults before validation so database defaults don't cause validation failures
|
||||||
attribute :awakening_level, :integer, default: 1
|
attribute :awakening_level, :integer, default: 1
|
||||||
|
|
||||||
|
|
@ -162,4 +166,12 @@ class CollectionWeapon < ApplicationRecord
|
||||||
errors.add(:transcendence_step, "not available for this weapon") if transcendence_step > 0
|
errors.add(:transcendence_step, "not available for this weapon") if transcendence_step > 0
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Marks all linked grid weapons as orphaned before destroying this collection weapon.
|
||||||
|
#
|
||||||
|
# @return [void]
|
||||||
|
def orphan_grid_items
|
||||||
|
grid_weapons.update_all(orphaned: true, collection_weapon_id: nil)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -11,6 +11,10 @@ class GridArtifact < ApplicationRecord
|
||||||
has_one :party, through: :grid_character
|
has_one :party, through: :grid_character
|
||||||
has_one :character, through: :grid_character
|
has_one :character, through: :grid_character
|
||||||
|
|
||||||
|
# Orphan status scopes
|
||||||
|
scope :orphaned, -> { where(orphaned: true) }
|
||||||
|
scope :not_orphaned, -> { where(orphaned: false) }
|
||||||
|
|
||||||
# Enums - using GranblueEnums::ELEMENTS values (excluding Null)
|
# Enums - using GranblueEnums::ELEMENTS values (excluding Null)
|
||||||
# Wind: 1, Fire: 2, Water: 3, Earth: 4, Dark: 5, Light: 6
|
# Wind: 1, Fire: 2, Water: 3, Earth: 4, Dark: 5, Light: 6
|
||||||
enum :element, {
|
enum :element, {
|
||||||
|
|
@ -55,6 +59,14 @@ class GridArtifact < ApplicationRecord
|
||||||
quirk_artifact? ? proficiency : artifact&.proficiency
|
quirk_artifact? ? proficiency : artifact&.proficiency
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Marks this grid artifact as orphaned and clears its collection link.
|
||||||
|
#
|
||||||
|
# @return [Boolean] true if the update succeeded
|
||||||
|
def mark_orphaned!
|
||||||
|
update!(orphaned: true, collection_artifact_id: nil)
|
||||||
|
end
|
||||||
|
|
||||||
##
|
##
|
||||||
# Syncs customizations from the linked collection artifact.
|
# Syncs customizations from the linked collection artifact.
|
||||||
#
|
#
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,10 @@ class GridSummon < ApplicationRecord
|
||||||
belongs_to :collection_summon, optional: true
|
belongs_to :collection_summon, optional: true
|
||||||
validates_presence_of :party
|
validates_presence_of :party
|
||||||
|
|
||||||
|
# Orphan status scopes
|
||||||
|
scope :orphaned, -> { where(orphaned: true) }
|
||||||
|
scope :not_orphaned, -> { where(orphaned: false) }
|
||||||
|
|
||||||
# Validate that position is provided.
|
# Validate that position is provided.
|
||||||
validates :position, presence: true
|
validates :position, presence: true
|
||||||
validate :compatible_with_position, on: :create
|
validate :compatible_with_position, on: :create
|
||||||
|
|
@ -40,6 +44,14 @@ class GridSummon < ApplicationRecord
|
||||||
GridSummonBlueprint
|
GridSummonBlueprint
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Marks this grid summon as orphaned and clears its collection link.
|
||||||
|
#
|
||||||
|
# @return [Boolean] true if the update succeeded
|
||||||
|
def mark_orphaned!
|
||||||
|
update!(orphaned: true, collection_summon_id: nil)
|
||||||
|
end
|
||||||
|
|
||||||
##
|
##
|
||||||
# Syncs customizations from the linked collection summon.
|
# Syncs customizations from the linked collection summon.
|
||||||
#
|
#
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,10 @@ class GridWeapon < ApplicationRecord
|
||||||
belongs_to :ax_modifier2, class_name: 'WeaponStatModifier', optional: true
|
belongs_to :ax_modifier2, class_name: 'WeaponStatModifier', optional: true
|
||||||
belongs_to :befoulment_modifier, class_name: 'WeaponStatModifier', optional: true
|
belongs_to :befoulment_modifier, class_name: 'WeaponStatModifier', optional: true
|
||||||
|
|
||||||
|
# Orphan status scopes
|
||||||
|
scope :orphaned, -> { where(orphaned: true) }
|
||||||
|
scope :not_orphaned, -> { where(orphaned: false) }
|
||||||
|
|
||||||
# Validate that uncap_level is present and numeric, transcendence_step is optional but must be numeric if present.
|
# Validate that uncap_level is present and numeric, transcendence_step is optional but must be numeric if present.
|
||||||
validates :uncap_level, presence: true, numericality: { only_integer: true }
|
validates :uncap_level, presence: true, numericality: { only_integer: true }
|
||||||
validates :transcendence_step, numericality: { only_integer: true }, allow_nil: true
|
validates :transcendence_step, numericality: { only_integer: true }, allow_nil: true
|
||||||
|
|
@ -100,6 +104,14 @@ class GridWeapon < ApplicationRecord
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Marks this grid weapon as orphaned and clears its collection link.
|
||||||
|
#
|
||||||
|
# @return [Boolean] true if the update succeeded
|
||||||
|
def mark_orphaned!
|
||||||
|
update!(orphaned: true, collection_weapon_id: nil)
|
||||||
|
end
|
||||||
|
|
||||||
##
|
##
|
||||||
# Checks if grid weapon is out of sync with collection.
|
# Checks if grid weapon is out of sync with collection.
|
||||||
#
|
#
|
||||||
|
|
|
||||||
|
|
@ -274,6 +274,19 @@ class Party < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Checks if the party has any orphaned grid items.
|
||||||
|
#
|
||||||
|
# An orphaned item is one whose linked collection item has been deleted,
|
||||||
|
# indicating the user no longer has that item in their game inventory.
|
||||||
|
#
|
||||||
|
# @return [Boolean] true if the party has orphaned weapons, summons, or artifacts.
|
||||||
|
def has_orphaned_items?
|
||||||
|
weapons.orphaned.exists? ||
|
||||||
|
summons.orphaned.exists? ||
|
||||||
|
characters.joins(:grid_artifact).where(grid_artifacts: { orphaned: true }).exists?
|
||||||
|
end
|
||||||
|
|
||||||
##
|
##
|
||||||
# Determines if the party meets the minimum requirements for preview generation.
|
# Determines if the party meets the minimum requirements for preview generation.
|
||||||
#
|
#
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
# end
|
# end
|
||||||
#
|
#
|
||||||
class ArtifactImportService
|
class ArtifactImportService
|
||||||
Result = Struct.new(:success?, :created, :updated, :skipped, :errors, keyword_init: true)
|
Result = Struct.new(:success?, :created, :updated, :skipped, :errors, :reconciliation, keyword_init: true)
|
||||||
|
|
||||||
# Game element values to our element enum values
|
# Game element values to our element enum values
|
||||||
# Game: 1=Fire, 2=Water, 3=Earth, 4=Wind, 5=Light, 6=Dark
|
# Game: 1=Fire, 2=Water, 3=Earth, 4=Wind, 5=Light, 6=Dark
|
||||||
|
|
@ -30,10 +30,38 @@ class ArtifactImportService
|
||||||
@user = user
|
@user = user
|
||||||
@game_data = game_data
|
@game_data = game_data
|
||||||
@update_existing = options[:update_existing] || false
|
@update_existing = options[:update_existing] || false
|
||||||
|
@is_full_inventory = options[:is_full_inventory] || false
|
||||||
|
@reconcile_deletions = options[:reconcile_deletions] || false
|
||||||
@created = []
|
@created = []
|
||||||
@updated = []
|
@updated = []
|
||||||
@skipped = []
|
@skipped = []
|
||||||
@errors = []
|
@errors = []
|
||||||
|
@processed_game_ids = []
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Previews what would be deleted in a sync operation.
|
||||||
|
# Does not modify any data, just returns items that would be removed.
|
||||||
|
#
|
||||||
|
# @return [Array<CollectionArtifact>] Collection artifacts that would be deleted
|
||||||
|
def preview_deletions
|
||||||
|
items = extract_items
|
||||||
|
return [] if items.empty?
|
||||||
|
|
||||||
|
# Extract all game_ids from the import data
|
||||||
|
# Artifacts use 'id' directly (not nested in 'param')
|
||||||
|
game_ids = items.filter_map do |item|
|
||||||
|
data = item.is_a?(Hash) ? item.with_indifferent_access : item
|
||||||
|
data['id'].to_s if data['id'].present?
|
||||||
|
end
|
||||||
|
|
||||||
|
return [] if game_ids.empty?
|
||||||
|
|
||||||
|
# Find collection artifacts with game_ids NOT in the import
|
||||||
|
@user.collection_artifacts
|
||||||
|
.includes(:artifact)
|
||||||
|
.where.not(game_id: nil)
|
||||||
|
.where.not(game_id: game_ids)
|
||||||
end
|
end
|
||||||
|
|
||||||
##
|
##
|
||||||
|
|
@ -43,7 +71,14 @@ class ArtifactImportService
|
||||||
def import
|
def import
|
||||||
items = extract_items
|
items = extract_items
|
||||||
if items.empty?
|
if items.empty?
|
||||||
return Result.new(success?: false, created: [], updated: [], skipped: [], errors: ['No artifact items found in data'])
|
return Result.new(
|
||||||
|
success?: false,
|
||||||
|
created: [],
|
||||||
|
updated: [],
|
||||||
|
skipped: [],
|
||||||
|
errors: ['No artifact items found in data'],
|
||||||
|
reconciliation: nil
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Preload artifacts and existing collection artifacts to avoid N+1 queries
|
# Preload artifacts and existing collection artifacts to avoid N+1 queries
|
||||||
|
|
@ -58,12 +93,19 @@ class ArtifactImportService
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Handle deletion reconciliation if requested
|
||||||
|
reconciliation_result = nil
|
||||||
|
if @reconcile_deletions && @is_full_inventory && @processed_game_ids.any?
|
||||||
|
reconciliation_result = reconcile_deletions
|
||||||
|
end
|
||||||
|
|
||||||
Result.new(
|
Result.new(
|
||||||
success?: @errors.empty?,
|
success?: @errors.empty?,
|
||||||
created: @created,
|
created: @created,
|
||||||
updated: @updated,
|
updated: @updated,
|
||||||
skipped: @skipped,
|
skipped: @skipped,
|
||||||
errors: @errors
|
errors: @errors,
|
||||||
|
reconciliation: reconciliation_result
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -92,6 +134,10 @@ class ArtifactImportService
|
||||||
# Handle both string and symbol keys from params
|
# Handle both string and symbol keys from params
|
||||||
data = item.is_a?(Hash) ? item.with_indifferent_access : item
|
data = item.is_a?(Hash) ? item.with_indifferent_access : item
|
||||||
|
|
||||||
|
# Track this game_id as processed (for reconciliation)
|
||||||
|
game_id = data['id']
|
||||||
|
@processed_game_ids << game_id.to_s if game_id.present?
|
||||||
|
|
||||||
artifact = find_artifact(data['artifact_id'])
|
artifact = find_artifact(data['artifact_id'])
|
||||||
unless artifact
|
unless artifact
|
||||||
@errors << { game_id: data['id'], artifact_id: data['artifact_id'], error: 'Artifact not found' }
|
@errors << { game_id: data['id'], artifact_id: data['artifact_id'], error: 'Artifact not found' }
|
||||||
|
|
@ -193,4 +239,34 @@ class ArtifactImportService
|
||||||
'level' => level.to_i
|
'level' => level.to_i
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Reconciles deletions by removing collection artifacts not in the processed list.
|
||||||
|
# Only called when @is_full_inventory and @reconcile_deletions are both true.
|
||||||
|
#
|
||||||
|
# @return [Hash] Reconciliation result with deleted count and orphaned grid item IDs
|
||||||
|
def reconcile_deletions
|
||||||
|
# Find collection artifacts with game_ids NOT in our processed list
|
||||||
|
missing = @user.collection_artifacts
|
||||||
|
.where.not(game_id: nil)
|
||||||
|
.where.not(game_id: @processed_game_ids)
|
||||||
|
|
||||||
|
deleted_count = 0
|
||||||
|
orphaned_grid_item_ids = []
|
||||||
|
|
||||||
|
missing.find_each do |coll_artifact|
|
||||||
|
# Collect IDs of grid items that will be orphaned
|
||||||
|
grid_artifact_ids = GridArtifact.where(collection_artifact_id: coll_artifact.id).pluck(:id)
|
||||||
|
orphaned_grid_item_ids.concat(grid_artifact_ids)
|
||||||
|
|
||||||
|
# The before_destroy callback on CollectionArtifact will mark grid items as orphaned
|
||||||
|
coll_artifact.destroy
|
||||||
|
deleted_count += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
{
|
||||||
|
deleted: deleted_count,
|
||||||
|
orphaned_grid_items: orphaned_grid_item_ids
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -12,16 +12,43 @@
|
||||||
# end
|
# end
|
||||||
#
|
#
|
||||||
class SummonImportService
|
class SummonImportService
|
||||||
Result = Struct.new(:success?, :created, :updated, :skipped, :errors, keyword_init: true)
|
Result = Struct.new(:success?, :created, :updated, :skipped, :errors, :reconciliation, keyword_init: true)
|
||||||
|
|
||||||
def initialize(user, game_data, options = {})
|
def initialize(user, game_data, options = {})
|
||||||
@user = user
|
@user = user
|
||||||
@game_data = game_data
|
@game_data = game_data
|
||||||
@update_existing = options[:update_existing] || false
|
@update_existing = options[:update_existing] || false
|
||||||
|
@is_full_inventory = options[:is_full_inventory] || false
|
||||||
|
@reconcile_deletions = options[:reconcile_deletions] || false
|
||||||
@created = []
|
@created = []
|
||||||
@updated = []
|
@updated = []
|
||||||
@skipped = []
|
@skipped = []
|
||||||
@errors = []
|
@errors = []
|
||||||
|
@processed_game_ids = []
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Previews what would be deleted in a sync operation.
|
||||||
|
# Does not modify any data, just returns items that would be removed.
|
||||||
|
#
|
||||||
|
# @return [Array<CollectionSummon>] Collection summons that would be deleted
|
||||||
|
def preview_deletions
|
||||||
|
items = extract_items
|
||||||
|
return [] if items.empty?
|
||||||
|
|
||||||
|
# Extract all game_ids from the import data
|
||||||
|
game_ids = items.filter_map do |item|
|
||||||
|
param = item['param'] || {}
|
||||||
|
param['id'].to_s if param['id'].present?
|
||||||
|
end
|
||||||
|
|
||||||
|
return [] if game_ids.empty?
|
||||||
|
|
||||||
|
# Find collection summons with game_ids NOT in the import
|
||||||
|
@user.collection_summons
|
||||||
|
.includes(:summon)
|
||||||
|
.where.not(game_id: nil)
|
||||||
|
.where.not(game_id: game_ids)
|
||||||
end
|
end
|
||||||
|
|
||||||
##
|
##
|
||||||
|
|
@ -30,7 +57,16 @@ class SummonImportService
|
||||||
# @return [Result] Import result with counts and errors
|
# @return [Result] Import result with counts and errors
|
||||||
def import
|
def import
|
||||||
items = extract_items
|
items = extract_items
|
||||||
return Result.new(success?: false, created: [], updated: [], skipped: [], errors: ['No summon items found in data']) if items.empty?
|
if items.empty?
|
||||||
|
return Result.new(
|
||||||
|
success?: false,
|
||||||
|
created: [],
|
||||||
|
updated: [],
|
||||||
|
skipped: [],
|
||||||
|
errors: ['No summon items found in data'],
|
||||||
|
reconciliation: nil
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
items.each_with_index do |item, index|
|
items.each_with_index do |item, index|
|
||||||
|
|
@ -40,12 +76,19 @@ class SummonImportService
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Handle deletion reconciliation if requested
|
||||||
|
reconciliation_result = nil
|
||||||
|
if @reconcile_deletions && @is_full_inventory && @processed_game_ids.any?
|
||||||
|
reconciliation_result = reconcile_deletions
|
||||||
|
end
|
||||||
|
|
||||||
Result.new(
|
Result.new(
|
||||||
success?: @errors.empty?,
|
success?: @errors.empty?,
|
||||||
created: @created,
|
created: @created,
|
||||||
updated: @updated,
|
updated: @updated,
|
||||||
skipped: @skipped,
|
skipped: @skipped,
|
||||||
errors: @errors
|
errors: @errors,
|
||||||
|
reconciliation: reconciliation_result
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -68,6 +111,9 @@ class SummonImportService
|
||||||
granblue_id = image_id || master['id']
|
granblue_id = image_id || master['id']
|
||||||
game_id = param['id']
|
game_id = param['id']
|
||||||
|
|
||||||
|
# Track this game_id as processed (for reconciliation)
|
||||||
|
@processed_game_ids << game_id.to_s if game_id.present?
|
||||||
|
|
||||||
summon = find_summon(granblue_id)
|
summon = find_summon(granblue_id)
|
||||||
unless summon
|
unless summon
|
||||||
@errors << { game_id: game_id, granblue_id: granblue_id, error: 'Summon not found' }
|
@errors << { game_id: game_id, granblue_id: granblue_id, error: 'Summon not found' }
|
||||||
|
|
@ -143,4 +189,34 @@ class SummonImportService
|
||||||
value = phase.to_i
|
value = phase.to_i
|
||||||
value.clamp(0, 10)
|
value.clamp(0, 10)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Reconciles deletions by removing collection summons not in the processed list.
|
||||||
|
# Only called when @is_full_inventory and @reconcile_deletions are both true.
|
||||||
|
#
|
||||||
|
# @return [Hash] Reconciliation result with deleted count and orphaned grid item IDs
|
||||||
|
def reconcile_deletions
|
||||||
|
# Find collection summons with game_ids NOT in our processed list
|
||||||
|
missing = @user.collection_summons
|
||||||
|
.where.not(game_id: nil)
|
||||||
|
.where.not(game_id: @processed_game_ids)
|
||||||
|
|
||||||
|
deleted_count = 0
|
||||||
|
orphaned_grid_item_ids = []
|
||||||
|
|
||||||
|
missing.find_each do |coll_summon|
|
||||||
|
# Collect IDs of grid items that will be orphaned
|
||||||
|
grid_summon_ids = GridSummon.where(collection_summon_id: coll_summon.id).pluck(:id)
|
||||||
|
orphaned_grid_item_ids.concat(grid_summon_ids)
|
||||||
|
|
||||||
|
# The before_destroy callback on CollectionSummon will mark grid items as orphaned
|
||||||
|
coll_summon.destroy
|
||||||
|
deleted_count += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
{
|
||||||
|
deleted: deleted_count,
|
||||||
|
orphaned_grid_items: orphaned_grid_item_ids
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
# end
|
# end
|
||||||
#
|
#
|
||||||
class WeaponImportService
|
class WeaponImportService
|
||||||
Result = Struct.new(:success?, :created, :updated, :skipped, :errors, keyword_init: true)
|
Result = Struct.new(:success?, :created, :updated, :skipped, :errors, :reconciliation, keyword_init: true)
|
||||||
|
|
||||||
# Game awakening form to our slug mapping
|
# Game awakening form to our slug mapping
|
||||||
AWAKENING_FORM_MAPPING = {
|
AWAKENING_FORM_MAPPING = {
|
||||||
|
|
@ -28,12 +28,39 @@ class WeaponImportService
|
||||||
@user = user
|
@user = user
|
||||||
@game_data = game_data
|
@game_data = game_data
|
||||||
@update_existing = options[:update_existing] || false
|
@update_existing = options[:update_existing] || false
|
||||||
|
@is_full_inventory = options[:is_full_inventory] || false
|
||||||
|
@reconcile_deletions = options[:reconcile_deletions] || false
|
||||||
@created = []
|
@created = []
|
||||||
@updated = []
|
@updated = []
|
||||||
@skipped = []
|
@skipped = []
|
||||||
@errors = []
|
@errors = []
|
||||||
@awakening_cache = {}
|
@awakening_cache = {}
|
||||||
@modifier_cache = {}
|
@modifier_cache = {}
|
||||||
|
@processed_game_ids = []
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Previews what would be deleted in a sync operation.
|
||||||
|
# Does not modify any data, just returns items that would be removed.
|
||||||
|
#
|
||||||
|
# @return [Array<CollectionWeapon>] Collection weapons that would be deleted
|
||||||
|
def preview_deletions
|
||||||
|
items = extract_items
|
||||||
|
return [] if items.empty?
|
||||||
|
|
||||||
|
# Extract all game_ids from the import data
|
||||||
|
game_ids = items.filter_map do |item|
|
||||||
|
param = item['param'] || {}
|
||||||
|
param['id'].to_s if param['id'].present?
|
||||||
|
end
|
||||||
|
|
||||||
|
return [] if game_ids.empty?
|
||||||
|
|
||||||
|
# Find collection weapons with game_ids NOT in the import
|
||||||
|
@user.collection_weapons
|
||||||
|
.includes(:weapon)
|
||||||
|
.where.not(game_id: nil)
|
||||||
|
.where.not(game_id: game_ids)
|
||||||
end
|
end
|
||||||
|
|
||||||
##
|
##
|
||||||
|
|
@ -42,7 +69,16 @@ class WeaponImportService
|
||||||
# @return [Result] Import result with counts and errors
|
# @return [Result] Import result with counts and errors
|
||||||
def import
|
def import
|
||||||
items = extract_items
|
items = extract_items
|
||||||
return Result.new(success?: false, created: [], updated: [], skipped: [], errors: ['No weapon items found in data']) if items.empty?
|
if items.empty?
|
||||||
|
return Result.new(
|
||||||
|
success?: false,
|
||||||
|
created: [],
|
||||||
|
updated: [],
|
||||||
|
skipped: [],
|
||||||
|
errors: ['No weapon items found in data'],
|
||||||
|
reconciliation: nil
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
items.each_with_index do |item, index|
|
items.each_with_index do |item, index|
|
||||||
|
|
@ -52,12 +88,19 @@ class WeaponImportService
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Handle deletion reconciliation if requested
|
||||||
|
reconciliation_result = nil
|
||||||
|
if @reconcile_deletions && @is_full_inventory && @processed_game_ids.any?
|
||||||
|
reconciliation_result = reconcile_deletions
|
||||||
|
end
|
||||||
|
|
||||||
Result.new(
|
Result.new(
|
||||||
success?: @errors.empty?,
|
success?: @errors.empty?,
|
||||||
created: @created,
|
created: @created,
|
||||||
updated: @updated,
|
updated: @updated,
|
||||||
skipped: @skipped,
|
skipped: @skipped,
|
||||||
errors: @errors
|
errors: @errors,
|
||||||
|
reconciliation: reconciliation_result
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -78,6 +121,9 @@ class WeaponImportService
|
||||||
granblue_id = param['image_id'] || master['id']
|
granblue_id = param['image_id'] || master['id']
|
||||||
game_id = param['id']
|
game_id = param['id']
|
||||||
|
|
||||||
|
# Track this game_id as processed (for reconciliation)
|
||||||
|
@processed_game_ids << game_id.to_s if game_id.present?
|
||||||
|
|
||||||
weapon = find_weapon(granblue_id)
|
weapon = find_weapon(granblue_id)
|
||||||
unless weapon
|
unless weapon
|
||||||
@errors << { game_id: game_id, granblue_id: granblue_id, error: 'Weapon not found' }
|
@errors << { game_id: game_id, granblue_id: granblue_id, error: 'Weapon not found' }
|
||||||
|
|
@ -307,4 +353,34 @@ class WeaponImportService
|
||||||
|
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Reconciles deletions by removing collection weapons not in the processed list.
|
||||||
|
# Only called when @is_full_inventory and @reconcile_deletions are both true.
|
||||||
|
#
|
||||||
|
# @return [Hash] Reconciliation result with deleted count and orphaned grid item IDs
|
||||||
|
def reconcile_deletions
|
||||||
|
# Find collection weapons with game_ids NOT in our processed list
|
||||||
|
missing = @user.collection_weapons
|
||||||
|
.where.not(game_id: nil)
|
||||||
|
.where.not(game_id: @processed_game_ids)
|
||||||
|
|
||||||
|
deleted_count = 0
|
||||||
|
orphaned_grid_item_ids = []
|
||||||
|
|
||||||
|
missing.find_each do |coll_weapon|
|
||||||
|
# Collect IDs of grid items that will be orphaned
|
||||||
|
grid_weapon_ids = GridWeapon.where(collection_weapon_id: coll_weapon.id).pluck(:id)
|
||||||
|
orphaned_grid_item_ids.concat(grid_weapon_ids)
|
||||||
|
|
||||||
|
# The before_destroy callback on CollectionWeapon will mark grid items as orphaned
|
||||||
|
coll_weapon.destroy
|
||||||
|
deleted_count += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
{
|
||||||
|
deleted: deleted_count,
|
||||||
|
orphaned_grid_items: orphaned_grid_item_ids
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -276,6 +276,7 @@ Rails.application.routes.draw do
|
||||||
post :batch
|
post :batch
|
||||||
delete :batch_destroy
|
delete :batch_destroy
|
||||||
post :import
|
post :import
|
||||||
|
post :preview_sync
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
resources :summons, only: [:create, :update, :destroy], controller: '/api/v1/collection_summons' do
|
resources :summons, only: [:create, :update, :destroy], controller: '/api/v1/collection_summons' do
|
||||||
|
|
@ -283,6 +284,7 @@ Rails.application.routes.draw do
|
||||||
post :batch
|
post :batch
|
||||||
delete :batch_destroy
|
delete :batch_destroy
|
||||||
post :import
|
post :import
|
||||||
|
post :preview_sync
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
resources :job_accessories, controller: '/api/v1/collection_job_accessories',
|
resources :job_accessories, controller: '/api/v1/collection_job_accessories',
|
||||||
|
|
@ -292,6 +294,7 @@ Rails.application.routes.draw do
|
||||||
post :batch
|
post :batch
|
||||||
delete :batch_destroy
|
delete :batch_destroy
|
||||||
post :import
|
post :import
|
||||||
|
post :preview_sync
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
13
db/migrate/20251223000000_add_orphaned_to_grid_items.rb
Normal file
13
db/migrate/20251223000000_add_orphaned_to_grid_items.rb
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddOrphanedToGridItems < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
add_column :grid_weapons, :orphaned, :boolean, default: false, null: false
|
||||||
|
add_column :grid_summons, :orphaned, :boolean, default: false, null: false
|
||||||
|
add_column :grid_artifacts, :orphaned, :boolean, default: false, null: false
|
||||||
|
|
||||||
|
add_index :grid_weapons, :orphaned
|
||||||
|
add_index :grid_summons, :orphaned
|
||||||
|
add_index :grid_artifacts, :orphaned
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Reference in a new issue