from django.contrib.auth.models import User from django.core import mail from django.test import TestCase, override_settings from django.urls import reverse from apps.accounts.models import ClientProfile from apps.projects.models import ProjectBrief, ProjectInternalNote class ProjectPortalTest(TestCase): def setUp(self): self.user = User.objects.create_user( username="client", email="client@example.com", password="Str0ngPass!word", first_name="Client", last_name="User", ) ClientProfile.objects.create(user=self.user, timezone="UTC") self.other = User.objects.create_user( username="other", email="other@example.com", password="Str0ngPass!word", ) ClientProfile.objects.create(user=self.other, timezone="UTC") def _brief_payload(self): return { "title": "Radiomics pipeline", "category": ProjectBrief.CATEGORY_MEDICAL_IMAGING, "description": "Need a standardized radiomics workflow.", "budget_range": ProjectBrief.BUDGET_5K_15K, "desired_deadline": "2026-12-31", "reference_links": "https://example.com/spec", } def _create_brief(self, **overrides): defaults = dict( client=self.user, title="Brief", category=ProjectBrief.CATEGORY_WEB, description="d", budget_range=ProjectBrief.BUDGET_UNDER_5K, ) defaults.update(overrides) return ProjectBrief.objects.create(**defaults) def test_dashboard_requires_login(self): response = self.client.get(reverse("projects:dashboard")) self.assertEqual(response.status_code, 302) def test_create_brief_sends_admin_email(self): self.client.login(username="client", password="Str0ngPass!word") with override_settings(MANAGED_PROJECTS_NOTIFY_EMAILS="ops@tecvico.com"): response = self.client.post(reverse("projects:create"), data=self._brief_payload()) self.assertEqual(response.status_code, 302) brief = ProjectBrief.objects.get(title="Radiomics pipeline") self.assertEqual(brief.client, self.user) self.assertEqual(brief.status, ProjectBrief.STATUS_SUBMITTED) self.assertEqual(len(mail.outbox), 1) self.assertIn("Radiomics pipeline", mail.outbox[0].subject) self.assertIn("ops@tecvico.com", mail.outbox[0].to) def test_create_brief_with_title_only(self): self.client.login(username="client", password="Str0ngPass!word") response = self.client.post( reverse("projects:create"), data={"title": "Minimal brief"}, ) self.assertEqual(response.status_code, 302) brief = ProjectBrief.objects.get(title="Minimal brief") self.assertEqual(brief.description, "") self.assertEqual(brief.category, "") self.assertEqual(brief.budget_range, "") def test_create_brief_requires_title(self): self.client.login(username="client", password="Str0ngPass!word") response = self.client.post(reverse("projects:create"), data={"title": ""}) self.assertEqual(response.status_code, 200) self.assertContains(response, "This field is required") self.assertEqual(ProjectBrief.objects.count(), 0) def test_create_renders_submit_only_on_last_step(self): self.client.login(username="client", password="Str0ngPass!word") response = self.client.get(reverse("projects:create")) self.assertEqual(response.status_code, 200) self.assertContains(response, 'data-wizard-submit hidden') self.assertContains(response, 'data-wizard-next') self.assertNotContains(response, 'data-wizard-submit">{{ submit_label }}') def test_dashboard_lists_only_own_briefs(self): self._create_brief(title="Mine") self._create_brief(client=self.other, title="Not mine") self.client.login(username="client", password="Str0ngPass!word") response = self.client.get(reverse("projects:dashboard")) self.assertEqual(response.status_code, 200) titles = [brief.title for brief in response.context["briefs"]] self.assertEqual(titles, ["Mine"]) def test_edit_allowed_only_when_submitted(self): brief = self._create_brief( title="Editable", category=ProjectBrief.CATEGORY_DATA, budget_range=ProjectBrief.BUDGET_NOT_SURE, status=ProjectBrief.STATUS_SUBMITTED, ) self.client.login(username="client", password="Str0ngPass!word") response = self.client.get(reverse("projects:edit", kwargs={"pk": brief.pk})) self.assertEqual(response.status_code, 200) brief.status = ProjectBrief.STATUS_UNDER_REVIEW brief.save() response = self.client.get(reverse("projects:edit", kwargs={"pk": brief.pk})) self.assertEqual(response.status_code, 302) def test_locked_project_blocks_edit(self): brief = self._create_brief( title="Locked", status=ProjectBrief.STATUS_SUBMITTED, locked=True, ) self.client.login(username="client", password="Str0ngPass!word") response = self.client.get(reverse("projects:edit", kwargs={"pk": brief.pk})) self.assertEqual(response.status_code, 302) def test_locked_field_is_disabled_on_edit_form(self): brief = self._create_brief( title="Lock field", status=ProjectBrief.STATUS_SUBMITTED, lock_title=True, ) self.client.login(username="client", password="Str0ngPass!word") response = self.client.get(reverse("projects:edit", kwargs={"pk": brief.pk})) self.assertEqual(response.status_code, 200) self.assertContains(response, 'name="title"') self.assertContains(response, "disabled") def test_locked_field_value_is_preserved_on_submit(self): brief = self._create_brief( title="Original title", status=ProjectBrief.STATUS_SUBMITTED, lock_title=True, ) self.client.login(username="client", password="Str0ngPass!word") self.client.post( reverse("projects:edit", kwargs={"pk": brief.pk}), data={ "title": "Hacked title", "category": brief.category, "description": brief.description, "budget_range": brief.budget_range, "desired_deadline": "", "reference_links": "", }, ) brief.refresh_from_db() self.assertEqual(brief.title, "Original title") def test_status_change_emails_client(self): brief = self._create_brief( title="Notify me", category=ProjectBrief.CATEGORY_RESEARCH, budget_range=ProjectBrief.BUDGET_15K_50K, status=ProjectBrief.STATUS_SUBMITTED, ) mail.outbox.clear() brief.status = ProjectBrief.STATUS_UNDER_REVIEW brief.save() self.assertEqual(len(mail.outbox), 1) self.assertIn("client@example.com", mail.outbox[0].to) def test_internal_notes_not_on_detail_page(self): brief = self._create_brief( title="Secret notes", category=ProjectBrief.CATEGORY_OTHER, ) ProjectInternalNote.objects.create(brief=brief, note="Internal only", author=self.other) self.client.login(username="client", password="Str0ngPass!word") response = self.client.get(reverse("projects:detail", kwargs={"pk": brief.pk})) self.assertEqual(response.status_code, 200) self.assertNotIn(b"Internal only", response.content) def test_website_notes_visible_on_detail_page(self): brief = self._create_brief( title="Public notes", category=ProjectBrief.CATEGORY_WEB, website_notes="Your kickoff call is scheduled for next week.", ) self.client.login(username="client", password="Str0ngPass!word") response = self.client.get(reverse("projects:detail", kwargs={"pk": brief.pk})) self.assertContains(response, "Notes from Tecvico") self.assertContains(response, "kickoff call is scheduled") def test_create_renders_wizard_steps(self): self.client.login(username="client", password="Str0ngPass!word") response = self.client.get(reverse("projects:create")) self.assertEqual(response.status_code, 200) self.assertContains(response, "data-brief-wizard") self.assertContains(response, "Project basics") self.assertContains(response, "References & files") def test_quote_visible_when_quoted(self): brief = self._create_brief( title="Quoted project", category=ProjectBrief.CATEGORY_WORKFLOW, budget_range=ProjectBrief.BUDGET_50K_PLUS, status=ProjectBrief.STATUS_QUOTED, quote_text="We can deliver in 8 weeks for $45,000.", ) self.client.login(username="client", password="Str0ngPass!word") response = self.client.get(reverse("projects:detail", kwargs={"pk": brief.pk})) self.assertContains(response, "We can deliver in 8 weeks")