from django.test import TestCase from django.core.files.uploadedfile import SimpleUploadedFile from django.contrib.auth import get_user_model from rest_framework.test import APIClient from rest_framework import status from apps.projects.models.project import ( Project, KanbanColumn, KanbanTask, ProjectDocument, ) from apps.profiles.models.institution import Institution, InstitutionMember User = get_user_model() class Phase5ProjectsTests(TestCase): """ Automated test suite for Phase 5: - Project Creation & Default Kanban Column Generation - Scoped Project Listing & Filtering (institution, category, status, search) - Project Detail View with Multi-Institution Collaborators - Drag-and-Drop Kanban Board View & Task Hierarchy - Task Creation, Priority, Column Migration & Reordering - Project Updates & Role-Based Permissions - Project Document Uploads & Deletions """ def setUp(self): self.client = APIClient() # Users self.lead_admin = User.objects.create_user( email="lead.director@mashhadcenter.org", password="SecurePassword123!", fullname="Hassan Razavi Director", user_type=User.UserType.INSTITUTION_ADMIN, country="Iran", city="Mashhad" ) self.collab_editor = User.objects.create_user( email="collab.editor@berlincenter.de", password="SecurePassword123!", fullname="Fatima Berlin Coordinator", user_type=User.UserType.EDITOR, country="Germany", city="Berlin" ) self.unrelated_user = User.objects.create_user( email="outsider@community.org", password="SecurePassword123!", fullname="Ali Outsider", user_type=User.UserType.CLIENT, country="France", city="Paris" ) # Institutions self.owner_inst = Institution.objects.create( name="Astan Quds International Relations", type=Institution.InstitutionType.INSTITUTE, country="Iran", city="Mashhad" ) InstitutionMember.objects.create( institution=self.owner_inst, user=self.lead_admin, role=InstitutionMember.MemberRole.ADMIN ) self.collab_inst = Institution.objects.create( name="Imam Ali Islamic Center Berlin", type=Institution.InstitutionType.CULTURAL_CENTER, country="Germany", city="Berlin" ) InstitutionMember.objects.create( institution=self.collab_inst, user=self.collab_editor, role=InstitutionMember.MemberRole.EDITOR ) # Sample Project self.project = Project.objects.create( title="International Encyclopedia of Razavi Manuscript Heritage", description="Joint digital restoration and multilingual publication of historic manuscripts.", owner_institution=self.owner_inst, category="Cultural Diplomacy", status=Project.Status.ACTIVE, progress_percentage=35, budget="€120,000", team_size=8, created_by=self.lead_admin ) self.project.collaborating_institutions.add(self.collab_inst) def test_project_creation_and_auto_columns(self): self.client.force_authenticate(user=self.lead_admin) payload = { "title": "Inter-Center Youth Leadership Summit 2026", "description": "Annual multilateral youth exchange program.", "owner_institution": self.owner_inst.id, "collaborating_institutions": [self.collab_inst.id], "category": "Educational Academy", "status": "active", "progress_percentage": 10, "budget": "€45,000", "team_size": 4 } response = self.client.post("/api/v1/projects/", payload, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.data["title"], payload["title"]) self.assertTrue(response.data["slug"]) self.assertEqual(len(response.data["collaborating_institutions"]), 1) # Verify auto-generated 4 default Kanban columns project_id = response.data["id"] columns_count = KanbanColumn.objects.filter(project_id=project_id).count() self.assertEqual(columns_count, 4) def test_project_listing_and_filtering(self): # 1. Base list response = self.client.get("/api/v1/projects/") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertGreaterEqual(len(response.data["results"]), 1) # 2. Filter by owner institution resp_inst = self.client.get(f"/api/v1/projects/?institution_id={self.owner_inst.id}") self.assertEqual(len(resp_inst.data["results"]), 1) # 3. Filter by collaborating institution resp_collab = self.client.get(f"/api/v1/projects/?institution_id={self.collab_inst.id}") self.assertEqual(len(resp_collab.data["results"]), 1) # 4. Filter by category resp_cat = self.client.get("/api/v1/projects/?category=Cultural Diplomacy") self.assertEqual(len(resp_cat.data["results"]), 1) # 5. Search resp_search = self.client.get("/api/v1/projects/?search=Manuscript") self.assertEqual(len(resp_search.data["results"]), 1) def test_project_detail_and_collaboration_links(self): self.client.force_authenticate(user=self.lead_admin) response = self.client.get(f"/api/v1/projects/{self.project.slug}/") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["title"], self.project.title) self.assertEqual(response.data["owner_institution"]["name"], self.owner_inst.name) self.assertEqual(len(response.data["collaborating_institutions"]), 1) self.assertEqual(response.data["collaborating_institutions"][0]["name"], self.collab_inst.name) self.assertTrue(response.data["can_edit"]) def test_kanban_board_hierarchy(self): response = self.client.get(f"/api/v1/projects/{self.project.id}/kanban/") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data), 4) column_names = [col["name"] for col in response.data] self.assertIn("To Do", column_names) self.assertIn("In Progress", column_names) self.assertIn("Review", column_names) self.assertIn("Done", column_names) def test_kanban_task_creation_and_reordering_across_columns(self): self.client.force_authenticate(user=self.lead_admin) todo_column = self.project.columns.get(code="todo") in_progress_column = self.project.columns.get(code="in_progress") done_column = self.project.columns.get(code="done") # 1. Create task in To Do task_payload = { "column": todo_column.id, "title": "Digitize 15th Century Manuscript Folios", "description": "High-resolution 1200 DPI archival scans.", "priority": "high", "assignee": self.collab_editor.id, "labels": ["Archival", "Digital Preservation"], "order": 1 } create_task_resp = self.client.post(f"/api/v1/projects/{self.project.id}/tasks/", task_payload, format="json") self.assertEqual(create_task_resp.status_code, status.HTTP_201_CREATED) task_id = create_task_resp.data["id"] self.assertEqual(create_task_resp.data["column_code"], "todo") # 2. Move task to In Progress (Drag-and-Drop Simulation) move_resp = self.client.patch( f"/api/v1/projects/{self.project.id}/tasks/{task_id}/", {"column": in_progress_column.id, "order": 2}, format="json" ) self.assertEqual(move_resp.status_code, status.HTTP_200_OK) self.assertEqual(move_resp.data["column_code"], "in_progress") # 3. Complete task -> Move to Done done_resp = self.client.patch( f"/api/v1/projects/{self.project.id}/tasks/{task_id}/", {"column": done_column.id}, format="json" ) self.assertEqual(done_resp.status_code, status.HTTP_200_OK) self.assertEqual(done_resp.data["column_code"], "done") # 4. Delete task del_resp = self.client.delete(f"/api/v1/projects/{self.project.id}/tasks/{task_id}/") self.assertEqual(del_resp.status_code, status.HTTP_204_NO_CONTENT) self.assertEqual(KanbanTask.objects.filter(id=task_id).count(), 0) def test_project_patch_update_and_permissions(self): patch_payload = { "progress_percentage": 50, "status": "active" } # 1. Unauthenticated user gets 401 unauth_resp = self.client.patch(f"/api/v1/projects/{self.project.id}/", patch_payload, format="json") self.assertEqual(unauth_resp.status_code, status.HTTP_401_UNAUTHORIZED) # 2. Unrelated user gets 403 self.client.force_authenticate(user=self.unrelated_user) forbid_resp = self.client.patch(f"/api/v1/projects/{self.project.id}/", patch_payload, format="json") self.assertEqual(forbid_resp.status_code, status.HTTP_403_FORBIDDEN) # 3. Collaborating institution editor can update progress self.client.force_authenticate(user=self.collab_editor) collab_resp = self.client.patch(f"/api/v1/projects/{self.project.id}/", patch_payload, format="json") self.assertEqual(collab_resp.status_code, status.HTTP_200_OK) self.assertEqual(collab_resp.data["progress_percentage"], 50) def test_project_document_upload_and_delete(self): self.client.force_authenticate(user=self.collab_editor) mock_doc = SimpleUploadedFile("bilateral_mou_2026.pdf", b"%PDF-1.4 Mock Bilateral MOU", content_type="application/pdf") upload_resp = self.client.post( f"/api/v1/projects/{self.project.id}/documents/", { "title": "Bilateral Cooperation MOU Astan-Berlin", "doc_type": "mou", "file": mock_doc }, format="multipart" ) self.assertEqual(upload_resp.status_code, status.HTTP_201_CREATED) doc_id = upload_resp.data["id"] # List documents list_resp = self.client.get(f"/api/v1/projects/{self.project.id}/documents/") self.assertEqual(list_resp.status_code, status.HTTP_200_OK) self.assertEqual(len(list_resp.data), 1) # Delete document del_resp = self.client.delete(f"/api/v1/projects/{self.project.id}/documents/{doc_id}/") self.assertEqual(del_resp.status_code, status.HTTP_204_NO_CONTENT) self.assertEqual(ProjectDocument.objects.filter(id=doc_id).count(), 0)