Раздел 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',)