@startuml ShooterHub Data Model skinparam classAttributeIconSize 0 skinparam classFontSize 12 skinparam packageFontSize 13 skinparam linetype ortho hide empty members ' ══════════════════════════════════════════════ ' PACKAGE: users ' ══════════════════════════════════════════════ package "users" #EBF5FB { class User { + id : int + email : str <> + username : str + display_name : str + language : str + avatar : FK → Photo <> + is_active : bool + is_staff : bool + date_joined : datetime } } ' ══════════════════════════════════════════════ ' PACKAGE: calibers ' ══════════════════════════════════════════════ package "calibers" #E9F7EF { class Caliber { + id : int + name : str + bullet_diameter_mm : decimal + case_length_mm : decimal + status : enum {PENDING, VERIFIED, REJECTED} + submitted_by : FK → User <> + reviewed_by : FK → User <> + created_at : datetime } } ' ══════════════════════════════════════════════ ' PACKAGE: photos ' ══════════════════════════════════════════════ package "photos" #FEF9E7 { class Photo { + id : int + data : bytea + mime_type : str + uploaded_at : datetime + uploaded_by : FK → User <> } class GroupPhoto { + id : int + photo : OneToOne → Photo + shot_group : FK → ShotGroup <> + caption : str + is_public : bool + created_at : datetime } class GroupPhotoAnalysis { + id : int + group_photo : OneToOne → GroupPhoto + group_size_mm : decimal <> + group_size_moa : decimal <> + mean_radius_mm : decimal <> + mean_radius_moa : decimal <> + windage_offset_mm : decimal <> + windage_offset_moa : decimal <> + elevation_offset_mm : decimal <> + elevation_offset_moa : decimal <> + computed_at : datetime } class PointOfImpact { + id : int + group_photo : FK → GroupPhoto + shot : OneToOne → Shot <> + x_px : float + y_px : float } } ' ══════════════════════════════════════════════ ' PACKAGE: tools ' ══════════════════════════════════════════════ package "tools" #F4ECF7 { class ChronographAnalysis { + id : int + user : FK → User <> + label : str + distance_m : decimal <> + is_public : bool + created_at : datetime } class ShotGroup { + id : int + analysis : FK → ChronographAnalysis + user : FK → User <> + label : str + distance_m : decimal <> + ammo : FK → Ammo <> + ammo_batch : FK → ReloadedAmmoBatch <> + firearm : FK → Firearm <> } class Shot { + id : int + group : FK → ShotGroup + velocity_ms : decimal + sequence : int + recorded_at : datetime <> } } ' ══════════════════════════════════════════════ ' PACKAGE: gears ' ══════════════════════════════════════════════ package "gears" #FDEDEC { abstract class Gear { + id : int + brand : str + model : str + status : enum {PENDING, VERIFIED, REJECTED} + submitted_by : FK → User <> + reviewed_by : FK → User <> + created_at : datetime } class Firearm { + gear_ptr : OneToOne → Gear + caliber : FK → Caliber + barrel_length_mm : decimal <> + action : str + firearm_type : str } class Scope { + gear_ptr : OneToOne → Gear + min_magnification : decimal <> + max_magnification : decimal <> + objective_mm : decimal <> + reticle : str + click_value_mrad : decimal <> } class Suppressor { + gear_ptr : OneToOne → Gear + caliber : FK → Caliber <> + length_mm : decimal <> + weight_g : decimal <> } class Bipod { + gear_ptr : OneToOne → Gear + min_height_mm : decimal <> + max_height_mm : decimal <> } class Magazine { + gear_ptr : OneToOne → Gear + caliber : FK → Caliber <> + capacity : int <> } class UserGear { + id : int + user : FK → User + gear : FK → Gear + serial_number : str + notes : str + acquired_at : date <> } class Rig { + id : int + user : FK → User + label : str + is_active : bool + notes : str } class RigItem { + id : int + rig : FK → Rig + user_gear : FK → UserGear + role : str } class Ammo { + id : int + brand : str + name : str + caliber : FK → Caliber <> + bullet_weight_gr : decimal <> + status : enum {PENDING, VERIFIED, REJECTED} + submitted_by : FK → User <> } abstract class ComponentMixin { + id : int + brand : str + name : str + status : enum {PENDING, VERIFIED, REJECTED} + submitted_by : FK → User <> } class Primer { + type : str } class Brass { + caliber : FK → Caliber <> } class Bullet { + caliber : FK → Caliber <> + weight_gr : decimal <> + bc_g7 : decimal <> + bc_g1 : decimal <> } class Powder { + burn_rate : str } class ReloadRecipe { + id : int + user : FK → User + name : str + caliber : FK → Caliber + bullet : FK → Bullet + primer : FK → Primer <> + brass : FK → Brass <> + powder : FK → Powder + powder_charge_gr : decimal + coal_mm : decimal <> + notes : str + created_at : datetime } class ReloadedAmmoBatch { + id : int + recipe : FK → ReloadRecipe + user : FK → User + quantity : int + lot_number : str + produced_at : date + notes : str } } ' ══════════════════════════════════════════════ ' PACKAGE: sessions ' ══════════════════════════════════════════════ package "sessions" #EBF5FB { abstract class AbstractSession { + id : int + user : FK → User + date : date + label : str + location : str + weather : str + notes : str + is_public : bool + firearm : FK → Firearm <> + ammo : FK → Ammo <> + ammo_batch : FK → ReloadedAmmoBatch <> + created_at : datetime } class PRSSession { + total_points : int <> + max_points : int <> } class PRSStage { + id : int + session : FK → PRSSession + stage_number : int + points_scored : int <> + max_points : int <> + distance_m : decimal <> + notes : str } class FreePracticeSession { + distance_m : decimal <> + round_count : int <> } class SpeedShootingSession { + target_count : int <> + time_seconds : decimal <> } } ' ══════════════════════════════════════════════ ' PACKAGE: social ' ══════════════════════════════════════════════ package "social" #FEF9E7 { class Message { + id : int + sender : FK → User + recipient : FK → User + subject : str + body : text + sent_at : datetime + read_at : datetime <> + deleted_by_sender : bool + deleted_by_recipient : bool } class BlogPost { + id : int + author : FK → User + title : str + body : text + is_public : bool + created_at : datetime + updated_at : datetime } class Bug { + id : int + reporter : FK → User <> + title : str + description : text + severity : enum {LOW, MEDIUM, HIGH, CRITICAL} + status : enum {OPEN, IN_PROGRESS, RESOLVED, CLOSED} + created_at : datetime + resolved_at : datetime <> } class Friendship { + id : int + from_user : FK → User + to_user : FK → User + status : enum {PENDING, ACCEPTED, BLOCKED} + created_at : datetime + <> (from_user, to_user) } } ' ══════════════════════════════════════════════ ' RELATIONSHIPS ' ══════════════════════════════════════════════ ' users ↔ photos User "0..1" -- "0..1" Photo : avatar > ' users → calibers User "1" --> "0..*" Caliber : submitted_by > User "0..1" --> "0..*" Caliber : reviewed_by > ' photos internal Photo "1" <-- "0..1" GroupPhoto : photo GroupPhoto "1" *-- "0..1" GroupPhotoAnalysis : group_photo GroupPhoto "1" *-- "0..*" PointOfImpact : group_photo ' photos ↔ tools ShotGroup "0..1" <-- "0..*" GroupPhoto : shot_group Shot "0..1" <-- "0..1" PointOfImpact : shot ' users → photos User "0..1" --> "0..*" Photo : uploaded_by > ' tools internal ChronographAnalysis "1" *-- "0..*" ShotGroup : analysis ShotGroup "1" *-- "0..*" Shot : group ' tools ↔ users User "0..1" --> "0..*" ChronographAnalysis : user > User "0..1" --> "0..*" ShotGroup : user > ' tools ↔ gears Ammo "0..1" <-- "0..*" ShotGroup : ammo ReloadedAmmoBatch "0..1" <-- "0..*" ShotGroup : ammo_batch Firearm "0..1" <-- "0..*" ShotGroup : firearm ' gears inheritance (MTI) Gear <|-- Firearm Gear <|-- Scope Gear <|-- Suppressor Gear <|-- Bipod Gear <|-- Magazine ' component inheritance (abstract) ComponentMixin <|-- Primer ComponentMixin <|-- Brass ComponentMixin <|-- Bullet ComponentMixin <|-- Powder ' gears ↔ calibers Caliber "0..1" <-- "0..*" Firearm : caliber Caliber "0..1" <-- "0..*" Suppressor : caliber Caliber "0..1" <-- "0..*" Magazine : caliber Caliber "1" <-- "0..*" Ammo : caliber Caliber "0..1" <-- "0..*" Brass : caliber Caliber "0..1" <-- "0..*" Bullet : caliber Caliber "1" <-- "0..*" ReloadRecipe : caliber ' gears ↔ users User "1" --> "0..*" UserGear : user > User "1" --> "0..*" Rig : user > Gear "1" <-- "0..*" UserGear : gear Rig "1" *-- "0..*" RigItem : rig UserGear "1" <-- "0..*" RigItem : user_gear ' reload chain User "1" --> "0..*" ReloadRecipe : user > User "1" --> "0..*" ReloadedAmmoBatch : user > ReloadRecipe "1" *-- "0..*" ReloadedAmmoBatch : recipe ReloadRecipe "1" --> "1" Bullet : bullet > ReloadRecipe "0..1" --> "0..1" Primer : primer > ReloadRecipe "0..1" --> "0..1" Brass : brass > ReloadRecipe "1" --> "1" Powder : powder > ' sessions inheritance AbstractSession <|-- PRSSession AbstractSession <|-- FreePracticeSession AbstractSession <|-- SpeedShootingSession PRSSession "1" *-- "0..*" PRSStage : session ' sessions ↔ users/gears User "1" --> "0..*" AbstractSession : user > Firearm "0..1" <-- "0..*" AbstractSession : firearm Ammo "0..1" <-- "0..*" AbstractSession : ammo ReloadedAmmoBatch "0..1" <-- "0..*" AbstractSession : ammo_batch ' social ↔ users User "1" --> "0..*" Message : sender > User "1" --> "0..*" Message : recipient > User "1" --> "0..*" BlogPost : author > User "0..1" --> "0..*" Bug : reporter > User "1" --> "0..*" Friendship : from_user > User "1" --> "0..*" Friendship : to_user > @enduml