444 lines
12 KiB
Plaintext
444 lines
12 KiB
Plaintext
|
|
@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
|