from django.shortcuts import get_object_or_404 from rest_framework import status from rest_framework.viewsets import ModelViewSet from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated from rest_framework.decorators import action from rest_framework.filters import SearchFilter, OrderingFilter from django_filters.rest_framework import DjangoFilterBackend from core.paginations.limit_offset import CustomLimitOffsetPagination from apps.workspaces.models import Workspace from apps.clients.models import Client from apps.projects.models import Project from apps.projects.api.serializers import ( ProjectSerializer, ProjectCreateSerializer, ProjectUpdateSerializer, ) from apps.projects.api.permissions import IsProjectMember, IsProjectManager from apps.projects.services.projects import ( create_project, update_project, toggle_project_archive ) from apps.workspaces.services import ( PROJECTS_ARCHIVE, PROJECTS_CREATE, PROJECTS_DELETE, PROJECTS_EDIT, can_delete_workspace_object, has_workspace_capability, ) class ProjectViewSet(ModelViewSet): """ API endpoints for managing projects. """ pagination_class = CustomLimitOffsetPagination filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter] filterset_fields = ["workspace", "client", "is_archived"] search_fields = ["name", "description"] ordering_fields = ["name", "created_at", "updated_at"] ordering = ["-updated_at", "-created_at"] def get_permissions(self): """ Instantiates and returns the list of permissions that this view requires. - Workspace-authorized users can update, delete, or archive. - Workspace members can retrieve/view. - Any authenticated user can list their workspace projects or attempt to create. """ if self.action in ["update", "partial_update", "destroy", "archive"]: permission_classes = [IsAuthenticated, IsProjectManager] elif self.action in ["retrieve"]: permission_classes = [IsAuthenticated, IsProjectMember] else: permission_classes = [IsAuthenticated] return [permission() for permission in permission_classes] def get_queryset(self): """ Returns active projects in workspaces where the current user is an active member. """ if getattr(self, "swagger_fake_view", False) or not self.request.user.is_authenticated: return Project.objects.none() queryset = Project.objects.filter( workspace__memberships__user=self.request.user, workspace__memberships__is_active=True, is_deleted=False ).distinct() client_ids = [client_id for client_id in self.request.query_params.getlist("clients") if client_id] if client_ids: queryset = queryset.filter(client_id__in=client_ids) return queryset def get_serializer_class(self): """ Selects the appropriate serializer based on the request action. """ if self.action == "create": return ProjectCreateSerializer elif self.action in ["update", "partial_update"]: return ProjectUpdateSerializer return ProjectSerializer def create(self, request, *args, **kwargs): """ Creates a new project using the project service layer. """ serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) workspace = get_object_or_404(Workspace, id=serializer.validated_data["workspace"], is_deleted=False) if not has_workspace_capability(request.user, workspace, PROJECTS_CREATE): return Response( {"detail": "You do not have permission to create projects in this workspace."}, status=status.HTTP_403_FORBIDDEN, ) client_id = serializer.validated_data.get("client") client = get_object_or_404(Client, id=client_id, is_deleted=False) if client_id else None project = create_project( user=request.user, workspace=workspace, name=serializer.validated_data["name"], client=client, description=serializer.validated_data.get("description", ""), color=serializer.validated_data.get("color", "") ) output_serializer = ProjectSerializer(project) return Response(output_serializer.data, status=status.HTTP_201_CREATED) def update(self, request, *args, **kwargs): """ Updates an existing project using the project service layer. """ partial = kwargs.pop("partial", False) project = self.get_object() serializer = self.get_serializer(data=request.data, partial=partial) serializer.is_valid(raise_exception=True) updated_project = update_project( project=project, **serializer.validated_data ) output_serializer = ProjectSerializer(updated_project) return Response(output_serializer.data, status=status.HTTP_200_OK) def destroy(self, request, *args, **kwargs): """ Soft deletes a project. """ project = self.get_object() if not can_delete_workspace_object(request.user, project, PROJECTS_DELETE): return Response( {"detail": "You do not have permission to delete this project."}, status=status.HTTP_403_FORBIDDEN, ) project.is_deleted = True project.save(update_fields=["is_deleted", "updated_at"]) return Response(status=status.HTTP_204_NO_CONTENT) @action(detail=True, methods=["post"]) def archive(self, request, pk=None): """ Custom endpoint to toggle the archive status of a project. """ project = self.get_object() updated_project = toggle_project_archive(project) output_serializer = ProjectSerializer(updated_project) return Response(output_serializer.data, status=status.HTTP_200_OK)