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

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

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

  • 2.1: CharField(max_length=40, unique=True), Meta: ordering=['name'], verbose_name_plural='categories'
  • 2.3: ForeignKey(Category, on_delete=models.PROTECT, related_name='products')
  • 2.4: OneToOneField(Product, on_delete=models.CASCADE, related_name='details')
  • 2.6: OneToOneField(Address, null=True, on_delete=models.SET_NULL), Meta: get_latest_by='date_joined'
  • 3.3: list_editable требует тех же полей в list_display
  • 3.6: поиск по связанной модели: customer__first_name

Раздел 1: Настройка проекта

Задание 1.1: Клонирование репозитория

Логика: стандартный Git-workflow для начала работы над существующим проектом.

git clone <URL вашего репозитория>
cd <название репозитория>

Задание 1.2: Установка зависимостей

Логика: виртуальное окружение изолирует зависимости проекта от системного Python. requirements.txt фиксирует версии пакетов.

python -m venv venv

# Windows:
venv\Scripts\activate
# macOS и Linux:
source venv/bin/activate

pip install -r requirements.txt

Задание 1.3: Настройка базы данных

Логика: migrate применяет все миграции из репозитория, создавая таблицы БД. Файл .env содержит чувствительные данные (пароли, секретный ключ) — он не хранится в git.

python manage.py migrate

Задание 1.4: Запуск сервера разработки

python manage.py runserver
# Открыть: http://127.0.0.1:8000

Раздел 2: Создание моделей

Задание 2.1: Модель Category

Логика: unique=True создаёт уникальный индекс в БД. ordering=['name'] в Meta обеспечивает сортировку по умолчанию при .all(). verbose_name_plural исправляет автоматически сгенерированное «categorys» → «categories».

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

    def __str__(self):
        return self.name

    class Meta:
        ordering = ['name']
        verbose_name_plural = 'categories'

Задание 2.2: Модель Supplier

Логика: EmailField автоматически валидирует формат email на уровне Django-форм. Уникальность email и телефона гарантирует отсутствие дублирующихся поставщиков по этим полям.

class Supplier(models.Model):
    name = models.CharField(max_length=100, unique=True)
    contact_email = models.EmailField(unique=True)
    phone_number = models.CharField(max_length=20, unique=True)

    def __str__(self):
        return self.name

Задание 2.3: Модель Product

Логика: on_delete=models.PROTECT защищает от случайного удаления Category или Supplier — сначала нужно удалить или перенести все продукты. PositiveSmallIntegerField для quantity — небольшие значения, занимает меньше места. db_index=True на article ускоряет поиск по артикулу.

class Product(models.Model):
    name = models.CharField(max_length=100)
    category = models.ForeignKey(
        Category, on_delete=models.PROTECT, related_name='products'
    )
    supplier = models.ForeignKey(
        Supplier, on_delete=models.PROTECT, related_name='products'
    )
    price = models.DecimalField(max_digits=10, decimal_places=2)
    quantity = models.PositiveSmallIntegerField()
    article = models.CharField(
        max_length=100, unique=True,
        help_text='Unique string product id',
        db_index=True
    )
    available = models.BooleanField(default=True)

    def __str__(self):
        return self.name

    class Meta:
        ordering = ['category', 'quantity']

Задание 2.4: Модель ProductDetail

Логика: OneToOneField с CASCADE — при удалении продукта его детали удаляются автоматически. Все поля nullable (null=True, blank=True) — детали необязательны.

class ProductDetail(models.Model):
    product = models.OneToOneField(
        Product, on_delete=models.CASCADE, related_name='details'
    )
    description = models.TextField(null=True, blank=True)
    manufacturing_date = models.DateField(null=True, blank=True)
    expiration_date = models.DateField(null=True, blank=True)
    weight = models.DecimalField(
        null=True, blank=True, max_digits=5, decimal_places=2
    )

    def __str__(self):
        return f"Details of {self.product.name}"

Задание 2.5: Модель Address

Логика: простая модель адреса. verbose_name_plural='addresses' нужен из-за нестандартного английского множественного числа (не «addresss»).

class Address(models.Model):
    country = models.CharField(max_length=100)
    city = models.CharField(max_length=100)
    street = models.CharField(max_length=255)
    house = models.CharField(max_length=6)

    def __str__(self):
        return f"{self.street}, {self.house}"

    class Meta:
        verbose_name_plural = 'addresses'

Задание 2.6: Модель Customer

Логика: OneToOneField(Address, null=True, on_delete=models.SET_NULL) — если адрес удалён, поле обнуляется (покупатель не теряется). auto_now_add=True заполняется один раз при создании. Паттерн мягкого удаления: deleted=BooleanField + deleted_at=DateTimeField(null=True). get_latest_by позволяет вызвать Customer.objects.latest() без аргументов.

