diff --git a/core/tests.py b/core/tests.py index 7332379..f954857 100644 --- a/core/tests.py +++ b/core/tests.py @@ -749,3 +749,68 @@ class ReportMultiFilterTests(TestCase): self.assertEqual(len(ctx['worker_breakdown']), 1) # All three records are for the same worker, R 100 each = R 300 self.assertEqual(ctx['worker_breakdown'][0]['total_paid'], Decimal('300.00')) + + +# ============================================================================= +# === TESTS FOR INLINE FILTERS (Report Page) === +# Pill-as-dropdown + cross-filter feature. Most behaviour is template/JS; +# the only backend surface is the project_team_pairs_json context key that +# powers the client-side Team<->Project cross-filter. +# ============================================================================= + + +class InlineFiltersPairsContextTests(TestCase): + """Report view must serialise distinct (project_id, team_id) pairs for + the pill-popover cross-filter JS.""" + + def setUp(self): + self.admin = User.objects.create_user( + username='admin-if', password='pass', is_staff=True + ) + self.p1 = Project.objects.create(name='P1') + self.p2 = Project.objects.create(name='P2') + self.t1 = Team.objects.create(name='T1', supervisor=self.admin) + self.t2 = Team.objects.create(name='T2', supervisor=self.admin) + self.w = Worker.objects.create( + name='W', id_number='W1', monthly_salary=Decimal('4000') + ) + # Log t1 on p1, t2 on p2 — so pairs should be [(p1,t1), (p2,t2)] + for proj, team in [(self.p1, self.t1), (self.p2, self.t2)]: + log = WorkLog.objects.create( + date=datetime.date(2026, 3, 1), + project=proj, team=team, supervisor=self.admin, + ) + log.workers.add(self.w) + + def test_pairs_context_key_populated(self): + import json as _json + self.client.login(username='admin-if', password='pass') + url = reverse('generate_report') + resp = self.client.get(url + '?from_month=2026-03&to_month=2026-04') + self.assertEqual(resp.status_code, 200) + pairs_json = resp.context['project_team_pairs_json'] + pairs = _json.loads(pairs_json) + # Each entry has both project_id and team_id + for p in pairs: + self.assertIn('project_id', p) + self.assertIn('team_id', p) + # Expected pairs (as tuples for set comparison) + pair_set = {(p['project_id'], p['team_id']) for p in pairs} + self.assertIn((self.p1.id, self.t1.id), pair_set) + self.assertIn((self.p2.id, self.t2.id), pair_set) + + def test_pairs_excludes_null_project_or_team(self): + """Logs with null project or null team should not appear in pairs.""" + import json as _json + # Add a log with team=None + log = WorkLog.objects.create( + date=datetime.date(2026, 3, 2), + project=self.p1, team=None, supervisor=self.admin, + ) + log.workers.add(self.w) + + self.client.login(username='admin-if', password='pass') + resp = self.client.get(reverse('generate_report') + '?from_month=2026-03&to_month=2026-04') + pairs = _json.loads(resp.context['project_team_pairs_json']) + # No pair should have team_id=None + self.assertTrue(all(p['team_id'] is not None for p in pairs)) diff --git a/core/views.py b/core/views.py index 8f1cdfe..ce6691d 100644 --- a/core/views.py +++ b/core/views.py @@ -2393,6 +2393,18 @@ def generate_report(request): return qd.urlencode() context['query_string_without_project'] = _qs_without('project') context['query_string_without_team'] = _qs_without('team') + # === Cross-filter source: serialised (project_id, team_id) pairs === + # The pill-popover JS on the report page uses this to hide teams that + # haven't worked on a selected project, and vice versa. Scope = entire + # history (not this report's date range) — cross-filter is about data + # possibility, not data in this period. + pairs = list( + WorkLog.objects + .filter(project__isnull=False, team__isnull=False) + .values('project_id', 'team_id') + .distinct() + ) + context['project_team_pairs_json'] = json.dumps(pairs) # Pass projects and teams so the "New Report" modal's dropdowns can # populate (same lists the Dashboard modal uses) context['projects'] = Project.objects.all().order_by('name')