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
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)
|