diff --git a/core/forms.py b/core/forms.py index 3e64c85..cc75298 100644 --- a/core/forms.py +++ b/core/forms.py @@ -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 diff --git a/core/tests.py b/core/tests.py index 50000a7..c46d493 100644 --- a/core/tests.py +++ b/core/tests.py @@ -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)