From 329f86df20592fd2e7ecd43cabaca426d86622cc Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Sun, 4 Jan 2026 21:47:02 -0800 Subject: [PATCH] add party_shares table and model with associations --- app/models/crew.rb | 2 + app/models/party.rb | 34 ++++++++++ app/models/party_share.rb | 63 +++++++++++++++++++ app/models/user.rb | 1 + db/data_schema.rb | 2 +- .../20260105053753_create_party_shares.rb | 21 +++++++ db/schema.rb | 19 +++++- 7 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 app/models/party_share.rb create mode 100644 db/migrate/20260105053753_create_party_shares.rb diff --git a/app/models/crew.rb b/app/models/crew.rb index 915c14a..c59ceae 100644 --- a/app/models/crew.rb +++ b/app/models/crew.rb @@ -10,6 +10,8 @@ class Crew < ApplicationRecord has_many :crew_gw_participations, dependent: :destroy has_many :gw_events, through: :crew_gw_participations has_many :phantom_players, dependent: :destroy + has_many :party_shares, as: :shareable, dependent: :destroy + has_many :shared_parties, through: :party_shares, source: :party validates :name, presence: true, length: { maximum: 100 } validates :gamertag, length: { maximum: 50 }, allow_nil: true diff --git a/app/models/party.rb b/app/models/party.rb index 17780e6..bd55f36 100644 --- a/app/models/party.rb +++ b/app/models/party.rb @@ -156,6 +156,8 @@ class Party < ApplicationRecord inverse_of: :party has_many :favorites, dependent: :destroy + has_many :party_shares, dependent: :destroy + has_many :shared_crews, through: :party_shares, source: :shareable, source_type: 'Crew' accepts_nested_attributes_for :characters accepts_nested_attributes_for :summons @@ -261,6 +263,38 @@ class Party < ApplicationRecord visibility == 3 end + ## + # Checks if the party is shared with a specific crew. + # + # @param crew [Crew] the crew to check. + # @return [Boolean] true if shared with the crew; false otherwise. + def shared_with_crew?(crew) + return false unless crew + + party_shares.exists?(shareable_type: 'Crew', shareable_id: crew.id) + end + + ## + # Checks if a user can view this party based on visibility and sharing rules. + # A user can view if: + # - The party is public + # - The party is unlisted (accessible via direct link) + # - They own the party + # - They are an admin + # - The party is shared with a crew they belong to + # + # @param user [User, nil] the user to check. + # @return [Boolean] true if the user can view the party; false otherwise. + def viewable_by?(user) + return true if public? + return true if unlisted? + return true if user && user_id == user.id + return true if user&.admin? + return true if user&.crew && shared_with_crew?(user.crew) + + false + end + ## # Checks if the party is favorited by a given user. # diff --git a/app/models/party_share.rb b/app/models/party_share.rb new file mode 100644 index 0000000..ef735dc --- /dev/null +++ b/app/models/party_share.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +## +# PartyShare represents a sharing relationship between a party and a group (e.g., a crew). +# It allows party owners to share their parties with specific groups, granting view access +# to members of those groups without changing the party's base visibility. +# +# @!attribute [rw] party +# @return [Party] the party being shared. +# @!attribute [rw] shareable +# @return [Crew] the polymorphic group the party is shared with. +# @!attribute [rw] shared_by +# @return [User] the user who created this share. +class PartyShare < ApplicationRecord + # Associations + belongs_to :party + belongs_to :shareable, polymorphic: true + belongs_to :shared_by, class_name: 'User' + + # Validations + validates :party_id, uniqueness: { + scope: [:shareable_type, :shareable_id], + message: 'has already been shared with this group' + } + validate :owner_can_share + validate :sharer_is_member_of_shareable + + # Scopes + scope :for_crew, ->(crew) { where(shareable_type: 'Crew', shareable_id: crew.id) } + scope :for_crews, ->(crew_ids) { where(shareable_type: 'Crew', shareable_id: crew_ids) } + scope :for_party, ->(party) { where(party_id: party.id) } + + ## + # Returns the blueprint class for serialization. + # + # @return [Class] the PartyShareBlueprint class. + def blueprint + PartyShareBlueprint + end + + private + + ## + # Validates that only the party owner can share the party. + # + # @return [void] + def owner_can_share + return if party&.user_id == shared_by_id + + errors.add(:shared_by, 'must be the party owner') + end + + ## + # Validates that the sharer is a member of the group they're sharing to. + # + # @return [void] + def sharer_is_member_of_shareable + return unless shareable_type == 'Crew' + return if shareable&.active_memberships&.exists?(user_id: shared_by_id) + + errors.add(:shareable, 'you must be a member of this crew') + end +end diff --git a/app/models/user.rb b/app/models/user.rb index ac2c8aa..9f24428 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -19,6 +19,7 @@ class User < ApplicationRecord has_many :crew_invitations, dependent: :destroy has_many :pending_crew_invitations, -> { where(status: :pending) }, class_name: 'CrewInvitation' has_many :sent_crew_invitations, class_name: 'CrewInvitation', foreign_key: :invited_by_id, dependent: :nullify + has_many :party_shares, foreign_key: :shared_by_id, dependent: :destroy ##### ActiveRecord Validations validates :username, diff --git a/db/data_schema.rb b/db/data_schema.rb index bb71a21..bc95d29 100644 --- a/db/data_schema.rb +++ b/db/data_schema.rb @@ -1 +1 @@ -DataMigrate::Data.define(version: 20251230000002) +DataMigrate::Data.define(version: 20260104000002) diff --git a/db/migrate/20260105053753_create_party_shares.rb b/db/migrate/20260105053753_create_party_shares.rb new file mode 100644 index 0000000..a1de39a --- /dev/null +++ b/db/migrate/20260105053753_create_party_shares.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class CreatePartyShares < ActiveRecord::Migration[8.0] + def change + create_table :party_shares, id: :uuid do |t| + t.references :party, type: :uuid, null: false, foreign_key: true + t.references :shareable, type: :uuid, null: false, polymorphic: true + t.references :shared_by, type: :uuid, null: false, foreign_key: { to_table: :users } + + t.timestamps + end + + # Prevent duplicate shares of the same party to the same group + add_index :party_shares, [:party_id, :shareable_type, :shareable_id], + unique: true, + name: 'index_party_shares_unique_per_shareable' + + # Quick lookup of all parties shared with a specific group + add_index :party_shares, [:shareable_type, :shareable_id] + end +end diff --git a/db/schema.rb b/db/schema.rb index 9442424..7bcee92 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_30_000004) do +ActiveRecord::Schema[8.0].define(version: 2026_01_05_053753) do # These are extensions that must be enabled in order to support this database enable_extension "btree_gin" enable_extension "pg_catalog.plpgsql" @@ -677,6 +677,20 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_30_000004) do t.index ["weapons_count", "characters_count", "summons_count"], name: "index_parties_on_counters" end + create_table "party_shares", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.uuid "party_id", null: false + t.string "shareable_type", null: false + t.uuid "shareable_id", null: false + t.uuid "shared_by_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["party_id", "shareable_type", "shareable_id"], name: "index_party_shares_unique_per_shareable", unique: true + t.index ["party_id"], name: "index_party_shares_on_party_id" + t.index ["shareable_type", "shareable_id"], name: "index_party_shares_on_shareable" + t.index ["shareable_type", "shareable_id"], name: "index_party_shares_on_shareable_type_and_shareable_id" + t.index ["shared_by_id"], name: "index_party_shares_on_shared_by_id" + end + create_table "pg_search_documents", force: :cascade do |t| t.text "content" t.string "granblue_id" @@ -1029,6 +1043,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_30_000004) do t.string "forged_from" t.uuid "forge_chain_id" t.integer "forge_order" + t.integer "max_exorcism_level" t.index ["forge_chain_id"], name: "index_weapons_on_forge_chain_id" t.index ["forged_from"], name: "index_weapons_on_forged_from" t.index ["gacha"], name: "index_weapons_on_gacha" @@ -1111,6 +1126,8 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_30_000004) do add_foreign_key "parties", "parties", column: "source_party_id" add_foreign_key "parties", "raids" add_foreign_key "parties", "users" + add_foreign_key "party_shares", "parties" + add_foreign_key "party_shares", "users", column: "shared_by_id" add_foreign_key "phantom_players", "crew_memberships", column: "claimed_from_membership_id" add_foreign_key "phantom_players", "crews" add_foreign_key "phantom_players", "users", column: "claimed_by_id"