add party_shares table and model with associations

This commit is contained in:
Justin Edmund 2026-01-04 21:47:02 -08:00
parent c3d9efa349
commit 329f86df20
7 changed files with 140 additions and 2 deletions

View file

@ -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

View file

@ -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.
#

63
app/models/party_share.rb Normal file
View file

@ -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

View file

@ -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,

View file

@ -1 +1 @@
DataMigrate::Data.define(version: 20251230000002)
DataMigrate::Data.define(version: 20260104000002)

View file

@ -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

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_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"