1043 lines
No EOL
30 KiB
Markdown
1043 lines
No EOL
30 KiB
Markdown
# Collection Tracking Implementation Guide
|
|
|
|
## Prerequisites
|
|
|
|
- Rails 8.0.1 environment set up
|
|
- PostgreSQL database running
|
|
- Basic understanding of the existing codebase structure
|
|
- Familiarity with Rails migrations, models, and controllers
|
|
|
|
## Step-by-Step Implementation
|
|
|
|
### Step 1: Create Database Migrations
|
|
|
|
#### 1.0 Add collection privacy levels to Users table
|
|
|
|
```bash
|
|
rails generate migration AddCollectionPrivacyToUsers
|
|
```
|
|
|
|
```ruby
|
|
# db/migrate/xxx_add_collection_privacy_to_users.rb
|
|
class AddCollectionPrivacyToUsers < ActiveRecord::Migration[8.0]
|
|
def change
|
|
# Privacy levels: 0 = public, 1 = crew_only, 2 = private
|
|
add_column :users, :collection_privacy, :integer, default: 0, null: false
|
|
add_index :users, :collection_privacy
|
|
end
|
|
end
|
|
```
|
|
|
|
#### 1.1 Create CollectionCharacters migration
|
|
|
|
```bash
|
|
rails generate migration CreateCollectionCharacters
|
|
```
|
|
|
|
```ruby
|
|
# db/migrate/xxx_create_collection_characters.rb
|
|
class CreateCollectionCharacters < ActiveRecord::Migration[8.0]
|
|
def change
|
|
create_table :collection_characters, id: :uuid do |t|
|
|
t.references :user, type: :uuid, null: false, foreign_key: true
|
|
t.references :character, type: :uuid, null: false, foreign_key: true
|
|
t.integer :uncap_level, default: 0, null: false
|
|
t.integer :transcendence_step, default: 0, null: false
|
|
t.boolean :perpetuity, default: false, null: false
|
|
t.references :awakening, type: :uuid, foreign_key: true
|
|
t.integer :awakening_level, default: 1
|
|
|
|
t.jsonb :ring1, default: { modifier: nil, strength: nil }, null: false
|
|
t.jsonb :ring2, default: { modifier: nil, strength: nil }, null: false
|
|
t.jsonb :ring3, default: { modifier: nil, strength: nil }, null: false
|
|
t.jsonb :ring4, default: { modifier: nil, strength: nil }, null: false
|
|
t.jsonb :earring, default: { modifier: nil, strength: nil }, null: false
|
|
|
|
t.timestamps
|
|
end
|
|
|
|
add_index :collection_characters, [:user_id, :character_id], unique: true
|
|
add_index :collection_characters, :user_id
|
|
add_index :collection_characters, :character_id
|
|
end
|
|
end
|
|
```
|
|
|
|
#### 1.2 Create CollectionWeapons migration
|
|
|
|
```bash
|
|
rails generate migration CreateCollectionWeapons
|
|
```
|
|
|
|
```ruby
|
|
# db/migrate/xxx_create_collection_weapons.rb
|
|
class CreateCollectionWeapons < ActiveRecord::Migration[8.0]
|
|
def change
|
|
create_table :collection_weapons, id: :uuid do |t|
|
|
t.references :user, type: :uuid, null: false, foreign_key: true
|
|
t.references :weapon, type: :uuid, null: false, foreign_key: true
|
|
t.integer :uncap_level, default: 0, null: false
|
|
t.integer :transcendence_step, default: 0
|
|
|
|
t.references :weapon_key1, type: :uuid, foreign_key: { to_table: :weapon_keys }
|
|
t.references :weapon_key2, type: :uuid, foreign_key: { to_table: :weapon_keys }
|
|
t.references :weapon_key3, type: :uuid, foreign_key: { to_table: :weapon_keys }
|
|
t.string :weapon_key4_id
|
|
|
|
t.references :awakening, type: :uuid, foreign_key: true
|
|
t.integer :awakening_level, default: 1, null: false
|
|
|
|
t.integer :ax_modifier1
|
|
t.float :ax_strength1
|
|
t.integer :ax_modifier2
|
|
t.float :ax_strength2
|
|
t.integer :element
|
|
|
|
t.timestamps
|
|
end
|
|
|
|
add_index :collection_weapons, :user_id
|
|
add_index :collection_weapons, :weapon_id
|
|
add_index :collection_weapons, [:user_id, :weapon_id]
|
|
end
|
|
end
|
|
```
|
|
|
|
#### 1.3 Create CollectionSummons migration
|
|
|
|
```bash
|
|
rails generate migration CreateCollectionSummons
|
|
```
|
|
|
|
```ruby
|
|
# db/migrate/xxx_create_collection_summons.rb
|
|
class CreateCollectionSummons < ActiveRecord::Migration[8.0]
|
|
def change
|
|
create_table :collection_summons, id: :uuid do |t|
|
|
t.references :user, type: :uuid, null: false, foreign_key: true
|
|
t.references :summon, type: :uuid, null: false, foreign_key: true
|
|
t.integer :uncap_level, default: 0, null: false
|
|
t.integer :transcendence_step, default: 0, null: false
|
|
|
|
t.timestamps
|
|
end
|
|
|
|
add_index :collection_summons, :user_id
|
|
add_index :collection_summons, :summon_id
|
|
add_index :collection_summons, [:user_id, :summon_id]
|
|
end
|
|
end
|
|
```
|
|
|
|
#### 1.4 Create CollectionJobAccessories migration
|
|
|
|
```bash
|
|
rails generate migration CreateCollectionJobAccessories
|
|
```
|
|
|
|
```ruby
|
|
# db/migrate/xxx_create_collection_job_accessories.rb
|
|
class CreateCollectionJobAccessories < ActiveRecord::Migration[8.0]
|
|
def change
|
|
create_table :collection_job_accessories, id: :uuid do |t|
|
|
t.references :user, type: :uuid, null: false, foreign_key: true
|
|
t.references :job_accessory, type: :uuid, null: false, foreign_key: true
|
|
|
|
t.timestamps
|
|
end
|
|
|
|
add_index :collection_job_accessories, [:user_id, :job_accessory_id],
|
|
unique: true, name: 'idx_collection_job_acc_user_accessory'
|
|
add_index :collection_job_accessories, :user_id
|
|
add_index :collection_job_accessories, :job_accessory_id
|
|
end
|
|
end
|
|
```
|
|
|
|
#### 1.5 Run migrations
|
|
|
|
```bash
|
|
rails db:migrate
|
|
```
|
|
|
|
### Step 2: Create Models
|
|
|
|
#### 2.1 Create CollectionCharacter model
|
|
|
|
```ruby
|
|
# app/models/collection_character.rb
|
|
class CollectionCharacter < ApplicationRecord
|
|
belongs_to :user
|
|
belongs_to :character
|
|
belongs_to :awakening, optional: true
|
|
|
|
validates :character_id, uniqueness: { scope: :user_id,
|
|
message: "already exists in your collection" }
|
|
validates :uncap_level, inclusion: { in: 0..5 }
|
|
validates :transcendence_step, inclusion: { in: 0..10 }
|
|
validates :awakening_level, inclusion: { in: 1..10 }
|
|
|
|
validate :validate_rings
|
|
validate :validate_awakening_compatibility
|
|
|
|
scope :by_element, ->(element) { joins(:character).where(characters: { element: element }) }
|
|
scope :by_rarity, ->(rarity) { joins(:character).where(characters: { rarity: rarity }) }
|
|
scope :transcended, -> { where('transcendence_step > 0') }
|
|
scope :with_awakening, -> { where.not(awakening_id: nil) }
|
|
|
|
def blueprint
|
|
CollectionCharacterBlueprint
|
|
end
|
|
|
|
private
|
|
|
|
def validate_rings
|
|
[ring1, ring2, ring3, ring4, earring].each_with_index do |ring, index|
|
|
next unless ring['modifier'].present? || ring['strength'].present?
|
|
|
|
if ring['modifier'].blank? || ring['strength'].blank?
|
|
errors.add(:base, "Ring #{index + 1} must have both modifier and strength")
|
|
end
|
|
end
|
|
end
|
|
|
|
def validate_awakening_compatibility
|
|
return unless awakening.present?
|
|
|
|
unless awakening.object_type == 'Character'
|
|
errors.add(:awakening, "must be a character awakening")
|
|
end
|
|
end
|
|
end
|
|
```
|
|
|
|
#### 2.2 Create CollectionWeapon model
|
|
|
|
```ruby
|
|
# app/models/collection_weapon.rb
|
|
class CollectionWeapon < ApplicationRecord
|
|
belongs_to :user
|
|
belongs_to :weapon
|
|
belongs_to :awakening, optional: true
|
|
|
|
belongs_to :weapon_key1, class_name: 'WeaponKey', optional: true
|
|
belongs_to :weapon_key2, class_name: 'WeaponKey', optional: true
|
|
belongs_to :weapon_key3, class_name: 'WeaponKey', optional: true
|
|
belongs_to :weapon_key4, class_name: 'WeaponKey', optional: true
|
|
|
|
validates :uncap_level, inclusion: { in: 0..5 }
|
|
validates :transcendence_step, inclusion: { in: 0..10 }, allow_nil: true
|
|
validates :awakening_level, inclusion: { in: 1..10 }
|
|
|
|
validate :validate_weapon_keys
|
|
validate :validate_ax_skills
|
|
validate :validate_element_change
|
|
validate :validate_awakening_compatibility
|
|
|
|
scope :by_weapon, ->(weapon_id) { where(weapon_id: weapon_id) }
|
|
scope :by_series, ->(series) { joins(:weapon).where(weapons: { series: series }) }
|
|
scope :with_keys, -> { where.not(weapon_key1_id: nil) }
|
|
scope :with_ax, -> { where.not(ax_modifier1: nil) }
|
|
|
|
def blueprint
|
|
CollectionWeaponBlueprint
|
|
end
|
|
|
|
def weapon_keys
|
|
[weapon_key1, weapon_key2, weapon_key3, weapon_key4].compact
|
|
end
|
|
|
|
private
|
|
|
|
def validate_weapon_keys
|
|
return unless weapon.present?
|
|
|
|
weapon_keys.each do |key|
|
|
unless weapon.compatible_with_key?(key)
|
|
errors.add(:weapon_keys, "#{key.name_en} is not compatible with this weapon")
|
|
end
|
|
end
|
|
|
|
# Check for duplicate keys
|
|
key_ids = [weapon_key1_id, weapon_key2_id, weapon_key3_id, weapon_key4_id].compact
|
|
if key_ids.length != key_ids.uniq.length
|
|
errors.add(:weapon_keys, "cannot have duplicate keys")
|
|
end
|
|
end
|
|
|
|
def validate_ax_skills
|
|
return unless weapon.present? && weapon.ax
|
|
|
|
if (ax_modifier1.present? && ax_strength1.blank?) ||
|
|
(ax_modifier1.blank? && ax_strength1.present?)
|
|
errors.add(:ax_modifier1, "AX skill 1 must have both modifier and strength")
|
|
end
|
|
|
|
if (ax_modifier2.present? && ax_strength2.blank?) ||
|
|
(ax_modifier2.blank? && ax_strength2.present?)
|
|
errors.add(:ax_modifier2, "AX skill 2 must have both modifier and strength")
|
|
end
|
|
end
|
|
|
|
def validate_element_change
|
|
return unless element.present? && weapon.present?
|
|
|
|
unless Weapon.element_changeable?(weapon.series)
|
|
errors.add(:element, "cannot be changed for this weapon series")
|
|
end
|
|
end
|
|
|
|
def validate_awakening_compatibility
|
|
return unless awakening.present?
|
|
|
|
unless awakening.object_type == 'Weapon'
|
|
errors.add(:awakening, "must be a weapon awakening")
|
|
end
|
|
end
|
|
end
|
|
```
|
|
|
|
#### 2.3 Create CollectionSummon model
|
|
|
|
```ruby
|
|
# app/models/collection_summon.rb
|
|
class CollectionSummon < ApplicationRecord
|
|
belongs_to :user
|
|
belongs_to :summon
|
|
|
|
validates :uncap_level, inclusion: { in: 0..5 }
|
|
validates :transcendence_step, inclusion: { in: 0..10 }
|
|
|
|
scope :by_summon, ->(summon_id) { where(summon_id: summon_id) }
|
|
scope :by_element, ->(element) { joins(:summon).where(summons: { element: element }) }
|
|
scope :transcended, -> { where('transcendence_step > 0') }
|
|
|
|
def blueprint
|
|
CollectionSummonBlueprint
|
|
end
|
|
end
|
|
```
|
|
|
|
#### 2.4 Create CollectionJobAccessory model
|
|
|
|
```ruby
|
|
# app/models/collection_job_accessory.rb
|
|
class CollectionJobAccessory < ApplicationRecord
|
|
belongs_to :user
|
|
belongs_to :job_accessory
|
|
|
|
validates :job_accessory_id, uniqueness: { scope: :user_id,
|
|
message: "already exists in your collection" }
|
|
|
|
scope :by_job, ->(job_id) { joins(:job_accessory).where(job_accessories: { job_id: job_id }) }
|
|
|
|
def blueprint
|
|
CollectionJobAccessoryBlueprint
|
|
end
|
|
end
|
|
```
|
|
|
|
#### 2.5 Update User model
|
|
|
|
```ruby
|
|
# app/models/user.rb - Add these associations and methods
|
|
|
|
# Associations
|
|
has_many :collection_characters, dependent: :destroy
|
|
has_many :collection_weapons, dependent: :destroy
|
|
has_many :collection_summons, dependent: :destroy
|
|
has_many :collection_job_accessories, dependent: :destroy
|
|
|
|
# Note: The crew association will be added when crews feature is implemented
|
|
# belongs_to :crew, optional: true
|
|
|
|
# Enum for collection privacy levels
|
|
enum collection_privacy: {
|
|
public: 0,
|
|
crew_only: 1,
|
|
private: 2
|
|
}
|
|
|
|
# Add collection statistics method
|
|
def collection_statistics
|
|
{
|
|
total_characters: collection_characters.count,
|
|
total_weapons: collection_weapons.count,
|
|
total_summons: collection_summons.count,
|
|
total_job_accessories: collection_job_accessories.count,
|
|
unique_weapons: collection_weapons.distinct.count(:weapon_id),
|
|
unique_summons: collection_summons.distinct.count(:summon_id)
|
|
}
|
|
end
|
|
|
|
# Check if collection is viewable by another user
|
|
def collection_viewable_by?(viewer)
|
|
return true if self == viewer # Owners can always view their own collection
|
|
|
|
case collection_privacy
|
|
when 'public'
|
|
true
|
|
when 'crew_only'
|
|
# Will be implemented when crew feature is added:
|
|
# viewer.present? && crew.present? && viewer.crew_id == crew_id
|
|
false # For now, crew_only acts like private until crews are implemented
|
|
when 'private'
|
|
false
|
|
else
|
|
false
|
|
end
|
|
end
|
|
|
|
# Helper method to check if user is in same crew (placeholder for future)
|
|
def in_same_crew_as?(other_user)
|
|
# Will be implemented when crew feature is added:
|
|
# return false unless other_user.present?
|
|
# crew.present? && other_user.crew_id == crew_id
|
|
false
|
|
end
|
|
```
|
|
|
|
### Step 3: Create Blueprints
|
|
|
|
#### 3.1 CollectionCharacterBlueprint
|
|
|
|
```ruby
|
|
# app/blueprints/api/v1/collection_character_blueprint.rb
|
|
module Api
|
|
module V1
|
|
class CollectionCharacterBlueprint < ApiBlueprint
|
|
identifier :id
|
|
|
|
fields :uncap_level, :transcendence_step, :perpetuity,
|
|
:ring1, :ring2, :ring3, :ring4, :earring,
|
|
:created_at, :updated_at
|
|
|
|
field :awakening, if: ->(_, obj, _) { obj.awakening.present? } do |obj|
|
|
{
|
|
type: AwakeningBlueprint.render_as_hash(obj.awakening),
|
|
level: obj.awakening_level
|
|
}
|
|
end
|
|
|
|
association :character, blueprint: CharacterBlueprint, view: :nested
|
|
|
|
view :full do
|
|
association :character, blueprint: CharacterBlueprint, view: :full
|
|
end
|
|
end
|
|
end
|
|
end
|
|
```
|
|
|
|
#### 3.2 CollectionWeaponBlueprint
|
|
|
|
```ruby
|
|
# app/blueprints/api/v1/collection_weapon_blueprint.rb
|
|
module Api
|
|
module V1
|
|
class CollectionWeaponBlueprint < ApiBlueprint
|
|
identifier :id
|
|
|
|
fields :uncap_level, :transcendence_step, :element,
|
|
:created_at, :updated_at
|
|
|
|
field :ax, if: ->(_, obj, _) { obj.ax_modifier1.present? } do |obj|
|
|
[
|
|
{ modifier: obj.ax_modifier1, strength: obj.ax_strength1 },
|
|
{ modifier: obj.ax_modifier2, strength: obj.ax_strength2 }
|
|
].compact_blank
|
|
end
|
|
|
|
field :awakening, if: ->(_, obj, _) { obj.awakening.present? } do |obj|
|
|
{
|
|
type: AwakeningBlueprint.render_as_hash(obj.awakening),
|
|
level: obj.awakening_level
|
|
}
|
|
end
|
|
|
|
association :weapon, blueprint: WeaponBlueprint, view: :nested
|
|
association :weapon_keys, blueprint: WeaponKeyBlueprint,
|
|
if: ->(_, obj, _) { obj.weapon_keys.any? }
|
|
|
|
view :full do
|
|
association :weapon, blueprint: WeaponBlueprint, view: :full
|
|
end
|
|
end
|
|
end
|
|
end
|
|
```
|
|
|
|
#### 3.3 CollectionSummonBlueprint
|
|
|
|
```ruby
|
|
# app/blueprints/api/v1/collection_summon_blueprint.rb
|
|
module Api
|
|
module V1
|
|
class CollectionSummonBlueprint < ApiBlueprint
|
|
identifier :id
|
|
|
|
fields :uncap_level, :transcendence_step,
|
|
:created_at, :updated_at
|
|
|
|
association :summon, blueprint: SummonBlueprint, view: :nested
|
|
|
|
view :full do
|
|
association :summon, blueprint: SummonBlueprint, view: :full
|
|
end
|
|
end
|
|
end
|
|
end
|
|
```
|
|
|
|
#### 3.4 CollectionJobAccessoryBlueprint
|
|
|
|
```ruby
|
|
# app/blueprints/api/v1/collection_job_accessory_blueprint.rb
|
|
module Api
|
|
module V1
|
|
class CollectionJobAccessoryBlueprint < ApiBlueprint
|
|
identifier :id
|
|
|
|
fields :created_at, :updated_at
|
|
|
|
association :job_accessory, blueprint: JobAccessoryBlueprint
|
|
end
|
|
end
|
|
end
|
|
```
|
|
|
|
### Step 4: Create Controllers
|
|
|
|
#### 4.1 Base Collection Controller (with User Collection Viewing and Privacy)
|
|
|
|
```ruby
|
|
# app/controllers/api/v1/collection_controller.rb
|
|
module Api
|
|
module V1
|
|
class CollectionController < ApiController
|
|
before_action :set_user
|
|
before_action :check_collection_access
|
|
|
|
# GET /api/v1/users/:user_id/collection
|
|
# GET /api/v1/users/:user_id/collection?type=weapons
|
|
def show
|
|
collection = case params[:type]
|
|
when 'characters'
|
|
{
|
|
characters: CollectionCharacterBlueprint.render_as_hash(
|
|
@user.collection_characters.includes(:character, :awakening),
|
|
view: :full
|
|
)
|
|
}
|
|
when 'weapons'
|
|
{
|
|
weapons: CollectionWeaponBlueprint.render_as_hash(
|
|
@user.collection_weapons.includes(:weapon, :awakening, :weapon_key1,
|
|
:weapon_key2, :weapon_key3, :weapon_key4),
|
|
view: :full
|
|
)
|
|
}
|
|
when 'summons'
|
|
{
|
|
summons: CollectionSummonBlueprint.render_as_hash(
|
|
@user.collection_summons.includes(:summon),
|
|
view: :full
|
|
)
|
|
}
|
|
when 'job_accessories'
|
|
{
|
|
job_accessories: CollectionJobAccessoryBlueprint.render_as_hash(
|
|
@user.collection_job_accessories.includes(job_accessory: :job)
|
|
)
|
|
}
|
|
else
|
|
# Return complete collection
|
|
{
|
|
characters: CollectionCharacterBlueprint.render_as_hash(
|
|
@user.collection_characters.includes(:character, :awakening),
|
|
view: :full
|
|
),
|
|
weapons: CollectionWeaponBlueprint.render_as_hash(
|
|
@user.collection_weapons.includes(:weapon, :awakening, :weapon_key1,
|
|
:weapon_key2, :weapon_key3, :weapon_key4),
|
|
view: :full
|
|
),
|
|
summons: CollectionSummonBlueprint.render_as_hash(
|
|
@user.collection_summons.includes(:summon),
|
|
view: :full
|
|
),
|
|
job_accessories: CollectionJobAccessoryBlueprint.render_as_hash(
|
|
@user.collection_job_accessories.includes(job_accessory: :job)
|
|
)
|
|
}
|
|
end
|
|
|
|
render json: collection
|
|
end
|
|
|
|
def statistics
|
|
stats = @user.collection_statistics
|
|
render json: stats
|
|
end
|
|
|
|
private
|
|
|
|
def set_user
|
|
@user = User.find(params[:user_id])
|
|
rescue ActiveRecord::RecordNotFound
|
|
render json: { error: "User not found" }, status: :not_found
|
|
end
|
|
|
|
def check_collection_access
|
|
unless @user.collection_viewable_by?(current_user)
|
|
render json: { error: "You do not have permission to view this collection" }, status: :forbidden
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
```
|
|
|
|
#### 4.2 CollectionCharactersController
|
|
|
|
```ruby
|
|
# app/controllers/api/v1/collection_characters_controller.rb
|
|
module Api
|
|
module V1
|
|
class CollectionCharactersController < ApiController
|
|
before_action :authenticate_user!
|
|
before_action :set_collection_character, only: [:show, :update, :destroy]
|
|
|
|
def index
|
|
@collection_characters = current_user.collection_characters
|
|
.includes(:character, :awakening)
|
|
.page(params[:page])
|
|
.per(params[:limit] || 50)
|
|
|
|
render json: CollectionCharacterBlueprint.render(
|
|
@collection_characters,
|
|
root: :collection_characters,
|
|
meta: pagination_meta(@collection_characters)
|
|
)
|
|
end
|
|
|
|
def show
|
|
render json: CollectionCharacterBlueprint.render(
|
|
@collection_character,
|
|
view: :full
|
|
)
|
|
end
|
|
|
|
def create
|
|
@collection_character = current_user.collection_characters.build(collection_character_params)
|
|
|
|
if @collection_character.save
|
|
render json: CollectionCharacterBlueprint.render(
|
|
@collection_character,
|
|
view: :full
|
|
), status: :created
|
|
else
|
|
render_errors(@collection_character.errors)
|
|
end
|
|
end
|
|
|
|
def update
|
|
if @collection_character.update(collection_character_params)
|
|
render json: CollectionCharacterBlueprint.render(
|
|
@collection_character,
|
|
view: :full
|
|
)
|
|
else
|
|
render_errors(@collection_character.errors)
|
|
end
|
|
end
|
|
|
|
def destroy
|
|
@collection_character.destroy
|
|
head :no_content
|
|
end
|
|
|
|
private
|
|
|
|
def set_collection_character
|
|
@collection_character = current_user.collection_characters.find(params[:id])
|
|
rescue ActiveRecord::RecordNotFound
|
|
render json: { error: "Collection character not found" }, status: :not_found
|
|
end
|
|
|
|
def collection_character_params
|
|
params.require(:collection_character).permit(
|
|
:character_id, :uncap_level, :transcendence_step, :perpetuity,
|
|
:awakening_id, :awakening_level,
|
|
ring1: [:modifier, :strength],
|
|
ring2: [:modifier, :strength],
|
|
ring3: [:modifier, :strength],
|
|
ring4: [:modifier, :strength],
|
|
earring: [:modifier, :strength]
|
|
)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
```
|
|
|
|
#### 4.3 CollectionWeaponsController
|
|
|
|
```ruby
|
|
# app/controllers/api/v1/collection_weapons_controller.rb
|
|
module Api
|
|
module V1
|
|
class CollectionWeaponsController < ApiController
|
|
before_action :authenticate_user!
|
|
before_action :set_collection_weapon, only: [:show, :update, :destroy]
|
|
|
|
def index
|
|
@collection_weapons = current_user.collection_weapons
|
|
.includes(:weapon, :awakening,
|
|
:weapon_key1, :weapon_key2,
|
|
:weapon_key3, :weapon_key4)
|
|
|
|
@collection_weapons = @collection_weapons.by_weapon(params[:weapon_id]) if params[:weapon_id]
|
|
|
|
@collection_weapons = @collection_weapons.page(params[:page]).per(params[:limit] || 50)
|
|
|
|
render json: CollectionWeaponBlueprint.render(
|
|
@collection_weapons,
|
|
root: :collection_weapons,
|
|
meta: pagination_meta(@collection_weapons)
|
|
)
|
|
end
|
|
|
|
def show
|
|
render json: CollectionWeaponBlueprint.render(
|
|
@collection_weapon,
|
|
view: :full
|
|
)
|
|
end
|
|
|
|
def create
|
|
@collection_weapon = current_user.collection_weapons.build(collection_weapon_params)
|
|
|
|
if @collection_weapon.save
|
|
render json: CollectionWeaponBlueprint.render(
|
|
@collection_weapon,
|
|
view: :full
|
|
), status: :created
|
|
else
|
|
render_errors(@collection_weapon.errors)
|
|
end
|
|
end
|
|
|
|
def update
|
|
if @collection_weapon.update(collection_weapon_params)
|
|
render json: CollectionWeaponBlueprint.render(
|
|
@collection_weapon,
|
|
view: :full
|
|
)
|
|
else
|
|
render_errors(@collection_weapon.errors)
|
|
end
|
|
end
|
|
|
|
def destroy
|
|
@collection_weapon.destroy
|
|
head :no_content
|
|
end
|
|
|
|
private
|
|
|
|
def set_collection_weapon
|
|
@collection_weapon = current_user.collection_weapons.find(params[:id])
|
|
rescue ActiveRecord::RecordNotFound
|
|
render json: { error: "Collection weapon not found" }, status: :not_found
|
|
end
|
|
|
|
def collection_weapon_params
|
|
params.require(:collection_weapon).permit(
|
|
:weapon_id, :uncap_level, :transcendence_step,
|
|
:weapon_key1_id, :weapon_key2_id, :weapon_key3_id, :weapon_key4_id,
|
|
:awakening_id, :awakening_level,
|
|
:ax_modifier1, :ax_strength1, :ax_modifier2, :ax_strength2,
|
|
:element
|
|
)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
```
|
|
|
|
#### 4.4 CollectionSummonsController
|
|
|
|
```ruby
|
|
# app/controllers/api/v1/collection_summons_controller.rb
|
|
module Api
|
|
module V1
|
|
class CollectionSummonsController < ApiController
|
|
before_action :authenticate_user!
|
|
before_action :set_collection_summon, only: [:show, :update, :destroy]
|
|
|
|
def index
|
|
@collection_summons = current_user.collection_summons
|
|
.includes(:summon)
|
|
|
|
@collection_summons = @collection_summons.by_summon(params[:summon_id]) if params[:summon_id]
|
|
|
|
@collection_summons = @collection_summons.page(params[:page]).per(params[:limit] || 50)
|
|
|
|
render json: CollectionSummonBlueprint.render(
|
|
@collection_summons,
|
|
root: :collection_summons,
|
|
meta: pagination_meta(@collection_summons)
|
|
)
|
|
end
|
|
|
|
def show
|
|
render json: CollectionSummonBlueprint.render(
|
|
@collection_summon,
|
|
view: :full
|
|
)
|
|
end
|
|
|
|
def create
|
|
@collection_summon = current_user.collection_summons.build(collection_summon_params)
|
|
|
|
if @collection_summon.save
|
|
render json: CollectionSummonBlueprint.render(
|
|
@collection_summon,
|
|
view: :full
|
|
), status: :created
|
|
else
|
|
render_errors(@collection_summon.errors)
|
|
end
|
|
end
|
|
|
|
def update
|
|
if @collection_summon.update(collection_summon_params)
|
|
render json: CollectionSummonBlueprint.render(
|
|
@collection_summon,
|
|
view: :full
|
|
)
|
|
else
|
|
render_errors(@collection_summon.errors)
|
|
end
|
|
end
|
|
|
|
def destroy
|
|
@collection_summon.destroy
|
|
head :no_content
|
|
end
|
|
|
|
private
|
|
|
|
def set_collection_summon
|
|
@collection_summon = current_user.collection_summons.find(params[:id])
|
|
rescue ActiveRecord::RecordNotFound
|
|
render json: { error: "Collection summon not found" }, status: :not_found
|
|
end
|
|
|
|
def collection_summon_params
|
|
params.require(:collection_summon).permit(
|
|
:summon_id, :uncap_level, :transcendence_step
|
|
)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
```
|
|
|
|
#### 4.5 CollectionJobAccessoriesController
|
|
|
|
```ruby
|
|
# app/controllers/api/v1/collection_job_accessories_controller.rb
|
|
module Api
|
|
module V1
|
|
class CollectionJobAccessoriesController < ApiController
|
|
before_action :authenticate_user!
|
|
before_action :set_collection_job_accessory, only: [:destroy]
|
|
|
|
def index
|
|
@collection_accessories = current_user.collection_job_accessories
|
|
.includes(job_accessory: :job)
|
|
|
|
if params[:job_id]
|
|
@collection_accessories = @collection_accessories.by_job(params[:job_id])
|
|
end
|
|
|
|
@collection_accessories = @collection_accessories.page(params[:page])
|
|
.per(params[:limit] || 50)
|
|
|
|
render json: CollectionJobAccessoryBlueprint.render(
|
|
@collection_accessories,
|
|
root: :collection_job_accessories,
|
|
meta: pagination_meta(@collection_accessories)
|
|
)
|
|
end
|
|
|
|
def create
|
|
@collection_accessory = current_user.collection_job_accessories
|
|
.build(collection_job_accessory_params)
|
|
|
|
if @collection_accessory.save
|
|
render json: CollectionJobAccessoryBlueprint.render(
|
|
@collection_accessory
|
|
), status: :created
|
|
else
|
|
render_errors(@collection_accessory.errors)
|
|
end
|
|
end
|
|
|
|
def destroy
|
|
@collection_job_accessory.destroy
|
|
head :no_content
|
|
end
|
|
|
|
private
|
|
|
|
def set_collection_job_accessory
|
|
@collection_job_accessory = current_user.collection_job_accessories.find(params[:id])
|
|
rescue ActiveRecord::RecordNotFound
|
|
render json: { error: "Collection job accessory not found" }, status: :not_found
|
|
end
|
|
|
|
def collection_job_accessory_params
|
|
params.require(:collection_job_accessory).permit(:job_accessory_id)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
```
|
|
|
|
### Step 5: Update Routes
|
|
|
|
```ruby
|
|
# config/routes.rb - Add these routes within the API scope
|
|
|
|
# User collection viewing (respects privacy settings)
|
|
get 'users/:user_id/collection', to: 'collection#show'
|
|
get 'users/:user_id/collection/statistics', to: 'collection#statistics'
|
|
|
|
# Collection management for current user
|
|
namespace :collection do
|
|
resources :characters, controller: '/api/v1/collection_characters'
|
|
resources :weapons, controller: '/api/v1/collection_weapons'
|
|
resources :summons, controller: '/api/v1/collection_summons'
|
|
resources :job_accessories, controller: '/api/v1/collection_job_accessories',
|
|
only: [:index, :create, :destroy]
|
|
end
|
|
```
|
|
|
|
### Step 6: Add Helper Methods to ApiController
|
|
|
|
```ruby
|
|
# app/controllers/api/v1/api_controller.rb - Add these helper methods
|
|
|
|
protected
|
|
|
|
def pagination_meta(collection)
|
|
{
|
|
current_page: collection.current_page,
|
|
total_pages: collection.total_pages,
|
|
total_count: collection.total_count,
|
|
per_page: collection.limit_value
|
|
}
|
|
end
|
|
|
|
def render_errors(errors, status = :unprocessable_entity)
|
|
render json: { errors: errors.full_messages }, status: status
|
|
end
|
|
```
|
|
|
|
## Testing the Implementation
|
|
|
|
### Manual Testing Steps
|
|
|
|
1. **Start Rails server**
|
|
```bash
|
|
rails server
|
|
```
|
|
|
|
2. **View a user's complete collection**
|
|
```bash
|
|
# Get complete collection
|
|
curl -X GET http://localhost:3000/api/v1/users/USER_ID/collection
|
|
|
|
# Get only weapons
|
|
curl -X GET http://localhost:3000/api/v1/users/USER_ID/collection?type=weapons
|
|
|
|
# Get only characters
|
|
curl -X GET http://localhost:3000/api/v1/users/USER_ID/collection?type=characters
|
|
|
|
# Get only summons
|
|
curl -X GET http://localhost:3000/api/v1/users/USER_ID/collection?type=summons
|
|
|
|
# Get only job accessories
|
|
curl -X GET http://localhost:3000/api/v1/users/USER_ID/collection?type=job_accessories
|
|
```
|
|
|
|
3. **Get collection statistics**
|
|
```bash
|
|
curl -X GET http://localhost:3000/api/v1/users/USER_ID/collection/statistics
|
|
```
|
|
|
|
4. **Create collection items (authenticated)**
|
|
```bash
|
|
# Create a character
|
|
curl -X POST http://localhost:3000/api/v1/collection/characters \
|
|
-H "Authorization: Bearer YOUR_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"collection_character": {"character_id": "uuid", "uncap_level": 3}}'
|
|
|
|
# Create a weapon
|
|
curl -X POST http://localhost:3000/api/v1/collection/weapons \
|
|
-H "Authorization: Bearer YOUR_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"collection_weapon": {"weapon_id": "uuid", "uncap_level": 4}}'
|
|
```
|
|
|
|
## Deployment Checklist
|
|
|
|
- [ ] Run all migrations in staging
|
|
- [ ] Test all endpoints in staging
|
|
- [ ] Verify database indexes are created
|
|
- [ ] Test with large datasets
|
|
- [ ] Set up error tracking (Sentry/Rollbar)
|
|
- [ ] Create backup before deployment
|
|
- [ ] Prepare rollback plan
|
|
- [ ] Update API documentation
|
|
- [ ] Notify frontend team of new endpoints
|
|
- [ ] Schedule deployment during low-traffic window
|
|
- [ ] Monitor application after deployment
|
|
|
|
## API Endpoint Summary
|
|
|
|
### Public Collection Viewing (Respects Privacy Settings)
|
|
- `GET /api/v1/users/:user_id/collection` - View complete collection (if not private)
|
|
- `GET /api/v1/users/:user_id/collection?type=characters` - View characters only (if not private)
|
|
- `GET /api/v1/users/:user_id/collection?type=weapons` - View weapons only (if not private)
|
|
- `GET /api/v1/users/:user_id/collection?type=summons` - View summons only (if not private)
|
|
- `GET /api/v1/users/:user_id/collection?type=job_accessories` - View job accessories only (if not private)
|
|
- `GET /api/v1/users/:user_id/collection/statistics` - View collection statistics (if not private)
|
|
|
|
### Collection Management (Authentication Required)
|
|
- `GET/POST/PUT/DELETE /api/v1/collection/characters` - Manage character collection
|
|
- `GET/POST/PUT/DELETE /api/v1/collection/weapons` - Manage weapon collection
|
|
- `GET/POST/PUT/DELETE /api/v1/collection/summons` - Manage summon collection
|
|
- `GET/POST/DELETE /api/v1/collection/job_accessories` - Manage job accessory collection
|
|
|
|
### Privacy Settings (Authentication Required)
|
|
To update collection privacy settings, use the existing user update endpoint:
|
|
- `PUT /api/v1/users/:id` - Update user settings including `collection_privacy` field
|
|
|
|
Privacy levels:
|
|
- `0` or `"public"`: Collection is viewable by everyone
|
|
- `1` or `"crew_only"`: Collection is viewable only by crew members (when crew feature is implemented)
|
|
- `2` or `"private"`: Collection is viewable only by the owner
|
|
|
|
Example request:
|
|
```json
|
|
{
|
|
"user": {
|
|
"collection_privacy": "crew_only"
|
|
}
|
|
}
|
|
``` |