You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

362 lines
15 KiB

import logging
from django.db.models import Q
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_lazy as _
from rest_framework import status
from rest_framework.generics import GenericAPIView
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiResponse
from apps.projects.models.project import (
Project,
KanbanColumn,
KanbanTask,
ProjectDocument,
)
from apps.projects.permissions import IsProjectParticipantOrReadOnly, CanCreateProject
from apps.projects.serializers import (
ProjectListSerializer,
ProjectDetailSerializer,
ProjectCreateUpdateSerializer,
KanbanColumnSerializer,
KanbanTaskSerializer,
KanbanTaskCreateUpdateSerializer,
ProjectDocumentSerializer,
)
from utils.pagination import StandardResultsSetPagination
logger = logging.getLogger(__name__)
def get_project_by_id_or_slug(lookup_val):
if str(lookup_val).isdigit():
return get_object_or_404(Project, id=int(lookup_val))
return get_object_or_404(Project, slug=lookup_val)
class ProjectListCreateView(GenericAPIView):
serializer_class = ProjectListSerializer
pagination_class = StandardResultsSetPagination
queryset = Project.objects.all().select_related('owner_institution').prefetch_related('collaborating_institutions', 'tasks')
def get_permissions(self):
if self.request.method == 'POST':
return [IsAuthenticated(), CanCreateProject()]
return [AllowAny()]
def get_queryset(self):
if getattr(self, 'swagger_fake_view', False):
return Project.objects.none()
qs = Project.objects.all().select_related('owner_institution').prefetch_related('collaborating_institutions', 'tasks')
# Filters
status_param = self.request.query_params.get('status')
if status_param:
qs = qs.filter(status=status_param)
category = self.request.query_params.get('category')
if category:
qs = qs.filter(category__iexact=category)
institution_id = self.request.query_params.get('institution_id')
if institution_id:
qs = qs.filter(
Q(owner_institution_id=institution_id) |
Q(collaborating_institutions__id=institution_id)
).distinct()
owner_id = self.request.query_params.get('owner_institution_id')
if owner_id:
qs = qs.filter(owner_institution_id=owner_id)
search = self.request.query_params.get('search')
if search:
qs = qs.filter(
Q(title__icontains=search) |
Q(description__icontains=search) |
Q(category__icontains=search)
)
return qs.order_by('-created_at')
@extend_schema(
summary="List collaborative projects",
description="Returns a paginated list of collaborative projects with filtering by institution, category, and status.",
parameters=[
OpenApiParameter('status', str, description='Filter by project status (active, completed, on_hold, planning)'),
OpenApiParameter('category', str, description='Filter by category name'),
OpenApiParameter('institution_id', int, description='Filter by owner or collaborating institution ID'),
OpenApiParameter('owner_institution_id', int, description='Filter specifically by owner institution ID'),
OpenApiParameter('search', str, description='Search query across title and description'),
],
responses={200: ProjectListSerializer(many=True)},
tags=["Collaborative Projects & Kanban"],
)
def get(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True, context={'request': request})
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True, context={'request': request})
return Response(serializer.data, status=status.HTTP_200_OK)
@extend_schema(
summary="Create collaborative project",
description="Creates a new multi-institution collaborative project with default Kanban columns.",
request=ProjectCreateUpdateSerializer,
responses={
201: ProjectDetailSerializer,
400: OpenApiResponse(description="Validation error"),
401: OpenApiResponse(description="Authentication required"),
},
tags=["Collaborative Projects & Kanban"],
)
def post(self, request, *args, **kwargs):
serializer = ProjectCreateUpdateSerializer(data=request.data, context={'request': request})
serializer.is_valid(raise_exception=True)
project = serializer.save()
response_serializer = ProjectDetailSerializer(project, context={'request': request})
return Response(response_serializer.data, status=status.HTTP_201_CREATED)
class ProjectDetailView(GenericAPIView):
serializer_class = ProjectDetailSerializer
queryset = Project.objects.all()
def get_permissions(self):
if self.request.method in ['PATCH', 'PUT', 'DELETE']:
return [IsAuthenticated(), IsProjectParticipantOrReadOnly()]
return [AllowAny()]
def get_object(self):
lookup = self.kwargs.get('pk_or_slug')
project = get_project_by_id_or_slug(lookup)
self.check_object_permissions(self.request, project)
return project
@extend_schema(
summary="Get project details",
description="Retrieves full project details, collaborating institutions, and Kanban boards.",
responses={
200: ProjectDetailSerializer,
404: OpenApiResponse(description="Project not found"),
},
tags=["Collaborative Projects & Kanban"],
)
def get(self, request, pk_or_slug, *args, **kwargs):
project = self.get_object()
serializer = ProjectDetailSerializer(project, context={'request': request})
return Response(serializer.data, status=status.HTTP_200_OK)
@extend_schema(
summary="Update project",
description="Updates project status, progress percentage, or details. Permitted for owner and collaborating editors.",
request=ProjectCreateUpdateSerializer,
responses={
200: ProjectDetailSerializer,
403: OpenApiResponse(description="Permission denied"),
},
tags=["Collaborative Projects & Kanban"],
)
def patch(self, request, pk_or_slug, *args, **kwargs):
project = self.get_object()
serializer = ProjectCreateUpdateSerializer(
project,
data=request.data,
partial=True,
context={'request': request}
)
serializer.is_valid(raise_exception=True)
serializer.save()
response_serializer = ProjectDetailSerializer(project, context={'request': request})
return Response(response_serializer.data, status=status.HTTP_200_OK)
@extend_schema(
summary="Delete project",
description="Removes a collaborative project and associated Kanban boards.",
responses={
204: OpenApiResponse(description="Project deleted successfully"),
403: OpenApiResponse(description="Permission denied"),
},
tags=["Collaborative Projects & Kanban"],
)
def delete(self, request, pk_or_slug, *args, **kwargs):
project = self.get_object()
project.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class ProjectKanbanBoardView(GenericAPIView):
serializer_class = KanbanColumnSerializer
queryset = KanbanColumn.objects.all()
def get_permissions(self):
return [AllowAny()]
@extend_schema(
summary="Get project Kanban board hierarchy",
description="Returns hierarchical Kanban columns with nested task lists for drag-and-drop board interfaces.",
responses={200: KanbanColumnSerializer(many=True)},
tags=["Collaborative Projects & Kanban"],
)
def get(self, request, pk_or_slug, *args, **kwargs):
project = get_project_by_id_or_slug(pk_or_slug)
columns = project.columns.all().order_by('order', 'id')
serializer = KanbanColumnSerializer(columns, many=True, context={'request': request})
return Response(serializer.data, status=status.HTTP_200_OK)
class KanbanTaskCreateView(GenericAPIView):
serializer_class = KanbanTaskCreateUpdateSerializer
permission_classes = [IsAuthenticated, IsProjectParticipantOrReadOnly]
queryset = KanbanTask.objects.all()
@extend_schema(
summary="Create Kanban task",
description="Adds a new task under a specific column of the project.",
request=KanbanTaskCreateUpdateSerializer,
responses={
201: KanbanTaskSerializer,
400: OpenApiResponse(description="Validation error"),
403: OpenApiResponse(description="Permission denied"),
},
tags=["Collaborative Projects & Kanban"],
)
def post(self, request, pk_or_slug, *args, **kwargs):
project = get_project_by_id_or_slug(pk_or_slug)
self.check_object_permissions(request, project)
serializer = KanbanTaskCreateUpdateSerializer(
data=request.data,
context={'project': project, 'request': request}
)
serializer.is_valid(raise_exception=True)
task = serializer.save(project=project)
response_serializer = KanbanTaskSerializer(task, context={'request': request})
return Response(response_serializer.data, status=status.HTTP_201_CREATED)
class KanbanTaskDetailView(GenericAPIView):
serializer_class = KanbanTaskCreateUpdateSerializer
permission_classes = [IsAuthenticated, IsProjectParticipantOrReadOnly]
queryset = KanbanTask.objects.all()
@extend_schema(
summary="Update or move Kanban task",
description="Updates task attributes, priority, assignee, or moves task to another column / reorders within column.",
request=KanbanTaskCreateUpdateSerializer,
responses={
200: KanbanTaskSerializer,
400: OpenApiResponse(description="Validation error"),
403: OpenApiResponse(description="Permission denied"),
},
tags=["Collaborative Projects & Kanban"],
)
def patch(self, request, pk_or_slug, task_id, *args, **kwargs):
project = get_project_by_id_or_slug(pk_or_slug)
self.check_object_permissions(request, project)
task = get_object_or_404(KanbanTask, id=task_id, project=project)
serializer = KanbanTaskCreateUpdateSerializer(
task,
data=request.data,
partial=True,
context={'project': project, 'request': request}
)
serializer.is_valid(raise_exception=True)
serializer.save()
response_serializer = KanbanTaskSerializer(task, context={'request': request})
return Response(response_serializer.data, status=status.HTTP_200_OK)
@extend_schema(
summary="Delete Kanban task",
description="Removes a task from the project Kanban board.",
responses={
204: OpenApiResponse(description="Task deleted successfully"),
403: OpenApiResponse(description="Permission denied"),
},
tags=["Collaborative Projects & Kanban"],
)
def delete(self, request, pk_or_slug, task_id, *args, **kwargs):
project = get_project_by_id_or_slug(pk_or_slug)
self.check_object_permissions(request, project)
task = get_object_or_404(KanbanTask, id=task_id, project=project)
task.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class ProjectDocumentListView(GenericAPIView):
serializer_class = ProjectDocumentSerializer
parser_classes = [MultiPartParser, FormParser, JSONParser]
queryset = ProjectDocument.objects.all()
def get_permissions(self):
if self.request.method == 'POST':
return [IsAuthenticated(), IsProjectParticipantOrReadOnly()]
return [AllowAny()]
@extend_schema(
summary="List project documents & MOUs",
description="Returns uploaded agreements, contracts, and progress reports for a project.",
responses={200: ProjectDocumentSerializer(many=True)},
tags=["Collaborative Projects & Kanban"],
)
def get(self, request, pk_or_slug, *args, **kwargs):
project = get_project_by_id_or_slug(pk_or_slug)
docs = project.documents.all().order_by('-uploaded_at')
serializer = ProjectDocumentSerializer(docs, many=True, context={'request': request})
return Response(serializer.data, status=status.HTTP_200_OK)
@extend_schema(
summary="Upload project document",
description="Uploads an agreement, contract, or milestone report.",
request=ProjectDocumentSerializer,
responses={
201: ProjectDocumentSerializer,
400: OpenApiResponse(description="Validation error"),
403: OpenApiResponse(description="Permission denied"),
},
tags=["Collaborative Projects & Kanban"],
)
def post(self, request, pk_or_slug, *args, **kwargs):
project = get_project_by_id_or_slug(pk_or_slug)
self.check_object_permissions(request, project)
serializer = ProjectDocumentSerializer(data=request.data, context={'request': request})
serializer.is_valid(raise_exception=True)
doc = serializer.save(project=project)
return Response(ProjectDocumentSerializer(doc, context={'request': request}).data, status=status.HTTP_201_CREATED)
class ProjectDocumentDetailView(GenericAPIView):
serializer_class = ProjectDocumentSerializer
permission_classes = [IsAuthenticated, IsProjectParticipantOrReadOnly]
queryset = ProjectDocument.objects.all()
@extend_schema(
summary="Delete project document",
description="Removes a document attachment from the project.",
responses={
204: OpenApiResponse(description="Document removed successfully"),
403: OpenApiResponse(description="Permission denied"),
},
tags=["Collaborative Projects & Kanban"],
)
def delete(self, request, pk_or_slug, doc_id, *args, **kwargs):
project = get_project_by_id_or_slug(pk_or_slug)
self.check_object_permissions(request, project)
doc = get_object_or_404(ProjectDocument, id=doc_id, project=project)
doc.delete()
return Response(status=status.HTTP_204_NO_CONTENT)