158 lines
6.9 KiB
Python
158 lines
6.9 KiB
Python
"""
|
|
Tests for the QI Projects app.
|
|
|
|
Covers the multi-department + derived-team behaviour:
|
|
- Team membership is derived from each department's champion + manager (+ lead).
|
|
- Sync fires on M2M changes to `departments`.
|
|
- ACL: only admins / project lead / team members can view & manage.
|
|
- Task `assigned_to` is limited to derived team members.
|
|
"""
|
|
|
|
from django.contrib.auth import get_user_model
|
|
from django.test import TestCase
|
|
from django.urls import reverse
|
|
|
|
from apps.organizations.models import Department, Hospital, Staff
|
|
from apps.projects.models import QIProject, QIProjectTask
|
|
|
|
User = get_user_model()
|
|
|
|
|
|
def _make_staff(hospital, department=None, first="Staff", email=None):
|
|
return Staff.objects.create(
|
|
hospital=hospital,
|
|
department=department,
|
|
first_name=first,
|
|
last_name="Test",
|
|
status="active",
|
|
staff_type="other",
|
|
job_title="T",
|
|
employee_id=f"EMP-{first}-{Hospital.objects.count()}",
|
|
user=User.objects.create_user(
|
|
username=email or f"{first.lower()}@test.local",
|
|
email=email or f"{first.lower()}@test.local",
|
|
password="pw",
|
|
),
|
|
)
|
|
|
|
|
|
class DerivedTeamTests(TestCase):
|
|
"""sync_team_members_from_departments + m2m_changed signal."""
|
|
|
|
def setUp(self):
|
|
self.hospital = Hospital.objects.create(name="H1", code="H1", status="active")
|
|
self.dept_a = Department.objects.create(hospital=self.hospital, name="Dept A", code="DA", status="active")
|
|
self.dept_b = Department.objects.create(hospital=self.hospital, name="Dept B", code="DB", status="active")
|
|
self.champ_a = _make_staff(self.hospital, self.dept_a, "ChampA")
|
|
self.champ_b = _make_staff(self.hospital, self.dept_b, "ChampB")
|
|
self.dept_a.champion = self.champ_a
|
|
self.dept_a.save()
|
|
self.dept_b.champion = self.champ_b
|
|
self.dept_b.save()
|
|
self.lead = _make_staff(self.hospital, self.dept_a, "Lead")
|
|
|
|
def _new_project(self, **kw):
|
|
defaults = dict(
|
|
name="P", description="d", hospital=self.hospital, status="pending", project_lead=self.lead
|
|
)
|
|
defaults.update(kw)
|
|
return QIProject.objects.create(**defaults)
|
|
|
|
def test_team_derived_from_department_champions(self):
|
|
p = self._new_project()
|
|
p.departments.set([self.dept_a, self.dept_b])
|
|
# champ_a, champ_b (champions) + lead (project_lead) → 3 members
|
|
self.assertEqual(set(p.team_members.values_list("pk", flat=True)),
|
|
{self.champ_a.pk, self.champ_b.pk, self.lead.pk})
|
|
|
|
def test_manager_without_staff_profile_is_skipped(self):
|
|
# Manager is a User with no staff_profile → excluded
|
|
mgr_user = User.objects.create_user(username="mgr", email="mgr@x", password="p")
|
|
self.dept_a.manager = mgr_user
|
|
self.dept_a.save()
|
|
p = self._new_project()
|
|
p.departments.set([self.dept_a])
|
|
# Only champ_a + lead; mgr_user has no staff_profile so not added
|
|
self.assertEqual(set(p.team_members.values_list("pk", flat=True)),
|
|
{self.champ_a.pk, self.lead.pk})
|
|
|
|
def test_manager_with_staff_profile_is_included(self):
|
|
mgr_staff = _make_staff(self.hospital, self.dept_a, "Mgr")
|
|
mgr_user = mgr_staff.user
|
|
self.dept_a.manager = mgr_user
|
|
self.dept_a.save()
|
|
p = self._new_project()
|
|
p.departments.set([self.dept_a])
|
|
self.assertIn(mgr_staff.pk, set(p.team_members.values_list("pk", flat=True)))
|
|
|
|
def test_removing_department_rebuilds_team(self):
|
|
p = self._new_project()
|
|
p.departments.set([self.dept_a, self.dept_b])
|
|
self.assertIn(self.champ_b.pk, set(p.team_members.values_list("pk", flat=True)))
|
|
p.departments.remove(self.dept_b)
|
|
# champ_b no longer in team; champ_a + lead remain
|
|
self.assertEqual(set(p.team_members.values_list("pk", flat=True)),
|
|
{self.champ_a.pk, self.lead.pk})
|
|
|
|
def test_is_team_member_helper(self):
|
|
p = self._new_project()
|
|
p.departments.set([self.dept_a])
|
|
self.assertTrue(p.is_team_member(self.champ_a.user))
|
|
self.assertTrue(p.is_team_member(self.lead.user))
|
|
self.assertFalse(p.is_team_member(self.champ_b.user))
|
|
|
|
|
|
class ACLTests(TestCase):
|
|
"""_check_project_permission + _get_can_edit via the views."""
|
|
|
|
def setUp(self):
|
|
self.hospital = Hospital.objects.create(name="H1", code="H1", status="active")
|
|
self.hospital2 = Hospital.objects.create(name="H2", code="H2", status="active")
|
|
self.dept = Department.objects.create(hospital=self.hospital, name="D", code="D", status="active")
|
|
self.champ = _make_staff(self.hospital, self.dept, "Champ")
|
|
self.outsider = _make_staff(self.hospital, self.dept, "Outsider")
|
|
self.dept.champion = self.champ
|
|
self.dept.save()
|
|
self.lead = _make_staff(self.hospital, self.dept, "Lead")
|
|
self.project = QIProject.objects.create(
|
|
name="P", description="d", hospital=self.hospital, status="pending", project_lead=self.lead
|
|
)
|
|
self.project.departments.set([self.dept]) # derives team = {champ, lead}
|
|
|
|
def test_team_member_can_view_detail(self):
|
|
# ACL helper level (view-level rendering is blocked by a pre-existing
|
|
# staticfiles-manifest issue in the test env, unrelated to this feature).
|
|
from apps.projects.ui_views import _check_project_permission, _get_can_edit
|
|
|
|
self.assertTrue(_check_project_permission(self.project, self.champ.user))
|
|
self.assertFalse(_get_can_edit(self.champ.user, self.project)) # not lead/admin
|
|
|
|
def test_non_team_same_hospital_user_is_denied(self):
|
|
from apps.projects.ui_views import _check_project_permission
|
|
|
|
# Outsider is same-hospital but NOT a team member → denied
|
|
self.assertFalse(_check_project_permission(self.project, self.outsider.user))
|
|
|
|
def test_project_lead_can_edit(self):
|
|
from apps.projects.ui_views import _get_can_edit
|
|
|
|
# Lead is not an admin but is the project lead → can edit
|
|
self.assertTrue(_get_can_edit(self.lead.user, self.project))
|
|
|
|
def test_task_assigned_to_is_limited_to_team_members(self):
|
|
from apps.projects.forms import QIProjectTaskForm
|
|
|
|
form = QIProjectTaskForm(project=self.project)
|
|
qs_ids = set(form.fields["assigned_to"].queryset.values_list("pk", flat=True))
|
|
self.assertEqual(qs_ids, {self.champ.pk, self.lead.pk})
|
|
self.assertNotIn(self.outsider.pk, qs_ids)
|
|
|
|
def test_task_toggle_requires_post(self):
|
|
task = QIProjectTask.objects.create(project=self.project, title="t", assigned_to=self.champ, status="pending")
|
|
self.client.force_login(self.champ.user)
|
|
url = reverse("projects:task_toggle_status",
|
|
kwargs={"project_pk": self.project.pk, "task_pk": task.pk})
|
|
# GET must be rejected (405) — CSRF-safe toggle
|
|
resp = self.client.get(url)
|
|
self.assertEqual(resp.status_code, 405)
|