Files
ShooterHub/models.puml

444 lines
12 KiB
Plaintext
Raw Normal View History

@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 <<unique>>
+ username : str
+ display_name : str
+ language : str
+ avatar : FK → Photo <<nullable>>
+ 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 <<nullable>>
+ reviewed_by : FK → User <<nullable>>
+ created_at : datetime
}
}
' ══════════════════════════════════════════════
' PACKAGE: photos
' ══════════════════════════════════════════════
package "photos" #FEF9E7 {
class Photo {
+ id : int
+ data : bytea
+ mime_type : str
+ uploaded_at : datetime
+ uploaded_by : FK → User <<nullable>>
}
class GroupPhoto {
+ id : int
+ photo : OneToOne → Photo
+ shot_group : FK → ShotGroup <<nullable>>
+ caption : str
+ is_public : bool
+ created_at : datetime
}
class GroupPhotoAnalysis {
+ id : int
+ group_photo : OneToOne → GroupPhoto
+ group_size_mm : decimal <<nullable>>
+ group_size_moa : decimal <<nullable>>
+ mean_radius_mm : decimal <<nullable>>
+ mean_radius_moa : decimal <<nullable>>
+ windage_offset_mm : decimal <<nullable>>
+ windage_offset_moa : decimal <<nullable>>
+ elevation_offset_mm : decimal <<nullable>>
+ elevation_offset_moa : decimal <<nullable>>
+ computed_at : datetime
}
class PointOfImpact {
+ id : int
+ group_photo : FK → GroupPhoto
+ shot : OneToOne → Shot <<nullable>>
+ x_px : float
+ y_px : float
}
}
' ══════════════════════════════════════════════
' PACKAGE: tools
' ══════════════════════════════════════════════
package "tools" #F4ECF7 {
class ChronographAnalysis {
+ id : int
+ user : FK → User <<nullable>>
+ label : str
+ distance_m : decimal <<nullable>>
+ is_public : bool
+ created_at : datetime
}
class ShotGroup {
+ id : int
+ analysis : FK → ChronographAnalysis
+ user : FK → User <<nullable>>
+ label : str
+ distance_m : decimal <<nullable>>
+ ammo : FK → Ammo <<nullable>>
+ ammo_batch : FK → ReloadedAmmoBatch <<nullable>>
+ firearm : FK → Firearm <<nullable>>
}
class Shot {
+ id : int
+ group : FK → ShotGroup
+ velocity_ms : decimal
+ sequence : int
+ recorded_at : datetime <<nullable>>
}
}
' ══════════════════════════════════════════════
' PACKAGE: gears
' ══════════════════════════════════════════════
package "gears" #FDEDEC {
abstract class Gear {
+ id : int
+ brand : str
+ model : str
+ status : enum {PENDING, VERIFIED, REJECTED}
+ submitted_by : FK → User <<nullable>>
+ reviewed_by : FK → User <<nullable>>
+ created_at : datetime
}
class Firearm {
+ gear_ptr : OneToOne → Gear
+ caliber : FK → Caliber
+ barrel_length_mm : decimal <<nullable>>
+ action : str
+ firearm_type : str
}
class Scope {
+ gear_ptr : OneToOne → Gear
+ min_magnification : decimal <<nullable>>
+ max_magnification : decimal <<nullable>>
+ objective_mm : decimal <<nullable>>
+ reticle : str
+ click_value_mrad : decimal <<nullable>>
}
class Suppressor {
+ gear_ptr : OneToOne → Gear
+ caliber : FK → Caliber <<nullable>>
+ length_mm : decimal <<nullable>>
+ weight_g : decimal <<nullable>>
}
class Bipod {
+ gear_ptr : OneToOne → Gear
+ min_height_mm : decimal <<nullable>>
+ max_height_mm : decimal <<nullable>>
}
class Magazine {
+ gear_ptr : OneToOne → Gear
+ caliber : FK → Caliber <<nullable>>
+ capacity : int <<nullable>>
}
class UserGear {
+ id : int
+ user : FK → User
+ gear : FK → Gear
+ serial_number : str
+ notes : str
+ acquired_at : date <<nullable>>
}
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 <<nullable>>
+ bullet_weight_gr : decimal <<nullable>>
+ status : enum {PENDING, VERIFIED, REJECTED}
+ submitted_by : FK → User <<nullable>>
}
abstract class ComponentMixin {
+ id : int
+ brand : str
+ name : str
+ status : enum {PENDING, VERIFIED, REJECTED}
+ submitted_by : FK → User <<nullable>>
}
class Primer {
+ type : str
}
class Brass {
+ caliber : FK → Caliber <<nullable>>
}
class Bullet {
+ caliber : FK → Caliber <<nullable>>
+ weight_gr : decimal <<nullable>>
+ bc_g7 : decimal <<nullable>>
+ bc_g1 : decimal <<nullable>>
}
class Powder {
+ burn_rate : str
}
class ReloadRecipe {
+ id : int
+ user : FK → User
+ name : str
+ caliber : FK → Caliber
+ bullet : FK → Bullet
+ primer : FK → Primer <<nullable>>
+ brass : FK → Brass <<nullable>>
+ powder : FK → Powder
+ powder_charge_gr : decimal
+ coal_mm : decimal <<nullable>>
+ 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 <<nullable>>
+ ammo : FK → Ammo <<nullable>>
+ ammo_batch : FK → ReloadedAmmoBatch <<nullable>>
+ created_at : datetime
}
class PRSSession {
+ total_points : int <<nullable>>
+ max_points : int <<nullable>>
}
class PRSStage {
+ id : int
+ session : FK → PRSSession
+ stage_number : int
+ points_scored : int <<nullable>>
+ max_points : int <<nullable>>
+ distance_m : decimal <<nullable>>
+ notes : str
}
class FreePracticeSession {
+ distance_m : decimal <<nullable>>
+ round_count : int <<nullable>>
}
class SpeedShootingSession {
+ target_count : int <<nullable>>
+ time_seconds : decimal <<nullable>>
}
}
' ══════════════════════════════════════════════
' 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 <<nullable>>
+ 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 <<nullable>>
+ title : str
+ description : text
+ severity : enum {LOW, MEDIUM, HIGH, CRITICAL}
+ status : enum {OPEN, IN_PROGRESS, RESOLVED, CLOSED}
+ created_at : datetime
+ resolved_at : datetime <<nullable>>
}
class Friendship {
+ id : int
+ from_user : FK → User
+ to_user : FK → User
+ status : enum {PENDING, ACCEPTED, BLOCKED}
+ created_at : datetime
+ <<unique>> (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