fix: cap attendance date range at 31 days (year-typo flood guard)

AttendanceLogForm.clean only checked end >= start — a typo'd year in
the end-date field created a WorkLog per worker per day for the whole
span (365+ days) in one submit, with no way back but manual deletion.
Ranges longer than 31 days (a full calendar month) are now rejected
with a message pointing at the year fields.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Konrad du Plessis 2026-06-12 17:47:27 +02:00
parent 81753695a1
commit 4d029dd6e5
2 changed files with 47 additions and 0 deletions

View File

@ -132,6 +132,16 @@ class AttendanceLogForm(forms.ModelForm):
if start_date and end_date and end_date < start_date:
raise forms.ValidationError('End date cannot be before start date.')
# === GUARD: cap the range length (audit fix, Jun 2026) ===
# A typo'd year in the end date (2027 instead of 2026) would
# otherwise create hundreds of WorkLogs in one submit. 31 days
# covers a full calendar month — the longest real logging period.
if start_date and end_date and (end_date - start_date).days > 31:
raise forms.ValidationError(
'Date range cannot exceed 31 days — check the year on both '
'dates. Log longer periods in separate submissions.'
)
return cleaned_data

View File

@ -3825,3 +3825,40 @@ class NegativePaymentGuardTests(TestCase):
self.assertEqual(record.amount_paid, Decimal('0.00'))
self.loan.refresh_from_db()
self.assertEqual(self.loan.remaining_balance, Decimal('1100.00'))
# =============================================================================
# === AUDIT FIX #5 — ATTENDANCE DATE RANGE CAP ===
# A typo'd year in the end-date field (2027 instead of 2026) would create
# hundreds of WorkLogs in a single submit. The form now rejects ranges
# longer than 31 days; one calendar month is the longest real logging
# period, anything longer is almost certainly a typo.
# =============================================================================
class AttendanceDateRangeCapTests(TestCase):
"""The attendance form refuses date ranges longer than 31 days."""
def setUp(self):
from core.forms import AttendanceLogForm
self.form_cls = AttendanceLogForm
self.admin = User.objects.create_user(
username='rangecap_admin', password='x', is_staff=True)
def _range_errors(self, start, end):
# Other required fields (project, workers) are deliberately left
# blank — we only care about the non-field errors clean() raises.
form = self.form_cls({'date': start, 'end_date': end}, user=self.admin)
form.is_valid()
return [str(e) for e in form.non_field_errors()]
def test_range_over_31_days_is_rejected(self):
errs = self._range_errors('2026-01-01', '2026-02-02') # 32-day span
self.assertTrue(any('31 days' in e for e in errs), errs)
def test_year_typo_is_rejected(self):
errs = self._range_errors('2026-01-05', '2027-01-05') # the real footgun
self.assertTrue(any('31 days' in e for e in errs), errs)
def test_range_of_exactly_31_days_is_allowed(self):
errs = self._range_errors('2026-01-01', '2026-02-01') # 31-day diff
self.assertFalse(any('31 days' in e for e in errs), errs)