158 lines
5.5 KiB
Python
158 lines
5.5 KiB
Python
from django.conf import settings
|
|
from django.core.exceptions import ValidationError
|
|
from django.db import models
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
|
|
# ── ChronographAnalysis ───────────────────────────────────────────────────────
|
|
|
|
class ChronographAnalysis(models.Model):
|
|
"""
|
|
A velocity recording session composed of one or more ShotGroups.
|
|
Can be used anonymously or by an authenticated user.
|
|
"""
|
|
user = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
null=True, blank=True,
|
|
on_delete=models.SET_NULL,
|
|
related_name='chronograph_analyses',
|
|
verbose_name=_('user'),
|
|
)
|
|
name = models.CharField(_('name'), max_length=150)
|
|
date = models.DateField(_('date'))
|
|
notes = models.TextField(_('notes'), blank=True)
|
|
is_public = models.BooleanField(_('public'), default=False)
|
|
created_at = models.DateTimeField(_('created at'), auto_now_add=True)
|
|
updated_at = models.DateTimeField(_('updated at'), auto_now=True)
|
|
|
|
class Meta:
|
|
verbose_name = _('chronograph analysis')
|
|
verbose_name_plural = _('chronograph analyses')
|
|
ordering = ['-date', '-created_at']
|
|
|
|
def __str__(self):
|
|
owner = self.user.email if self.user_id else _('anonymous')
|
|
return f"{self.name} ({owner})"
|
|
|
|
def clean(self):
|
|
if not self.name or not self.name.strip():
|
|
raise ValidationError({'name': _('Name may not be blank.')})
|
|
|
|
|
|
# ── ShotGroup ─────────────────────────────────────────────────────────────────
|
|
|
|
class ShotGroup(models.Model):
|
|
"""
|
|
A named group of shots. Can be nested under a ChronographAnalysis
|
|
or exist as a standalone group (analysis=None).
|
|
"""
|
|
# Optional link to a chronograph session; SET_NULL keeps the group alive
|
|
# when an analysis is deleted.
|
|
analysis = models.ForeignKey(
|
|
ChronographAnalysis,
|
|
null=True, blank=True,
|
|
on_delete=models.SET_NULL,
|
|
related_name='shot_groups',
|
|
verbose_name=_('analysis'),
|
|
)
|
|
# Owner for standalone groups (groups nested under an analysis inherit
|
|
# ownership via analysis.user).
|
|
user = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
null=True, blank=True,
|
|
on_delete=models.SET_NULL,
|
|
related_name='shot_groups',
|
|
verbose_name=_('user'),
|
|
)
|
|
label = models.CharField(_('label'), max_length=100)
|
|
distance_m = models.DecimalField(
|
|
_('distance (m)'),
|
|
max_digits=7, decimal_places=1,
|
|
null=True, blank=True,
|
|
)
|
|
order = models.PositiveSmallIntegerField(_('order'), default=0)
|
|
notes = models.TextField(_('notes'), blank=True)
|
|
# Intentional cross-app FKs: tools → gears (string refs avoid circular imports)
|
|
ammo_batch = models.ForeignKey(
|
|
'gears.ReloadedAmmoBatch',
|
|
null=True, blank=True,
|
|
on_delete=models.SET_NULL,
|
|
related_name='shot_groups',
|
|
verbose_name=_('reloaded ammo batch'),
|
|
)
|
|
ammo = models.ForeignKey(
|
|
'gears.Ammo',
|
|
null=True, blank=True,
|
|
on_delete=models.SET_NULL,
|
|
related_name='shot_groups',
|
|
verbose_name=_('factory ammo'),
|
|
)
|
|
|
|
class Meta:
|
|
verbose_name = _('shot group')
|
|
verbose_name_plural = _('shot groups')
|
|
ordering = ['order', 'id']
|
|
|
|
def __str__(self):
|
|
prefix = self.analysis.name if self.analysis_id else _('Standalone')
|
|
return f"{prefix} / {self.label}"
|
|
|
|
def clean(self):
|
|
if self.distance_m is not None and self.distance_m <= 0:
|
|
raise ValidationError(
|
|
{'distance_m': _('Distance must be a positive value.')}
|
|
)
|
|
|
|
|
|
# ── Shot ──────────────────────────────────────────────────────────────────────
|
|
|
|
class Shot(models.Model):
|
|
"""A single bullet velocity reading within a ShotGroup."""
|
|
group = models.ForeignKey(
|
|
ShotGroup,
|
|
on_delete=models.CASCADE,
|
|
related_name='shots',
|
|
verbose_name=_('group'),
|
|
)
|
|
shot_number = models.PositiveSmallIntegerField(
|
|
_('shot number'),
|
|
editable=False,
|
|
)
|
|
velocity_fps = models.DecimalField(
|
|
_('velocity (fps)'),
|
|
max_digits=6, decimal_places=1,
|
|
)
|
|
notes = models.CharField(_('notes'), max_length=255, blank=True)
|
|
|
|
class Meta:
|
|
verbose_name = _('shot')
|
|
verbose_name_plural = _('shots')
|
|
ordering = ['shot_number']
|
|
constraints = [
|
|
models.UniqueConstraint(
|
|
fields=['group', 'shot_number'],
|
|
name='unique_shot_number_per_group',
|
|
)
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"Shot #{self.shot_number} — {self.velocity_fps} fps"
|
|
|
|
def clean(self):
|
|
if self.velocity_fps is not None and self.velocity_fps <= 0:
|
|
raise ValidationError(
|
|
{'velocity_fps': _('Velocity must be a positive value.')}
|
|
)
|
|
|
|
def save(self, *args, **kwargs):
|
|
if not self.pk:
|
|
last = (
|
|
Shot.objects
|
|
.filter(group=self.group)
|
|
.order_by('-shot_number')
|
|
.values_list('shot_number', flat=True)
|
|
.first()
|
|
)
|
|
self.shot_number = (last or 0) + 1
|
|
super().save(*args, **kwargs)
|