add crew and crew_membership models with migrations

- crews table with name, gamertag, granblue_crew_id, description
- crew_memberships with role enum (member/vice_captain/captain)
- partial unique index ensures one active crew per user
- updated User model with crew associations and helper methods
This commit is contained in:
Justin Edmund 2025-12-03 22:41:19 -08:00
parent 35b8a674ab
commit 9b01aa0ff3
7 changed files with 175 additions and 11 deletions

32
app/models/crew.rb Normal file
View file

@ -0,0 +1,32 @@
# frozen_string_literal: true
class Crew < ApplicationRecord
has_many :crew_memberships, dependent: :destroy
has_many :users, through: :crew_memberships
has_many :active_memberships, -> { where(retired: false) }, class_name: 'CrewMembership'
has_many :active_members, through: :active_memberships, source: :user
validates :name, presence: true, length: { maximum: 100 }
validates :gamertag, length: { maximum: 50 }, allow_nil: true
validates :granblue_crew_id, uniqueness: true, allow_nil: true
def captain
crew_memberships.find_by(role: :captain, retired: false)&.user
end
def vice_captains
crew_memberships.where(role: :vice_captain, retired: false).includes(:user).map(&:user)
end
def officers
crew_memberships.where(role: [:captain, :vice_captain], retired: false).includes(:user).map(&:user)
end
def member_count
active_memberships.count
end
def blueprint
CrewBlueprint
end
end

View file

@ -0,0 +1,48 @@
# frozen_string_literal: true
class CrewMembership < ApplicationRecord
belongs_to :crew
belongs_to :user
enum :role, { member: 0, vice_captain: 1, captain: 2 }
validates :user_id, uniqueness: { scope: :crew_id }
validate :one_active_crew_per_user, on: :create
validate :captain_limit
validate :vice_captain_limit
scope :active, -> { where(retired: false) }
scope :retired, -> { where(retired: true) }
def retire!
update!(retired: true, retired_at: Time.current, role: :member)
end
def blueprint
CrewMembershipBlueprint
end
private
def one_active_crew_per_user
return if retired
if CrewMembership.where(user_id: user_id, retired: false).where.not(id: id).exists?
errors.add(:user, 'can only be in one active crew')
end
end
def captain_limit
return unless captain? && !retired
existing = crew.crew_memberships.where(role: :captain, retired: false).where.not(id: id)
errors.add(:role, 'crew can only have one captain') if existing.exists?
end
def vice_captain_limit
return unless vice_captain? && !retired
existing = crew.crew_memberships.where(role: :vice_captain, retired: false).where.not(id: id)
errors.add(:role, 'crew can only have up to 3 vice captains') if existing.count >= 3
end
end

View file

@ -12,8 +12,10 @@ class User < ApplicationRecord
has_many :collection_job_accessories, dependent: :destroy
has_many :collection_artifacts, dependent: :destroy
# Note: The crew association will be added when crews feature is implemented
# belongs_to :crew, optional: true
# Crew associations
has_many :crew_memberships, dependent: :destroy
has_one :active_crew_membership, -> { where(retired: false) }, class_name: 'CrewMembership'
has_one :crew, through: :active_crew_membership
##### ActiveRecord Validations
validates :username,
@ -76,9 +78,7 @@ class User < ApplicationRecord
when 'everyone'
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
viewer.present? && in_same_crew_as?(viewer)
when 'private_collection'
false
else
@ -86,11 +86,26 @@ class User < ApplicationRecord
end
end
# Helper method to check if user is in same crew (placeholder for future)
# Check if user is in same crew as another user
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
return false unless other_user.present?
return false unless crew.present? && other_user.crew.present?
crew.id == other_user.crew.id
end
# Get the user's crew role
def crew_role
active_crew_membership&.role
end
# Check if user is a crew officer (captain or vice captain)
def crew_officer?
crew_role.in?(%w[captain vice_captain])
end
# Check if user is a crew captain
def crew_captain?
crew_role == 'captain'
end
end

View file

@ -0,0 +1,15 @@
class CreateCrews < ActiveRecord::Migration[8.0]
def change
create_table :crews, id: :uuid do |t|
t.string :name, null: false
t.string :gamertag
t.string :granblue_crew_id
t.text :description
t.timestamps
end
add_index :crews, :name
add_index :crews, :granblue_crew_id, unique: true, where: "granblue_crew_id IS NOT NULL"
end
end

View file

@ -0,0 +1,20 @@
class CreateCrewMemberships < ActiveRecord::Migration[8.0]
def change
create_table :crew_memberships, id: :uuid do |t|
t.references :crew, type: :uuid, null: false, foreign_key: true
t.references :user, type: :uuid, null: false, foreign_key: true
t.integer :role, default: 0, null: false
t.boolean :retired, default: false, null: false
t.datetime :retired_at
t.timestamps
end
add_index :crew_memberships, [:crew_id, :user_id], unique: true
add_index :crew_memberships, [:crew_id, :role]
add_index :crew_memberships, [:user_id],
unique: true,
where: "retired = false",
name: "index_crew_memberships_on_active_user"
end
end

View file

@ -0,0 +1,5 @@
class AddShowGamertagToUsers < ActiveRecord::Migration[8.0]
def change
add_column :users, :show_gamertag, :boolean, default: true, null: false
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2025_12_03_221115) do
ActiveRecord::Schema[8.0].define(version: 2025_12_04_063711) do
# These are extensions that must be enabled in order to support this database
enable_extension "btree_gin"
enable_extension "pg_catalog.plpgsql"
@ -234,6 +234,32 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_03_221115) do
t.index ["weapon_key4_id"], name: "index_collection_weapons_on_weapon_key4_id"
end
create_table "crew_memberships", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.uuid "crew_id", null: false
t.uuid "user_id", null: false
t.integer "role", default: 0, null: false
t.boolean "retired", default: false, null: false
t.datetime "retired_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["crew_id", "role"], name: "index_crew_memberships_on_crew_id_and_role"
t.index ["crew_id", "user_id"], name: "index_crew_memberships_on_crew_id_and_user_id", unique: true
t.index ["crew_id"], name: "index_crew_memberships_on_crew_id"
t.index ["user_id"], name: "index_crew_memberships_on_active_user", unique: true, where: "(retired = false)"
t.index ["user_id"], name: "index_crew_memberships_on_user_id"
end
create_table "crews", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.string "name", null: false
t.string "gamertag"
t.string "granblue_crew_id"
t.text "description"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["granblue_crew_id"], name: "index_crews_on_granblue_crew_id", unique: true, where: "(granblue_crew_id IS NOT NULL)"
t.index ["name"], name: "index_crews_on_name"
end
create_table "data_migrations", primary_key: "version", id: :string, force: :cascade do |t|
end
@ -718,6 +744,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_03_221115) do
t.string "theme", default: "system", null: false
t.integer "role", default: 1, null: false
t.integer "collection_privacy", default: 0, null: false
t.boolean "show_gamertag", default: true, null: false
t.index ["collection_privacy"], name: "index_users_on_collection_privacy"
end
@ -847,6 +874,8 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_03_221115) do
add_foreign_key "collection_weapons", "weapon_keys", column: "weapon_key3_id"
add_foreign_key "collection_weapons", "weapon_keys", column: "weapon_key4_id"
add_foreign_key "collection_weapons", "weapons"
add_foreign_key "crew_memberships", "crews"
add_foreign_key "crew_memberships", "users"
add_foreign_key "effects", "effects", column: "effect_family_id"
add_foreign_key "favorites", "parties"
add_foreign_key "favorites", "users"