💻 Примеры кода: Agile Projects ч.2

К оглавлению урока

⚡ Ключевые фрагменты

# ProjectDetailAPIView — GET/PUT(partial)/DELETE
class ProjectDetailAPIView(APIView):
    def get_object(self, pk):
        return get_object_or_404(Project, pk=pk)
    def put(self, request, pk):
        serializer = CreateProjectSerializer(
            instance=self.get_object(pk), data=request.data, partial=True)
        serializer.is_valid(raise_exception=True)
        serializer.save()
        return Response(serializer.validated_data)

# TasksListAPIView с пагинацией
class TasksListAPIView(APIView):
    paginator = StandardResultsSetPagination
    def get(self, request):
        tasks = Task.objects.filter(project__name=request.query_params.get('project_name'))
        paginator = self.paginator()
        page = paginator.paginate_queryset(tasks, request, view=self)
        if page is not None:
            return paginator.get_paginated_response(AllTasksSerializer(page, many=True).data)

Пример 1: ProjectDetailSerializer и ProjectDetailAPIView

Сериализатор для отображения деталей проекта (задача 1):

# apps/projects/serializers/project_serializers.py
class ProjectDetailSerializer(serializers.ModelSerializer):
    class Meta:
        model = Project
        fields = ('id', 'name', 'description', 'count_of_files')

View для GET/PUT/DELETE конкретного проекта:

# apps/projects/views/project_views.py
from rest_framework.views import APIView
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework import status
from rest_framework.generics import get_object_or_404
from apps.projects.models import Project
from apps.projects.serializers.project_serializers import (
    ProjectDetailSerializer, CreateProjectSerializer
)

class ProjectDetailAPIView(APIView):
    def get_object(self, pk: int):
        return get_object_or_404(Project, pk=pk)

    def get(self, request: Request, pk: int) -> Response:
        project = self.get_object(pk=pk)
        serializer = ProjectDetailSerializer(project)
        return Response(serializer.data, status=status.HTTP_200_OK)

    def put(self, request: Request, pk: int) -> Response:
        project = self.get_object(pk=pk)
        serializer = CreateProjectSerializer(
            instance=project,
            data=request.data,
            partial=True          # <-- частичное обновление
        )
        if serializer.is_valid(raise_exception=True):
            serializer.save()
            return Response(serializer.validated_data, status=status.HTTP_200_OK)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    def delete(self, request: Request, pk: int) -> Response:
        project = self.get_object(pk=pk)
        project.delete()
        return Response(
            data={"message": "Project was deleted successfully"},
            status=status.HTTP_200_OK
        )

URL-регистрация:

# apps/projects/urls.py
urlpatterns = [
    path('', ProjectsListAPIView.as_view()),
    path('<int:pk>/', ProjectDetailAPIView.as_view()),  # NEW
]

Пример 2: AllTasksSerializer и TasksListAPIView с пагинацией

# apps/tasks/serializers/task_serializers.py
from rest_framework import serializers
from apps.projects.models import Project
from apps.tasks.models import Task, Tag

class AllTasksSerializer(serializers.ModelSerializer):
    project = serializers.SlugRelatedField(read_only=True, slug_field='name')
    assignee = serializers.SlugRelatedField(read_only=True, slug_field='email')

    class Meta:
        model = Task
        fields = ('id', 'name', 'status', 'priority', 'project', 'assignee', 'deadline')
# apps/tasks/views/task_views.py
from rest_framework.pagination import PageNumberPagination
from rest_framework.views import APIView

class StandardResultsSetPagination(PageNumberPagination):
    page_size = 5
    page_size_query_param = 'page_size'
    max_page_size = 15

class TasksListAPIView(APIView):
    paginator = StandardResultsSetPagination

    def get_objects(self):
        project_name = self.request.query_params.get('project_name')
        assignee_email = self.request.query_params.get('assignee_email')
        if project_name:
            return Task.objects.filter(project__name=project_name)
        elif assignee_email:
            return Task.objects.filter(assignee__email=assignee_email)
        return Task.objects.all()

    def get(self, request, *args, **kwargs):
        tasks = self.get_objects()
        if not tasks.exists():
            return Response(data=[], status=status.HTTP_204_NO_CONTENT)
        paginator = self.paginator()
        page = paginator.paginate_queryset(tasks, request, view=self)
        if page is not None:
            serializer = AllTasksSerializer(page, many=True)
            return paginator.get_paginated_response(serializer.data)
        serializer = AllTasksSerializer(tasks, many=True)
        return Response(serializer.data, status=status.HTTP_200_OK)

    def post(self, request, *args, **kwargs):
        serializer = CreateTaskSerializer(data=request.data)
        if serializer.is_valid(raise_exception=True):
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

Пример 3: CreateProjectFileSerializer с chunked save

# apps/projects/serializers/project_file_serializers.py
from rest_framework import serializers
from apps.projects.models import ProjectFile
from apps.projects.utils.upload_file_helpers import (
    check_extension, create_file_path, check_file_size, save_file
)

class CreateProjectFileSerializer(serializers.ModelSerializer):
    class Meta:
        model = ProjectFile
        fields = ('file_name',)

    def validate_file_name(self, value: str) -> str:
        if not value.isascii():
            raise serializers.ValidationError("Please, provide a valid file name.")
        if not check_extension(value):
            raise serializers.ValidationError(
                "Valid file extensions: ['.csv', '.doc', '.pdf', '.xlsx']"
            )
        return value

    def create(self, validated_data):
        project = self.context.get('project')
        raw_file = self.context.get('raw_file')
        file_path = create_file_path(
            project_name=project.name,
            file_name=validated_data['file_name']
        )
        if check_file_size(file=raw_file):
            save_file(file_path=file_path, file_content=raw_file)
            validated_data['file_path'] = file_path
            project_file = ProjectFile.objects.create(**validated_data)
            project_file.project.add(project)
            return project_file
        else:
            raise serializers.ValidationError("File size is too large (2 MB as maximum).")

Пример 4: Кастомная модель 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)
    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}"
# settings.py
AUTH_USER_MODEL = 'users.User'  # добавить до первой миграции!

Пример 5: ProjectFileDetailGenericView с удалением файла

# apps/projects/views/project_file_views.py
from rest_framework.generics import RetrieveDestroyAPIView, get_object_or_404
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, *args, **kwargs):
        task = self.get_object()
        serializer = self.get_serializer(task)
        return Response(data=serializer.data, status=status.HTTP_200_OK)

    def destroy(self, request, *args, **kwargs):
        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)