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:
parent
81753695a1
commit
4d029dd6e5
@ -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
|
||||
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user