✅ Решения: Practicum 1 Django Models

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

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

  • Задача 2: URLField(null=True, blank=True) + validators=[Min/MaxValueValidator]
  • Задача 3: verbose_name и help_text — не меняют БД
  • Задача 4: ForeignKey(Author, on_delete=models.SET_NULL, null=True)
  • Задача 8: ManyToManyField(Library, related_name='books')
  • Задача 11: метод is_overdue() через timezone.now().date()
  • Задача 12: @property на Book — вычисляет среднее из Review
  • Задача 13: OneToOneField(Author, on_delete=models.CASCADE, related_name='details')

← Вернуться к заданиям

Задача 1: Создание проекта и модели Author

Логика: Создаём Django-проект, регистрируем приложение в INSTALLED_APPS, определяем базовую модель, применяем миграции.

# Команды создания проекта
django-admin startproject config .
python manage.py startapp library

# config/settings.py — добавить в INSTALLED_APPS:
INSTALLED_APPS = [
    ...
    'library',
]
# library/models.py
from django.db import models

class Author(models.Model):
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    birth_date = models.DateField()
# library/admin.py
from django.contrib import admin
from .models import Author

admin.site.register(Author)
# Применение миграций
python manage.py makemigrations
python manage.py migrate
python manage.py createsuperuser  # для входа в Admin
После migrate запустите сервер (runserver) и откройте http://127.0.0.1:8000/admin/ — в разделе Library вы увидите «Authors».

Задача 2: Дополнительные поля Author

Логика: URLField для ссылки, BooleanField для флага удаления, IntegerField с валидаторами для рейтинга.

from django.core.validators import MinValueValidator, MaxValueValidator

class Author(models.Model):
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    birth_date = models.DateField()
    profile = models.URLField(null=True, blank=True)
    deleted = models.BooleanField(default=False)
    rating = models.IntegerField(
        default=1,
        validators=[MinValueValidator(1), MaxValueValidator(10)]
    )

После изменений: python manage.py makemigrations && python manage.py migrate

Задача 3: verbose_name и help_text

Логика: verbose_name меняет отображение поля в Admin-интерфейсе, не затрагивая имена колонок в БД. help_text добавляет подсказку под полем в форме Admin.

class Author(models.Model):
    first_name = models.CharField(max_length=100, verbose_name="Имя")
    last_name = models.CharField(max_length=100, verbose_name="Фамилия")
    birth_date = models.DateField(verbose_name="Дата рождения")
    profile = models.URLField(null=True, blank=True, verbose_name="Ссылка на профиль")
    deleted = models.BooleanField(
        default=False,
        verbose_name="Удалён ли автор",
        help_text="Если False - автор активен. Если True - автора больше нет в списке доступных"
    )
    rating = models.IntegerField(
        default=1,
        validators=[MinValueValidator(1), MaxValueValidator(10)],
        verbose_name="Рейтинг автора"
    )
Миграция в данном случае не требуется — verbose_name и help_text хранятся только в Python-коде, не в схеме БД. Django может создать пустую миграцию, но применение её не изменит таблицу.

Задача 4: Модель Book с ForeignKey

Логика: Связь Author→Book — «один ко многим». on_delete=models.SET_NULL означает: при удалении автора поле author_id у книги становится NULL (книга не удаляется).

class Book(models.Model):
    title = models.CharField(max_length=100)
    author_id = models.ForeignKey(Author, null=True, on_delete=models.SET_NULL)
    publishing_date = models.DateField()
Параметр on_delete обязателен в Django 2.0+. Без него получите ошибку TypeError.

Задача 5: Дополнение Book — choices и validators

Логика: choices ограничивает допустимые значения поля. null=True, blank=True делает поле необязательным и в БД, и в формах.

GENRE_CHOICES = [
    ('Fiction', 'Fiction'),
    ('Non-Fiction', 'Non-Fiction'),
    ('Science Fiction', 'Science Fiction'),
    ('Fantasy', 'Fantasy'),
    ('Mystery', 'Mystery'),
    ('Biography', 'Biography'),
]

