✅ Решения практикума 2

Разбор всех 18 задач с пояснением логики

⚡ Ключевые решения

  • Задача 1: CharField(unique=True), TextField(), DateTimeField(auto_now_add=True)
  • Задача 2: validators=[MinLengthValidator(10)], choices=STATUSES_CHOICES, default='New', ForeignKey(Project, on_delete=models.CASCADE)
  • Задача 3: ManyToManyField('Tag', blank=True), DateTimeField(null=True, blank=True) для due_date
  • Задача 7: ordering=['-name'], unique_together=(('name', 'created_at'),)
  • Задача 12: @property + метод в ModelAdmin с .short_description
  • Задача 18: dumpdata auth.Group → удалить db → migrateloaddata

Задача 1: Модель Project

Логика: unique=True на CharField создаёт уникальный индекс в БД. TextField() — текст без ограничения длины. auto_now_add=True заполняется один раз при создании объекта.

from django.db import models

class Project(models.Model):
    name = models.CharField(max_length=100, unique=True)
    description = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.name

Задача 2: Модель Task

Логика: MinLengthValidator(10) проверяет минимальную длину на уровне Django-валидации. Choices ограничивают допустимые значения. on_delete=models.CASCADE каскадно удаляет задачи при удалении проекта. auto_now=True обновляется при каждом сохранении.

from django.core.validators import MinLengthValidator

STATUSES_CHOICES = [
    ('New', 'New'),
    ('In_progress', 'In_progress'),
    ('Completed', 'Completed'),
    ('Closed', 'Closed'),
    ('Pending', 'Pending'),
    ('Blocked', 'Blocked'),
]

PRIORITY_CHOICES = [
    ('Low', 'Low'),
    ('Medium', 'Medium'),
    ('High', 'High'),
    ('Very High', 'Very High'),
]