class Customer(models.Model):
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    email = models.EmailField(unique=True)
    phone_number = models.CharField(max_length=15)
    address = models.OneToOneField(
        Address, null=True, on_delete=models.SET_NULL, related_name='customer'
    )
    date_joined = models.DateTimeField(auto_now_add=True)
    deleted = models.BooleanField(default=False)
    deleted_at = models.DateTimeField(null=True, blank=True)

    def __str__(self):
        return f"{self.first_name} {self.last_name}"

    class Meta:
        ordering = ['-date_joined']
        get_latest_by = 'date_joined'

Задание 2.7: Модель Order

Логика: PROTECT на customer — нельзя удалить покупателя, пока у него есть заказы. Минус знак в ordering обеспечивает убывающий порядок (свежие заказы первыми).

class Order(models.Model):
    order_date = models.DateTimeField(auto_now_add=True)
    customer = models.ForeignKey(
        Customer, on_delete=models.PROTECT, related_name='orders'
    )

    def __str__(self):
        return f"Order {self.id} by {self.customer}"

    class Meta:
        ordering = ['-order_date']
        get_latest_by = 'order_date'

Задание 2.8: Модель OrderItem

Логика: CASCADE на order — при удалении заказа все его позиции удаляются. PROTECT на product — нельзя удалить продукт, пока он встречается в заказах. Поле price в OrderItem фиксирует цену на момент заказа (независимо от изменений цены в Product).

class OrderItem(models.Model):
    order = models.ForeignKey(
        Order, on_delete=models.CASCADE, related_name='order_items'
    )
    product = models.ForeignKey(
        Product, on_delete=models.PROTECT, related_name='order_items'
    )
    quantity = models.PositiveSmallIntegerField()
    price = models.DecimalField(max_digits=10, decimal_places=2)

    def __str__(self):
        return f"{self.quantity} x {self.product.name} for {self.order}"

Раздел 3: Admin-классы

Задание 3.0: Импорт моделей

from .models import (
    Category, Supplier, Product,
    ProductDetail, Address, Customer, Order, OrderItem
)

Задание 3.1: CategoryAdmin

Логика: минимальная настройка для простой модели. ordering в Admin дублирует Meta.ordering, но позволяет пользователю Admin менять порядок по клику на заголовок колонки.

@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
    list_display = ('name',)
    search_fields = ('name',)
    ordering = ('name',)

Задание 3.2: SupplierAdmin

@admin.register(Supplier)
class SupplierAdmin(admin.ModelAdmin):
    list_display = ('name', 'contact_email', 'phone_number')
    search_fields = ('name', 'contact_email', 'phone_number')
    ordering = ('name',)

Задание 3.3: ProductAdmin

Логика: list_editable позволяет массово редактировать цену, количество и доступность прямо в таблице Admin — без открытия каждой записи. Все поля из list_editable обязательно должны быть в list_display.

@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
    list_display = [
        'name',
        'category',
        'supplier',
        'price',
        'quantity',
        'article',
        'available',
    ]
    list_filter = ['category', 'supplier', 'available']
    search_fields = ['name', 'article']
    ordering = ['category', 'quantity']
    list_editable = ['price', 'quantity', 'available']
Обратите внимание: в оригинальном решении из источника в list_display между 'quantity' и 'article' отсутствует запятая — это опечатка. Python склеивает соседние строковые литералы: 'quantity' 'article' превращается в 'quantityarticle'. Django выбросит FieldDoesNotExist. Правильный вариант — запятая после каждого элемента.

Задание 3.4: AddressAdmin

@admin.register(Address)
class AddressAdmin(admin.ModelAdmin):
    list_display = ('country', 'city', 'street', 'house')
    search_fields = ('country', 'city', 'street', 'house')
    ordering = ('country', 'city', 'street')

Задание 3.5: CustomerAdmin

Логика: list_editable = ('deleted',) позволяет «восстанавливать» удалённых клиентов прямо из списка, снимая флаг. list_filter = ('deleted',) — быстрый фильтр «показать только удалённых/активных».

@admin.register(Customer)
class CustomerAdmin(admin.ModelAdmin):
    list_display = (
        'first_name', 'last_name', 'email',
        'phone_number', 'date_joined', 'deleted'
    )
    search_fields = ('first_name', 'last_name', 'email', 'phone_number')
    ordering = ('-date_joined',)
    list_filter = ('deleted',)
    list_editable = ('deleted',)

Задание 3.6: OrderAdmin

Логика: поиск по связанным полям — двойное подчёркивание customer__first_name позволяет Django сделать JOIN и искать в таблице Customer. Так Admin ищет заказы по имени клиента без ручного написания SQL.

@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    list_display = ('id', 'order_date', 'customer')
    search_fields = (
        'customer__first_name',
        'customer__last_name',
        'customer__email',
    )
    ordering = ('-order_date',)