✅ Решения мини-проекта Agile Projects
⚡ Ключевые решения
# Задание 1: модель Tag
class Tag(models.Model):
name = models.CharField(max_length=20, validators=[MinLengthValidator(4)])
def __str__(self): return self.name
# Задание 4: модель Task — deadline по умолчанию
deadline = models.DateTimeField(default=calculate_end_of_month)
# где calculate_end_of_month() считает последний день текущего месяца
# Задание 5: TagSerializer
class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
fields = '__all__'
# Задание 7: фильтрация по датам
date_from = timezone.make_aware(datetime.strptime(date_from, '%Y-%m-%d'))
projects = Project.objects.filter(created_at__range=[date_from, date_to])
# Задание 8: частичное обновление
serializer = CreateProjectSerializer(instance=project, data=request.data, partial=True)
# Задание 9: файл передаётся через context
serializer = CreateProjectFileSerializer(data=request.data, context={"raw_file": file_content})
Решение 1 — Модель Tag
Структура файлов:
- В пустом проекте создайте папку
appsс пустым файлом__init__.py— модуль. - Перейдите в эту папку в консоли и введите команду
python ../manage.py startapp tasks. - В настройках Django найдите список
INSTALLED_APPSи добавьте строку'apps.tasks.apps.TasksConfig'. - В новом приложении создайте модуль
models: удалите файлmodels.py, создайте файлtag.py.
# apps/tasks/models/tag.py
from django.db import models
from django.core.validators import MinLengthValidator
class Tag(models.Model):
name = models.CharField(max_length=20, validators=[MinLengthValidator(4)])
def __str__(self):
return self.name
# apps/tasks/models/__init__.py
from apps.tasks.models.tag import Tag
5. В консоль введите python manage.py makemigrations и следом python manage.py migrate.
6. Git-команды: git add . && git commit -m "feat: task 1 — Tag model" && git push origin feature/task-1
Решение 2 — Модель Project
- Перейдите в папку
apps, введите командуpython manage.py startapp projects. - В
INSTALLED_APPSдобавьте'apps.projects.apps.ProjectsConfig'. - Удалите
models.py, создайте файлproject.py.
# apps/projects/models/project.py
from django.db import models
class Project(models.Model):
name = models.CharField(unique=True, max_length=100)
description = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.name
class Meta:
ordering = ['-name']
Сортировка ordering = ['-name'] — по имени в обратном алфавитном порядке (знак - означает убывание).
Решение 3 — Модель ProjectFile и связь M2M
# apps/projects/models/project.py — обновлённая версия
from django.db import models
class Project(models.Model):
name = models.CharField(unique=True, max_length=100)
description = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
files = models.ManyToManyField('ProjectFile', related_name='project')
@property
def count_of_files(self):
return self.files.count()
def __str__(self):
return self.name
class Meta:
ordering = ['-name']
class ProjectFile(models.Model):
file_name = models.CharField(max_length=120)
file_path = models.FileField(upload_to='documents/')
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.file_name
class Meta:
ordering = ['-created_at']
@property count_of_files — вычисляемое поле, не хранится в БД, вычисляется динамически. Используется в сериализаторе ProjectDetailSerializer.
Решение 4 — Модель Task со статусами, приоритетами и дедлайном
# apps/tasks/utils/set_end_of_month.py
from datetime import datetime
import calendar
from django.utils import timezone
def calculate_end_of_month() -> datetime:
current_date = timezone.now()
amount_of_days = calendar.monthrange(
current_date.year,
current_date.month
)[1]
date = datetime(
year=current_date.year,
month=current_date.month,
day=amount_of_days,
)
return date.astimezone()
# apps/tasks/choices/statuses.py
from enum import Enum
class Statuses(Enum):
NEW = "NEW"
IN_PROGRESS = "IN_PROGRESS"
PENDING = "PENDING"
BLOCKED = "BLOCKED"
TESTING = "TESTING"
CLOSED = "CLOSED"
@classmethod
def choices(cls):
return [(attr.name, attr.value) for attr in cls]
# apps/tasks/choices/priority.py
from enum import Enum
class Priority(Enum):
VERY_LOW = (1, 'Very Low')
LOW = (2, 'Low')
MEDIUM = (3, 'Medium')
HIGH = (4, 'High')
CRITICAL = (5, 'Critical')
@classmethod
def choices(cls):
return [(key.value[0], key.value[1]) for key in cls]
def __getitem__(self, item):
return self.value[item]
# apps/tasks/models/task.py
from django.contrib.auth.models import User
from django.db import models
from apps.projects.models.project import Project
from apps.tasks.choices.statuses import Statuses
from apps.tasks.choices.priority import Priority
from apps.tasks.utils.set_end_of_month import calculate_end_of_month
class Task(models.Model):
name = models.CharField(max_length=120)
description = models.TextField()
status = models.CharField(
max_length=15,
choices=Statuses.choices,
default=Statuses.NEW
)
priority = models.SmallIntegerField(
choices=Priority,
default=Priority.MEDIUM[0]
)
project = models.ForeignKey(
Project,
on_delete=models.CASCADE,
related_name='tasks'
)
tags = models.ManyToManyField('Tag', related_name='tasks')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
deleted_at = models.DateTimeField(null=True, blank=True)
deadline = models.DateTimeField(default=calculate_end_of_month)
assignee = models.ForeignKey(
User,
on_delete=models.PROTECT,
related_name='tasks',
null=True,
blank=True
)
class Meta:
ordering = ['-deadline']
unique_together = ('name', 'project')
def __str__(self):
return f"{self.name}, status: {self.status}"
Решение 5 — TagListAPIView (GET список + POST создание)
# apps/tasks/serializers/tag_serializers.py
from rest_framework import serializers
from apps.tasks.models import Tag
class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
fields = '__all__'
# apps/tasks/views/tag_views.py
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework import status
from rest_framework.views import APIView
from apps.tasks.models import Tag
from apps.tasks.serializers.tag_serializers import TagSerializer
class TagListAPIView(APIView):
def get_objects(self):
return Tag.objects.all()
def get(self, request: Request) -> Response:
tags = self.get_objects()
if not tags.exists():
return Response(data=[], status=status.HTTP_204_NO_CONTENT)
serializer = TagSerializer(tags, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
def post(self, request: Request) -> Response:
serializer = TagSerializer(data=request.data)
if serializer.is_valid(raise_exception=True):
serializer.save()
return Response(serializer.validated_data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
# agile_projects/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/v1/', include('apps.routers')),
]
# apps/routers.py
from django.urls import path, include
urlpatterns = [
path('tasks/', include('apps.tasks.urls')),
]
# apps/tasks/urls.py
from django.urls import path
from apps.tasks.views.tag_views import TagListAPIView
urlpatterns = [
path('tags/', TagListAPIView.as_view()),
]
Решение 6 — TagDetailAPIView (GET/PUT/DELETE)
# apps/tasks/views/tag_views.py — добавить класс
from rest_framework.generics import get_object_or_404
class TagDetailAPIView(APIView):
def get_object(self, pk: int) -> Tag:
return get_object_or_404(Tag, pk=pk)
def get(self, request: Request, pk: int) -> Response:
tag = self.get_object(pk=pk)
serializer = TagSerializer(tag)
return Response(serializer.data, status=status.HTTP_200_OK)
def put(self, request: Request, pk: int) -> Response:
tag = self.get_object(pk=pk)
serializer = TagSerializer(tag, data=request.data)
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:
tag = self.get_object(pk=pk)
tag.delete()
return Response(
data={"message": "Tag was deleted successfully"},
status=status.HTTP_200_OK
)
# apps/tasks/urls.py — обновлённый
from apps.tasks.views.tag_views import TagListAPIView, TagDetailAPIView
urlpatterns = [
path('tags/', TagListAPIView.as_view()),
path('tags/<int:pk>/', TagDetailAPIView.as_view()),
]
Решение 7 — ProjectsListAPIView с фильтрацией по датам
# apps/projects/serializers/project_serializers.py
from rest_framework import serializers
from apps.projects.models import Project
class AllProjectsSerializer(serializers.ModelSerializer):
class Meta:
model = Project
fields = ('id', 'name', 'created_at')
class CreateProjectSerializer(serializers.ModelSerializer):
created_at = serializers.DateTimeField(read_only=True)
class Meta:
model = Project
fields = ('name', 'description', 'created_at')
def validate_description(self, value: str) -> str:
if len(value) < 30:
raise serializers.ValidationError(
"Description must be at least 30 characters long"
)
return value
# apps/projects/views/project_views.py
from datetime import datetime
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework import status
from rest_framework.views import APIView
from django.utils import timezone
from apps.projects.models import Project
from apps.projects.serializers.project_serializers import (
AllProjectsSerializer, CreateProjectSerializer
)
class ProjectsListAPIView(APIView):
def get_objects(self, date_from=None, date_to=None):
if date_from and date_to:
date_from = timezone.make_aware(
datetime.strptime(date_from, '%Y-%m-%d')
)
date_to = timezone.make_aware(
datetime.strptime(date_to, '%Y-%m-%d')
) if date_from else timezone.now().strftime('%Y-%m-%d')
return Project.objects.filter(
created_at__range=[date_from, date_to]
)
return Project.objects.all()
def get(self, request: Request) -> Response:
date_from = request.query_params.get('date_from')
date_to = request.query_params.get('date_to')
projects = self.get_objects(date_from, date_to)
if not projects.exists():
return Response(data=[], status=status.HTTP_204_NO_CONTENT)
serializer = AllProjectsSerializer(projects, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
def post(self, request: Request) -> Response:
serializer = CreateProjectSerializer(data=request.data)
if serializer.is_valid(raise_exception=True):
serializer.save()
return Response(serializer.validated_data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
# apps/routers.py — обновлённый
urlpatterns = [
path('tasks/', include('apps.tasks.urls')),
path('projects/', include('apps.projects.urls')),
]
# apps/projects/urls.py
from apps.projects.views.project_views import ProjectsListAPIView
urlpatterns = [
path('', ProjectsListAPIView.as_view()),
]
Решение 8 — ProjectDetailAPIView с частичным обновлением
# apps/projects/serializers/project_serializers.py — добавить
class ProjectDetailSerializer(serializers.ModelSerializer):
class Meta:
model = Project
fields = ('id', 'name', 'description', 'count_of_files')
# apps/projects/views/project_views.py — добавить класс
from rest_framework.generics import get_object_or_404
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
)
# apps/projects/urls.py — обновлённый
urlpatterns = [
path('', ProjectsListAPIView.as_view()),
path('<int:pk>/', ProjectDetailAPIView.as_view()),
]
Решение 9 — Работа с файлами
# apps/projects/utils/upload_file_helpers.py
import os
from pathlib import Path
ALLOWED_EXTENSIONS = ['.csv', '.doc', '.pdf', '.xlsx', '.py']
def check_extension(filename: str) -> bool:
extension = Path(filename).suffix
return extension in ALLOWED_EXTENSIONS
def check_file_size(file, required_size: int = 2) -> bool:
file_size = file.size / (1024 * 1024)
return file_size <= required_size
def create_file_path(file_name: str) -> str:
new_file_name, file_ext = file_name.split('.')
return f"documents/{new_file_name}.{file_ext}"
def save_file(file_path: str, file_content) -> str:
os.makedirs(os.path.dirname('documents/'), exist_ok=True)
with open(file_path, 'wb') as f:
for chunk in file_content.chunks():
f.write(chunk)
return file_path
# 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 AllProjectFilesSerializer(serializers.ModelSerializer):
class Meta:
model = ProjectFile
fields = ('id', 'file_name', 'project')
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):
file_path = create_file_path(file_name=validated_data['file_name'])
raw_file = self.context.get('raw_file')
if check_file_size(file=raw_file):
save_file(file_path=file_path, file_content=raw_file)
validated_data['file_path'] = file_path
return ProjectFile.objects.create(**validated_data)
raise serializers.ValidationError("File size is too large (2 MB as maximum).")
# apps/projects/views/project_file_views.py
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework import status
from rest_framework.views import APIView
from rest_framework.generics import get_object_or_404
from apps.projects.models import ProjectFile, Project
from apps.projects.serializers.project_file_serializers import (
AllProjectFilesSerializer, CreateProjectFileSerializer
)
class ProjectFileListAPIView(APIView):
def get_objects(self, project_name=None):
if project_name:
return ProjectFile.objects.filter(project__name=project_name)
return ProjectFile.objects.all()
def get(self, request: Request) -> Response:
project_name = request.query_params.get('project')
project_files = self.get_objects(project_name)
if not project_files.exists():
return Response(data=[], status=status.HTTP_204_NO_CONTENT)
serializer = AllProjectFilesSerializer(project_files, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
def post(self, request: Request) -> Response:
file_content = request.FILES["file"]
project_id = request.data["project_id"]
project = get_object_or_404(Project, pk=project_id)
serializer = CreateProjectFileSerializer(
data=request.data,
context={"raw_file": file_content}
)
if serializer.is_valid(raise_exception=True):
project_file = serializer.save()
project_file.project.set([project])
return Response(
{"message": "File upload successfully"},
status=status.HTTP_200_OK
)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
# apps/projects/urls.py — финальный
from apps.projects.views.project_file_views import ProjectFileListAPIView
urlpatterns = [
path('', ProjectsListAPIView.as_view()),
path('<int:pk>/', ProjectDetailAPIView.as_view()),
path('files/', ProjectFileListAPIView.as_view()),
]
Проверка через Postman: POST /api/v1/projects/files/ с Content-Type: multipart/form-data, поля: file_name, project_id, file (файл).