From 9b01aa0ff3ae22ce2f3ceab6c10a890fa72f8108 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Wed, 3 Dec 2025 22:41:19 -0800 Subject: [PATCH] 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 --- app/models/crew.rb | 32 +++++++++++++ app/models/crew_membership.rb | 48 +++++++++++++++++++ app/models/user.rb | 35 ++++++++++---- db/migrate/20251204063628_create_crews.rb | 15 ++++++ .../20251204063649_create_crew_memberships.rb | 20 ++++++++ ...251204063711_add_show_gamertag_to_users.rb | 5 ++ db/schema.rb | 31 +++++++++++- 7 files changed, 175 insertions(+), 11 deletions(-) create mode 100644 app/models/crew.rb create mode 100644 app/models/crew_membership.rb create mode 100644 db/migrate/20251204063628_create_crews.rb create mode 100644 db/migrate/20251204063649_create_crew_memberships.rb create mode 100644 db/migrate/20251204063711_add_show_gamertag_to_users.rb diff --git a/app/models/crew.rb b/app/models/crew.rb new file mode 100644 index 0000000..c075ab8 --- /dev/null +++ b/app/models/crew.rb @@ -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 diff --git a/app/models/crew_membership.rb b/app/models/crew_membership.rb new file mode 100644 index 0000000..cc0b749 --- /dev/null +++ b/app/models/crew_membership.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index 2b8eea3..502490a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 \ No newline at end of file diff --git a/db/migrate/20251204063628_create_crews.rb b/db/migrate/20251204063628_create_crews.rb new file mode 100644 index 0000000..70a3701 --- /dev/null +++ b/db/migrate/20251204063628_create_crews.rb @@ -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 diff --git a/db/migrate/20251204063649_create_crew_memberships.rb b/db/migrate/20251204063649_create_crew_memberships.rb new file mode 100644 index 0000000..b73fa19 --- /dev/null +++ b/db/migrate/20251204063649_create_crew_memberships.rb @@ -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 diff --git a/db/migrate/20251204063711_add_show_gamertag_to_users.rb b/db/migrate/20251204063711_add_show_gamertag_to_users.rb new file mode 100644 index 0000000..5fb22d4 --- /dev/null +++ b/db/migrate/20251204063711_add_show_gamertag_to_users.rb @@ -0,0 +1,5 @@ +class AddShowGamertagToUsers < ActiveRecord::Migration[8.0] + def change + add_column :users, :show_gamertag, :boolean, default: true, null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index fa89fb4..680dc16 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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"