💻 Примеры кода

Ключевые фрагменты из задач 1–7

⚡ Ключевые паттерны

# get_serializer_class
def get_serializer_class(self):
    return AllProjectFilesSerializer if self.request.method == 'GET' \
           else CreateProjectFileSerializer

# 204 при пустом queryset
if not qs.exists():
    return Response(data=[], status=status.HTTP_204_NO_CONTENT)

# FileResponse
response = FileResponse(file_handle, content_type='application/octet-stream')
response['Content-Disposition'] = f'attachment; filename="{name}"'

# set_password
user.set_password(password)
user.save()

Пример 1. ProjectFileListGenericView — динамический сериализатор

Один класс, два сериализатора: для GET — показываем больше полей, для POST — принимаем минимум.

# apps/projects/views/project_file_views.py
from rest_framework.generics import ListCreateAPIView
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework import status

from apps.projects.models import ProjectFile
from apps.projects.serializers.project_file_serializers import (
    AllProjectFilesSerializer,
    CreateProjectFileSerializer,
)


class ProjectFileListGenericView(ListCreateAPIView):
    def get_serializer_class(self):
        if self.request.method == 'GET':
            return AllProjectFilesSerializer
        return CreateProjectFileSerializer

    def get_queryset(self):
        project_name = self.request.query_params.get('project')
        if project_name:
            return ProjectFile.objects.filter(
                project__name=project_name
            )
        return ProjectFile.objects.all()

    def list(self, request: Request, *args, **kwargs) -> Response:
        project_files = self.get_queryset()
        if not project_files.exists():
            return Response(
                data=[],
                status=status.HTTP_204_NO_CONTENT
            )
        serializer = self.get_serializer(project_files, many=True)
        return Response(
            serializer.data,
            status=status.HTTP_200_OK
        )

Пример 2. ProjectFileDetailGenericView — детали + физическое удаление

# apps/projects/views/project_file_views.py
from rest_framework.generics import RetrieveDestroyAPIView, get_object_or_404
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework import status

from apps.projects.models import ProjectFile
from apps.projects.serializers.project_file_serializers import ProjectFileDetailSerializer
from apps.projects.utils.upload_file_helpers import delete_file


class ProjectFileDetailGenericView(RetrieveDestroyAPIView):
    serializer_class = ProjectFileDetailSerializer

    def get_object(self):
        return get_object_or_404(ProjectFile, pk=self.kwargs['pk'])

    def retrieve(self, request: Request, *args, **kwargs) -> Response:
        task = self.get_object()
        serializer = self.get_serializer(task)
        return Response(
            data=serializer.data,
            status=status.HTTP_200_OK
        )

    def destroy(self, request: Request, *args, **kwargs) -> Response:
        task = self.get_object()
        try:
            delete_file(file_path=task.file_path.path)
        except Exception as e:
            return Response(
                data={"message": str(e)},
                status=status.HTTP_400_BAD_REQUEST
            )
        else:
            task.delete()
        return Response(
            data={"message": "File deleted successfully"},
            status=status.HTTP_200_OK
        )

Пример 3. Кастомная модель User

# apps/users/models.py
from django.contrib.auth.base_user import AbstractBaseUser
from django.contrib.auth.models import PermissionsMixin, UserManager
from django.core.validators import MinLengthValidator
from django.db import models
from django.utils.translation import gettext_lazy as _

from apps.projects.models import Project
from apps.users.choices.positions import UserPositions


class User(AbstractBaseUser, PermissionsMixin):
    username = models.CharField(
        _("username"), max_length=50, unique=True,
        error_messages={"unique": _("A user with that username already exists.")}
    )
    first_name = models.CharField(
        _("first name"), max_length=40, validators=[MinLengthValidator(2)]
    )
    last_name = models.CharField(
        _("last name"), max_length=40, validators=[MinLengthValidator(2)]
    )
    email = models.EmailField(_("email address"), max_length=150, unique=True)
    phone = models.CharField(max_length=75, null=True, blank=True)
    is_staff = models.BooleanField(default=False)
    is_active = models.BooleanField(default=True)
    date_joined = models.DateTimeField(name="registered", auto_now_add=True)
    last_login = models.DateTimeField(null=True, blank=True)
    updated_at = models.DateTimeField(auto_now=True)
    deleted_at = models.DateTimeField(null=True, blank=True)
    deleted = models.BooleanField(default=False)
    position = models.CharField(
        max_length=15,
        choices=UserPositions.choices,
        default=UserPositions.PROGRAMMER
    )
    project = models.ForeignKey(
        Project,
        on_delete=models.CASCADE,
        related_name="users",
        null=True,
        blank=True,
    )

    USERNAME_FIELD = "email"
    REQUIRED_FIELDS = ["username", "first_name", "last_name", "position"]
    objects = UserManager()

    def __str__(self):
        return f"{self.last_name} {self.first_name}"

Пример 4. DownloadProjectFileView — FileResponse

# apps/projects/views/project_file_views.py
from django.http import FileResponse
from rest_framework.views import APIView
from rest_framework.request import Request
from rest_framework.generics import get_object_or_404

from apps.projects.models import ProjectFile


class DownloadProjectFileView(APIView):
    def get_object(self):
        return get_object_or_404(ProjectFile, pk=self.kwargs['pk'])

    def get(self, request: Request, *args, **kwargs) -> FileResponse:
        project_file = self.get_object()
        file_handle = project_file.file_path.open()
        response = FileResponse(
            file_handle,
            content_type='application/octet-stream'
        )
        response['Content-Disposition'] = (
            f'attachment; filename="{project_file.file_name}"'
        )
        return response

Эндпоинт: GET /api/v1/projects/files/download/<pk>/

Пример 5. URL-конфигурация — правильный порядок

# apps/projects/urls.py
from django.urls import path
from apps.projects.views.project_views import (
    ProjectListAPIView, ProjectDetailAPIView
)
from apps.projects.views.project_file_views import (
    ProjectFileListGenericView,
    ProjectFileDetailGenericView,
    DownloadProjectFileView,
)

urlpatterns = [
    path('', ProjectListAPIView.as_view()),
    path('<int:pk>/', ProjectDetailAPIView.as_view()),
    path('files/', ProjectFileListGenericView.as_view()),
    # ВАЖНО: download/ стоит ДО <int:pk>/, иначе Django попытается
    # найти ProjectFile с pk="download"
    path('files/download/<int:pk>/', DownloadProjectFileView.as_view()),
    path('files/<int:pk>/', ProjectFileDetailGenericView.as_view()),
]

Пример 6. UserListGenericView — фильтрация по проекту

# apps/users/views.py
from rest_framework.generics import ListAPIView
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework import status

from apps.users.models import User
from apps.users.serializers import UserListSerializer


class UserListGenericView(ListAPIView):
    serializer_class = UserListSerializer

    def get_queryset(self):
        project_name = self.request.query_params.get('project_name')
        if project_name:
            return User.objects.filter(project__name=project_name)
        return User.objects.all()

    def list(self, request: Request, *args, **kwargs) -> Response:
        users = self.get_queryset()
        if not users.exists():
            return Response(data=[], status=status.HTTP_204_NO_CONTENT)
        serializer = self.get_serializer(users, many=True)
        return Response(serializer.data, status=status.HTTP_200_OK)