213 lines
9.1 KiB
Python
213 lines
9.1 KiB
Python
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 }}</button>')
|
|
|
|
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")
|