First commit of claude's rework in django + vanillajs fronted
This commit is contained in:
157
apps/tools/models.py
Normal file
157
apps/tools/models.py
Normal file
@@ -0,0 +1,157 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user