class Book(models.Model):
    title = models.CharField(max_length=100)
    author_id = models.ForeignKey(Author, null=True, on_delete=models.SET_NULL)
    publishing_date = models.DateField()
    summary = models.TextField(null=True, blank=True)
    genre = models.CharField(max_length=50, null=True, choices=GENRE_CHOICES)
    page_count = models.IntegerField(
        null=True, blank=True,
        validators=[MaxValueValidator(10000)]
    )

Задача 6: Модель Publisher + Book.publisher_id

Логика: Создаём отдельную таблицу Publisher и ссылаемся на неё из Book через ForeignKey.

class Publisher(models.Model):
    name = models.CharField(max_length=100)
    address = models.CharField(max_length=255, null=True)
    city = models.CharField(max_length=100, null=True)
    country = models.CharField(max_length=100)

class Book(models.Model):
    ...
    publisher_id = models.ForeignKey(Publisher, null=True, on_delete=models.CASCADE)

Задача 7: Модель Category + related_name

Логика: Связь Category→Book — «один ко многим» (у одной категории много книг, у книги одна категория). related_name='books' позволяет обратный доступ: category.books.all().

class Category(models.Model):
    name = models.CharField(max_length=30, unique=True)

class Book(models.Model):
    ...
    category = models.ForeignKey(
        Category, null=True, on_delete=models.SET_NULL, related_name='books'
    )

Задача 8: Модель Library + ManyToMany

Логика: Библиотека↔Книга — «многие ко многим». Одна книга может быть в нескольких библиотеках, одна библиотека содержит много книг. Django сам создаёт промежуточную таблицу.

class Library(models.Model):
    name = models.CharField(max_length=100)
    location = models.CharField(max_length=200)
    site = models.URLField(null=True, blank=True)

class Book(models.Model):
    ...
    libraries = models.ManyToManyField(Library, related_name='books')
# Использование в shell:
book.libraries.all()     # все библиотеки книги
library.books.all()      # все книги библиотеки
book.libraries.add(lib)  # добавить книгу в библиотеку

Задача 9: Модель Member, удалить Publisher

Логика: Member заменяет Publisher. Удаляем класс Publisher из models.py, меняем ForeignKey в Book. null=True на publisher_id гарантирует, что книги с пустым полем не вызовут ошибку.

GENDER_CHOICES = [
    ('Male', 'Мужской'),
    ('Female', 'Женский'),
    ('Other', 'Другой'),
]

ROLE_CHOICES = [
    ('Admin', 'Администратор'),
    ('Staff', 'Сотрудник'),
    ('Reader', 'Читатель'),
]

class Member(models.Model):
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    email = models.EmailField(unique=True)
    gender = models.CharField(max_length=50, choices=GENDER_CHOICES)
    birth_date = models.DateField()
    age = models.IntegerField(validators=[MinValueValidator(6), MaxValueValidator(120)])
    role = models.CharField(max_length=20, choices=ROLE_CHOICES)
    active = models.BooleanField(default=True)
    libraries = models.ManyToManyField('Library', related_name='members')

class Book(models.Model):
    ...
    publisher_id = models.ForeignKey(Member, null=True, on_delete=models.CASCADE)

Задача 10: Модель Posts с unique_for_date

Логика: unique_for_date='created_at' означает: комбинация (title, дата created_at) должна быть уникальной. auto_now=True на updated_at — дата обновляется автоматически при каждом .save().

class Posts(models.Model):
    title = models.CharField(max_length=255, unique_for_date='created_at')
    body = models.TextField()
    author = models.ForeignKey(Member, on_delete=models.CASCADE, related_name='posts')
    moderated = models.BooleanField(default=False)
    library = models.ForeignKey(Library, on_delete=models.CASCADE, related_name='posts')
    created_at = models.DateField()
    updated_at = models.DateField(auto_now=True)

Задача 11: Модель Borrow + метод is_overdue

Логика: Три ForeignKey — по одному на каждую связанную сущность. Метод is_overdue() сравнивает return_date с текущей датой через timezone.now().date() (timezone-aware, учитывает часовой пояс Django).

from django.utils import timezone

