✅ Решения задач мини-проекта ч.3
⚡ Решения — ключевые файлы
- Задача 1:
project_file_views.py— get_serializer_class + get_queryset + list - Задача 2:
project_file_serializers.py+project_file_views.py+upload_file_helpers.py+urls.py - Задача 3:
users/models.py+users/choices/positions.py+settings.py - Задача 4:
users/serializers.py+users/views.py+users/urls.py+router.py - Задача 5:
users/serializers.py(RegisterUserSerializer) +users/views.py+users/urls.py - Задача 6:
tasks/serializers/task_serializers.py(SlugRelatedField) - Задача 7:
project_file_views.py(DownloadProjectFileView) +urls.py
Задача 1 — Список файлов проекта
1. Добавление методов (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:
project_file = ProjectFile.objects.filter(
project__name=project_name
)
return project_file
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
)
Почему get_serializer_class? Метод вызывается DRF автоматически при каждом запросе — не нужно переопределять get() и post() отдельно. GET возвращает больше полей (для чтения), POST принимает минимум (для записи).
2. Запуск и тестирование
python manage.py runserver
# GET /api/v1/projects/files/ — все файлы
# GET /api/v1/projects/files/?project=Alpha — файлы проекта "Alpha"
3. Git
git add apps/projects/views/project_file_views.py
git commit -m "feat: add get_serializer_class and list to ProjectFileListGenericView"
git push origin feature/task-35-1
Задача 2 — Детали файла + удаление
1. Сериализатор (apps/projects/serializers/project_file_serializers.py)
from rest_framework import serializers
from apps.projects.models import ProjectFile
from apps.projects.serializers.project_serializers import ProjectShortInfoSerializer
class ProjectFileDetailSerializer(serializers.ModelSerializer):
project = ProjectShortInfoSerializer(many=False)
class Meta:
model = ProjectFile
exclude = ('file_path',)
2. Отображение (apps/projects/views/project_file_views.py)
from rest_framework.generics import RetrieveDestroyAPIView, get_object_or_404
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. Функция удаления (apps/projects/utils/upload_file_helpers.py)
import os
def delete_file(file_path):
os.remove(os.path.realpath(file_path))
4. URL (apps/projects/urls.py)
from apps.projects.views.project_file_views import (
ProjectFileListGenericView,
ProjectFileDetailGenericView,
)
urlpatterns = [
path('', ProjectListAPIView.as_view()),
path('<int:pk>/', ProjectDetailAPIView.as_view()),
path('files/', ProjectFileListGenericView.as_view()),
path('files/<int:pk>/', ProjectFileDetailGenericView.as_view()), # NEW
]
Почему try/except вокруг delete_file? Файл на диске может быть уже удалён вручную, или у сервиса нет прав на директорию. Нужно поймать ошибку и вернуть 400, не давая краша. После успешного удаления с диска — удаляем запись из БД.
Задача 3 — Кастомный пользователь
1. Создание приложения
python manage.py startapp users
2. Позиции (apps/users/choices/positions.py)
from enum import Enum
class Positions(Enum):
CEO = "CEO"
CTO = "CTO"
DESIGNER = "Designer"
PRODUCT_OWNER = "Product Owner"
PROJECT_OWNER = "Project Owner"
PROGRAMMER = "Programmer"
PROJECT_MANAGER = "Project Manager"
QA = "QA"
@classmethod
def choices(cls):
return [(attr.name, attr.value) for attr in cls]
Современная альтернатива — TextChoices, но в лекции используется Enum.
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. settings.py
INSTALLED_APPS = [
...
'apps.users',
]
AUTH_USER_MODEL = 'users.User'
5. Миграции и суперпользователь
python manage.py makemigrations
python manage.py migrate
python manage.py createsuperuser
Задача 4 — Список пользователей
1. Сериализатор (apps/users/serializers.py)
from rest_framework import serializers
from apps.users.models import User
class UserListSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = (
'first_name',
'last_name',
'position',
'email',
'phone',
'last_login',
)
2. Отображение (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:
projects = self.get_queryset()
if not projects.exists():
return Response(data=[], status=status.HTTP_204_NO_CONTENT)
serializer = self.get_serializer(projects, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
3. router.py
from django.urls import path, include
urlpatterns = [
path('tasks/', include('apps.tasks.urls')),
path('projects/', include('apps.projects.urls')),
path('users/', include('apps.users.urls')), # NEW
]
4. apps/users/urls.py
from django.urls import path
from apps.users.views import UserListGenericView
urlpatterns = [
path('', UserListGenericView.as_view()),
]
Задача 5 — Регистрация пользователя
1. Сериализатор (apps/users/serializers.py)
from django.core.exceptions import ValidationError
from django.contrib.auth.password_validation import validate_password
import re
class RegisterUserSerializer(serializers.ModelSerializer):
re_password = serializers.CharField(
max_length=128,
write_only=True,
)
class Meta:
model = User
fields = (
'username', 'first_name', 'last_name',
'email', 'position', 'password', 're_password',
)
extra_kwargs = {
'password': {'write_only': True}
}
def validate(self, data):
username = data.get('username')
first_name = data.get('first_name')
last_name = data.get('last_name')
if not re.match('^[a-zA-Z0-9_]*$', username):
raise serializers.ValidationError(
"The username must be alphanumeric characters or have only _ symbol"
)
if not re.match('^[a-zA-Z]*$', first_name):
raise serializers.ValidationError(
"The first name must contain only alphabet symbols"
)
if not re.match('^[a-zA-Z]*$', last_name):
raise serializers.ValidationError(
"The last name must contain only alphabet symbols"
)
password = data.get("password")
re_password = data.get("re_password")
if password != re_password:
raise serializers.ValidationError({"password": "Passwords don't match"})
try:
validate_password(password)
except ValidationError as err:
raise serializers.ValidationError({"password": err.messages})
return data
def create(self, validated_data):
password = validated_data.pop('password')
validated_data.pop('re_password') # удаляем служебное поле!
user = User(**validated_data)
user.set_password(password) # хешируем
user.save()
return user
Ключевой момент: validated_data.pop('re_password') обязателен — иначе Django попытается сохранить несуществующее поле модели и выдаст ошибку.
2. Отображение (apps/users/views.py)
from rest_framework.generics import CreateAPIView
from apps.users.serializers import RegisterUserSerializer
class RegisterUserGenericView(CreateAPIView):
serializer_class = RegisterUserSerializer
def create(self, request: Request, *args, **kwargs) -> Response:
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
headers = self.get_success_headers(serializer.data)
return Response(
serializer.data,
status=status.HTTP_201_CREATED,
headers=headers
)
3. URL (apps/users/urls.py)
from apps.users.views import UserListGenericView, RegisterUserGenericView
urlpatterns = [
path('', UserListGenericView.as_view()),
path('register/', RegisterUserGenericView.as_view()),
]
Задача 6 — Assignee в задаче
1. Добавление поля (apps/tasks/serializers/task_serializers.py)
from apps.users.models import User
class CreateUpdateTaskSerializer(serializers.ModelSerializer):
# ... другие поля ...
assignee = serializers.SlugRelatedField( # NEW
slug_field='email',
queryset=User.objects.all(),
required=False
)
class Meta:
model = Task
fields = (
'deadline',
'assignee', # NEW (строчная буква!)
# ... другие нужные поля
)
Почему SlugRelatedField? Вместо передачи числового ID пользователя передаём email — более читаемо в API и не требует предварительного запроса для получения ID.
Задача 7 — Скачивание файла
1. Отображение (apps/projects/views/project_file_views.py)
from django.http import FileResponse
from rest_framework.views import APIView
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
2. URL (apps/projects/urls.py)
from apps.projects.views.project_file_views import (
ProjectFileListGenericView,
ProjectFileDetailGenericView,
DownloadProjectFileView, # NEW
)
urlpatterns = [
path('', ProjectsListAPIView.as_view()),
path('<int:pk>/', ProjectDetailAPIView.as_view()),
path('files/', ProjectFileListGenericView.as_view()),
path('files/download/<int:pk>/', DownloadProjectFileView.as_view()), # NEW — ВЫШЕ files/<pk>/!
path('files/<int:pk>/', ProjectFileDetailGenericView.as_view()),
]
Важно: files/download/<pk>/ должен стоять ВЫШЕ files/<pk>/. Иначе Django попытается найти файл с pk="download", что приведёт к 404.