class Task(models.Model):
    title = models.CharField(
        max_length=255, unique=True,
        validators=[MinLengthValidator(10)]
    )
    description = models.TextField(null=True, blank=True)
    status = models.CharField(
        max_length=15, choices=STATUSES_CHOICES, default='New'
    )
    priority = models.CharField(max_length=15, choices=PRIORITY_CHOICES)
    project = models.ForeignKey(Project, on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    deleted_at = models.DateTimeField(null=True, blank=True)

    def __str__(self):
        return self.title

Задача 3: Tag + due_date + M2M

Логика: Модель Tag создаётся отдельно. Поле due_date — nullable DateTimeField. Для M2M достаточно blank=True, null=True для ManyToManyField игнорируется Django.

class Tag(models.Model):
    name = models.CharField(max_length=20, unique=True)

    def __str__(self):
        return self.name

class Task(models.Model):
    ...  # поля из задачи 2
    due_date = models.DateTimeField(null=True, blank=True)  # NEW
    tags = models.ManyToManyField('Tag', blank=True, related_name='tasks')  # NEW

Задача 4: Admin — базовая настройка

Логика: list_display — колонки в списке. search_fields — поля для полнотекстового поиска. list_filter — панель фильтров справа.

from django.contrib import admin
from .models import Project, Task, Tag

@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
    list_display = ['name']
    search_fields = ['name']

@admin.register(Project)
class ProjectAdmin(admin.ModelAdmin):
    list_display = ['name', 'created_at']
    search_fields = ['name']

@admin.register(Task)
class TaskAdmin(admin.ModelAdmin):
    list_display = ['title', 'project', 'status', 'priority', 'created_at', 'due_date']
    search_fields = ['title']
    list_filter = ['status', 'priority', 'project', 'created_at', 'due_date']

Задача 5: Связь Task → User (assignee)

Логика: ForeignKey к встроенной модели User. null=True, blank=True — задача может быть без исполнителя. При удалении пользователя задача сохраняется с assignee=None (решение безопаснее, чем CASCADE из источника).

from django.contrib.auth.models import User

class Task(models.Model):
    ...
    assignee = models.ForeignKey(
        User, null=True, blank=True, on_delete=models.CASCADE
    )  # NEW — из источника практикума

Задача 6: Пользователь в Admin

Логика: Пользователи создаются напрямую в Admin → Authentication and Authorization → Users. Для отображения в TaskAdmin добавляем assignee в list_display и list_filter.

@admin.register(Task)
class TaskAdmin(admin.ModelAdmin):
    list_display = [
        'title', 'project', 'status', 'priority',
        'created_at', 'due_date', 'assignee'  # добавлено
    ]
    list_filter = [
        'status', 'priority', 'project',
        'created_at', 'due_date', 'assignee'  # добавлено
    ]

Создание пользователя решается напрямую в Административной панели. Пользователя можно создать в Django Admin в таблице User.

Задача 7: Meta для Project

Логика: ordering=['-name'] — убывание по имени. unique_together гарантирует уникальность комбинации полей на уровне БД.

class Project(models.Model):
    ...
    class Meta:
        ordering = ['-name']
        verbose_name = 'Project'
        verbose_name_plural = 'Projects'
        unique_together = (('name', 'created_at'),)

Задача 8: Meta для Task

Логика: Порядок сортировки по due_date (убывание) и priority. Уникальность — пара (title, project): одна задача с таким именем в рамках одного проекта.

class Task(models.Model):
    ...
    class Meta:
        ordering = ['-due_date', '-priority']
        verbose_name = 'Task'
        verbose_name_plural = 'Tasks'
        unique_together = (('title', 'project'),)

Задача 9: Модель ProjectFile + связь M2M с Project

Логика: FileField(upload_to='projects/') сохраняет файлы в папку MEDIA_ROOT/projects/. M2M позволяет одному файлу принадлежать нескольким проектам.

class ProjectFile(models.Model):
    name = models.CharField(max_length=120)
    file = models.FileField(upload_to='projects/')
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.name

class Project(models.Model):
    ...
    files = models.ManyToManyField(ProjectFile, related_name='projects', blank=True)  # NEW

Задача 10: Admin для ProjectFile

@admin.register(ProjectFile)
class ProjectFileAdmin(admin.ModelAdmin):
    list_display = ['name', 'file', 'created_at']
    search_fields = ['name']
    list_filter = ['created_at']

Задача 11: Meta для ProjectFile

class ProjectFile(models.Model):
    ...
    class Meta:
        verbose_name = 'Project File'
        verbose_name_plural = 'Project Files'
        ordering = ['-created_at']

Задача 12: Property + Admin (count_of_files)

Логика: @property в модели возвращает вычисляемое значение. В Admin нельзя напрямую указать property в list_display — нужен оборачивающий метод в ModelAdmin с атрибутом short_description.

class Project(models.Model):
    ...
    @property
    def count_of_files(self):
        return self.files.count()

@admin.register(Project)
class ProjectAdmin(admin.ModelAdmin):
    list_display = ['name', 'display_count_of_files', 'created_at']
    search_fields = ['name']

    def display_count_of_files(self, obj):
        return obj.count_of_files
    display_count_of_files.short_description = 'Count of Files'

Задача 13: Admin-действие — замена пробелов

Логика: Метод принимает request и queryset (в источнике — objects, но это одно и то же). short_description — отображаемое название в выпадающем меню Actions.

@admin.register(Project)
class ProjectAdmin(admin.ModelAdmin):
    actions = ['replace_spaces_with_underscores']

    def replace_spaces_with_underscores(self, request, objects):
        for obj in objects:
            obj.name = obj.name.replace(' ', '_')
            obj.save()
        return objects
    replace_spaces_with_underscores.short_description = 'Replace spaces with underscores'

Задача 14: Admin-действие — смена статуса

@admin.register(Task)
class TaskAdmin(admin.ModelAdmin):
    actions = ['change_status']

    def change_status(self, request, objects):
        for obj in objects:
            obj.status = 'Closed'
            obj.save()
        return objects
    change_status.short_description = 'Mark as Closed'

Задача 15: Admin-действия — смена приоритета (4 шт.)

@admin.register(Task)
class TaskAdmin(admin.ModelAdmin):
    actions = [
        'change_status',
        'change_priority_to_low',
        'change_priority_to_medium',
        'change_priority_to_high',
        'change_priority_to_very_high',
    ]

    def change_priority_to_low(self, request, objects):
        for obj in objects:
            obj.priority = 'Low'
            obj.save()
    change_priority_to_low.short_description = 'Mark as Low priority'

    def change_priority_to_medium(self, request, objects):
        for obj in objects:
            obj.priority = 'Medium'
            obj.save()
    change_priority_to_medium.short_description = 'Mark as Medium priority'

    def change_priority_to_high(self, request, objects):
        for obj in objects:
            obj.priority = 'High'
            obj.save()
    change_priority_to_high.short_description = 'Mark as High priority'

    def change_priority_to_very_high(self, request, objects):
        for obj in objects:
            obj.priority = 'Very High'
            obj.save()
    change_priority_to_very_high.short_description = 'Mark as Very High priority'

Задача 16: Группы разрешений

Логика: Группы настраиваются в Admin → Authentication and Authorization → Groups. Нажмите "Add group", задайте имя и выберите нужные разрешения из списка.

Manager — разрешения:

Authentication and Authorization | group | Can view group
Authentication and Authorization | permission | Can view permission
Authentication and Authorization | user | Can add user
Authentication and Authorization | user | Can view user
Practicum_2 | Project | Can add/change/delete/view project
Practicum_2 | Project File | Can add/change/delete/view project file
Practicum_2 | Tag | Can add/change/view tag
Practicum_2 | Task | Can add/change/view task

Client — разрешения:

Authentication and Authorization | user | Can add/change/view user
Practicum_2 | Project | Can add/change/view project
Practicum_2 | Project File | Can add/view project file
Practicum_2 | Tag | Can add/change/delete/view tag
Practicum_2 | Task | Can add/change/delete/view task

Developer — разрешения:

Authentication and Authorization | user | Can add/change/delete/view user
Practicum_2 | Project | Can add/change/delete/view project
Practicum_2 | Project File | Can add/change/delete/view project file
Practicum_2 | Tag | Can add/change/delete/view tag
Practicum_2 | Task | Can add/change/delete/view task

Задача 17: Пользователь с группой Client

Логика: Django предоставляет базовую модель User. Нужно создать нового пользователя, отредактировать его, добавив группу разрешений и флаг is_staff.

  1. Admin → Users → Add User → заполнить username/password
  2. Отредактировать пользователя: поставить галочку Staff status
  3. В разделе "Groups" выбрать группу "Client"
  4. Сохранить и войти под этим пользователем в Admin
  5. Убедиться, что недоступные действия (удаление проекта, удаление пользователя) не отображаются

Задача 18: Fixtures

Логика: dumpdata сериализует данные в JSON. --natural-foreign заменяет числовые ID на "естественные" ключи (имена), что делает fixture переносимым. loaddata воссоздаёт объекты из JSON.

# 1. Сохранить группы разрешений
python manage.py dumpdata auth.Group --natural-foreign --indent=4 \
    > practicum_2/fixtures/groups_fixture.json

# 2. Сохранить пользователей
python manage.py dumpdata auth.User --natural-foreign --indent=4 \
    > practicum_2/fixtures/users_fixture.json

# 3. Удалить базу данных (удалить файл db.sqlite3)

# 4. Провести все миграции заново
python manage.py makemigrations
python manage.py migrate

# 5. Применить снимок групп разрешений
python manage.py loaddata practicum_2/fixtures/groups_fixture.json

# 6. Применить снимок пользователей
python manage.py loaddata practicum_2/fixtures/users_fixture.json
Порядок важен: сначала загружать группы, потом пользователей — потому что пользователи ссылаются на группы. Если поменять порядок, loaddata завершится с ошибкой IntegrityError.