class Borrow(models.Model):
    member = models.ForeignKey(Member, on_delete=models.CASCADE, related_name='borrows')
    book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name='borrows')
    library = models.ForeignKey(Library, on_delete=models.CASCADE, related_name='borrows')
    borrow_date = models.DateField()
    return_date = models.DateField()
    returned = models.BooleanField(default=False)

    def is_overdue(self):
        if self.returned:
            return False  # книга возвращена — просрочки нет
        return self.return_date < timezone.now().date()

Задача 12: Модель Review + property на Book

Логика: @property не создаёт колонку в БД — это вычисляемое значение. При обращении к book.rating Django делает запрос в таблицу Review и считает среднее.

class Review(models.Model):
    book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name='reviews')
    reviewer = models.ForeignKey(Member, on_delete=models.CASCADE, related_name='reviews')
    rating = models.FloatField()
    description = models.TextField()

# Добавить в класс Book:
class Book(models.Model):
    ...
    @property
    def rating(self):
        reviews = self.reviews.all()
        total_reviews = reviews.count()
        if total_reviews == 0:
            return 0
        total_rating = sum(review.rating for review in reviews)
        average_rating = total_rating / total_reviews
        return round(average_rating, 2)
Поле rating в задаче 2 и @property rating в задаче 12 конфликтуют — они определены в одном классе Author / Book. В данном практикуме rating IntegerField у Author — рейтинг популярности, а @property rating у Book — средняя оценка отзывов. Это разные модели, конфликта нет.

Задача 13: Модель AuthorDetail с OneToOneField

Логика: OneToOne гарантирует, что у каждого автора может быть ровно одна запись AuthorDetail. related_name='details' позволяет author.details.biography.

class AuthorDetail(models.Model):
    author = models.OneToOneField(Author, on_delete=models.CASCADE, related_name='details')
    biography = models.TextField()
    birth_city = models.CharField(max_length=50)
    gender = models.CharField(max_length=50, choices=GENDER_CHOICES)
# Использование:
author.details.biography  # получить биографию
AuthorDetail.objects.create(author=author, biography="...", birth_city="Тула", gender="Male")

Задача 14: __str__ для всех моделей

Логика: Метод __str__ определяет, как объект отображается в Django Admin, shell, и repr(). Под каждую модель — свой формат.

class Author(models.Model):
    def __str__(self):
        return f"{self.first_name} {self.last_name[0]}."  # "Leo T."

class Book(models.Model):
    def __str__(self):
        return self.title

class Library(models.Model):
    def __str__(self):
        return self.name

class Member(models.Model):
    def __str__(self):
        return f"{self.first_name} {self.last_name} ({self.role})"

class Category(models.Model):
    def __str__(self):
        return self.name

class Borrow(models.Model):
    def __str__(self):
        return f"{self.member} → {self.book}"

class Review(models.Model):
    def __str__(self):
        return f"Отзыв {self.reviewer} на {self.book}: {self.rating}"

class AuthorDetail(models.Model):
    def __str__(self):
        return f"Детали: {self.author}"

class Posts(models.Model):
    def __str__(self):
        return self.title

class Event(models.Model):
    def __str__(self):
        return self.title

Задача 15: Система событий Event + EventParticipant

Логика: Event связан с Library (ForeignKey) и книгами (ManyToMany). EventParticipant связывает событие и участника. default=timezone.now — текущая дата по умолчанию (передаём функцию, не её результат).

class Event(models.Model):
    title = models.CharField(max_length=255)
    description = models.TextField()
    date = models.DateField()  # в лекции DateField; для дат+времени — DateTimeField
    library = models.ForeignKey(Library, on_delete=models.CASCADE, related_name='events')
    books = models.ManyToManyField(Book, related_name='events')

class EventParticipant(models.Model):
    event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name='participants')
    member = models.ManyToManyField(Member, related_name='event_participations')
    registration_date = models.DateField(default=timezone.now)
Разница между DateField и DateTimeField:
В задаче сказано «и дата, и время» для поля события, однако в решении лекции использован DateField. Если нужно хранить время начала события — используйте DateTimeField.