Эта статья не является полноценным руководством, а только содержит отдельные кусочки кода. Тезисно.
Установка Django в виртуальное окружение
Командами ниже мы сначала обновляем менеджер пакетов pip, а потом инсталлируем Джанго
python.exe -m pip install --upgrade pip
pip install django
Установка django-mptt (нужен для того, чтобы можно было создавать древовидные категории), а также Pillow (для работы с изображениями)
pip install django-mptt
pip install Pillow
Создание проекта Django
Существует два распространённых варианта установки. Самый популярный из них - это когда главная папка и папка проекта (папка основных настроек Дажнго) будет иметь одно и тоже название:
django-admin startproject ИМЯ_ПРОЕКТА
Но лично мне удобнее, чтобы папка основных настроек называлась main. В этом случае команда будет выглядеть так (точка после слова main через пробел - обязательна):
django-admin startproject main .
В файле main\settings.py зададим временную зону и русифицируем:
LANGUAGE_CODE = 'ru-RU'
TIME_ZONE = 'Europe/Moscow'
Создадим константу, которая содержит путь к html-шаблонам. Для этого находим в файле константу строку BASE_DIR = Path(__file__).resolve().parent.parent и ниже пишем:
TEMPLATES_DIRS = os.path.join(BASE_DIR, 'templates')
далее укажем константу TEMPLATES_DIRS в 'DIRS':
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [TEMPLATES_DIRS],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
Пропишем статику после строки STATIC_URL = 'static/':
STATICFILES_DIRS = (
os.path.join(BASE_DIR, 'static'),
)
В корне проекта создадим папки static и templates:

Создадим приложение shop
django-admin startapp shop
Проводим первую миграцию
python manage.py makemigrations
python manage.py migrate
Создаём суперпользователя
python manage.py createsuperuser
Стартуем сервер из консоли
python manage.py runserver
А вообще, лучше всего, если в PyCharm создадим конфигурацию запуска на этот случай вот так:

Добавим вывод приложения Shop в админку Django
INSTALLED_APPS = [
...
'mptt',
'shop.apps.ShopConfig',
]
Модель магазина:
import json
import requests
from django.db import models
from django.contrib.auth.models import User
from django.urls import reverse # для древовидной структуры категорий
from mptt.models import MPTTModel, TreeForeignKey # для древовидной структуры категорий
from django.core.validators import MinValueValidator # валидатор значений
# Create your models here.
# ========== Товары ==========
class Product(models.Model):
title = models.CharField(max_length=255, verbose_name='Название товара')
slug = models.SlugField(max_length=150, verbose_name='slug', null=False, unique=True)
description = models.TextField(verbose_name='Описание товара')
category = TreeForeignKey('ProductCategory', on_delete=models.PROTECT, default=1,
related_name='products', verbose_name='Категория товара')
# meta
sku = models.CharField(max_length=255, verbose_name='Артикул')
barcode = models.CharField(verbose_name='Штрихкод', max_length=255, blank=True)
manufacturer = models.CharField(verbose_name='Компания-производитель', max_length=255, blank=True)
manufacturer_countries = models.CharField(max_length=255, verbose_name='Страна-производитель', blank=True)
vendor = models.CharField(max_length=255, verbose_name='Производитель', blank=True)
vendor_code = models.CharField(max_length=255, verbose_name='Артикул производителя', blank=True)
# габариты
length = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='Длина (см.)', null=True, blank=True)
width = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='Ширина (см.)', null=True, blank=True)
height = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='Высота (см.)', null=True, blank=True)
weight = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='Вес (г.)', null=True, blank=True)
# цена и остатки
price = models.DecimalField(max_digits=20, decimal_places=2, verbose_name='Цена', default=1,
validators=[MinValueValidator(0)])
sale_price = models.DecimalField(max_digits=20, decimal_places=2, verbose_name='Цена со скидкой',
null=True, blank=True, validators=[MinValueValidator(0)])
quantity = models.IntegerField(verbose_name='Количество товара', default=0, null=True)
# информация о поставках
supplier = models.ForeignKey('Supplier', verbose_name='Поставщик', # в кавычках имя класса
on_delete=models.CASCADE, null=True, blank=True)
supplier_sku = models.IntegerField(verbose_name='Артикул у поставщика', default=0, null=True)
supplier_url = models.URLField(verbose_name='URL', blank=True)
supplier_price = models.IntegerField(verbose_name='Цена у поставщика', default=0, null=True,
validators=[MinValueValidator(0)])
supplier_quantity = models.IntegerField(verbose_name='Количество товара у поставщика', default=0, null=True)
# данные синхронизации
ozon_id = models.PositiveIntegerField(verbose_name='OZON ID', null=True, blank=True)
wb_id = models.PositiveIntegerField(verbose_name='Wildberries ID', null=True, blank=True)
yandex_id = models.PositiveIntegerField(verbose_name='Яндекс ID', null=True, blank=True)
vk_id = models.PositiveIntegerField(verbose_name='VK ID', null=True, blank=True)
# мета публикации
publish = models.BooleanField(verbose_name='Опубликовано', default=True)
author = models.ForeignKey(User, verbose_name='Автор', on_delete=models.CASCADE, default=1)
created = models.DateTimeField(verbose_name='Дата создания', auto_now_add=True)
updated = models.DateTimeField(verbose_name='Дата обновления', auto_now=True)
class Meta:
ordering = ['pk']
verbose_name = 'Товар'
verbose_name_plural = 'Товары'
def __str__(self):
return f'{self.title} --- {self.price}'
class ProductImages(models.Model):
product = models.ForeignKey(Product, on_delete=models.CASCADE)
image = models.ImageField(upload_to='static/uploads/images/%Y/%m', verbose_name='Изображение')
alt = models.CharField(max_length=255, verbose_name='Атрибут alt', blank=True)
order = models.PositiveIntegerField(verbose_name='Порядок сортировки', null=True, default=0, blank=True)
supplier_url = models.URLField(verbose_name='URL', blank=True)
vk_id = models.PositiveIntegerField(verbose_name='VK ID', null=True, blank=True)
class ProductCategory(MPTTModel):
title = models.CharField(max_length=50, unique=True, verbose_name='Название')
slug = models.SlugField(max_length=150, verbose_name='slug', null=False, unique=True)
parent = TreeForeignKey('self', on_delete=models.PROTECT, null=True, blank=True, related_name='children',
db_index=True, verbose_name='Родительская категория')
supplier = models.ForeignKey('Supplier', verbose_name='Поставщик', # в кавычках имя класса
on_delete=models.CASCADE, null=True, blank=True)
# данные синхронизации
ozon_id = models.PositiveIntegerField(verbose_name='OZON ID', null=True, blank=True)
wb_id = models.PositiveIntegerField(verbose_name='Wildberries ID', null=True, blank=True)
yandex_id = models.PositiveIntegerField(verbose_name='Яндекс ID', null=True, blank=True)
vk_id = models.PositiveIntegerField(verbose_name='VK ID', null=True, blank=True)
class MPTTMeta:
order_insertion_by = ['title']
class Meta:
unique_together = [['parent', 'slug']]
verbose_name = 'Категория'
verbose_name_plural = 'Категории'
def get_absolute_url(self):
return reverse('product-by-category', args=[str(self.slug)])
def __str__(self):
return self.title
# ========== Поставщики ==========
class Supplier(models.Model):
name = models.CharField(max_length=255, verbose_name='Название')
site = models.URLField(verbose_name='Сайт', blank=True)
first_name = models.CharField(max_length=50, verbose_name='Имя', blank=True)
last_name = models.CharField(max_length=50, verbose_name='Фамилия', blank=True)
email = models.EmailField(verbose_name='Email', blank=True)
phone = models.CharField(max_length=50, verbose_name='Телефон', blank=True)
address = models.CharField(max_length=250, verbose_name='Адрес', blank=True)
comment = models.TextField(verbose_name='Примечание', blank=True)
markup = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='Наценка', null=True, blank=True)
class Meta:
verbose_name = 'Поставщик'
verbose_name_plural = 'Поставщики'
def __str__(self):
return f'{self.name}'
# ========== Заказы ==========
class Order(models.Model):
first_name = models.CharField(verbose_name='Имя', max_length=50)
last_name = models.CharField(verbose_name='Фамилия', max_length=50)
email = models.EmailField(verbose_name='Email')
address = models.CharField(verbose_name='Адрес', max_length=250)
postal_code = models.CharField(verbose_name='Индекс', max_length=20)
city = models.CharField(verbose_name='Город', max_length=100)
comment = models.TextField(verbose_name='Комментарий', blank=True, null=True)
created = models.DateTimeField(verbose_name='Дата создания', auto_now_add=True)
updated = models.DateTimeField(verbose_name='Дата обновления', auto_now=True)
paid = models.BooleanField(verbose_name='Оплачено', default=False)
STATUS_CART = '1_cart'
STATUS_WAITING_FOR_PAYMENT = '2_waiting_for_payment'
STATUS_PAID = '3_paid'
STATUS_CHOICES = [
(STATUS_CART, 'cart'),
(STATUS_WAITING_FOR_PAYMENT, 'waiting_for_payment'),
(STATUS_PAID, 'paid')
]
status = models.CharField(max_length=32, choices=STATUS_CHOICES, default=STATUS_CART)
class Meta:
ordering = ('-created',)
verbose_name = 'Заказ'
verbose_name_plural = 'Заказы'
def __str__(self):
return 'Order {}'.format(self.id)
def get_total_cost(self):
return sum(item.get_cost() for item in self.items.all())
class OrderItem(models.Model):
order = models.ForeignKey(Order, verbose_name='Заказ', on_delete=models.CASCADE)
product = models.ForeignKey(Product, verbose_name='Товар', on_delete=models.PROTECT)
quantity = models.PositiveIntegerField(verbose_name='Количество', default=1)
price = models.DecimalField(verbose_name='Цена', max_digits=20, decimal_places=2)
discount = models.DecimalField(verbose_name='Цена со скидкой', max_digits=20, decimal_places=2, default=0)
class Meta:
ordering = ['pk']
def __str__(self):
return f'{self.product} --- {self.price}'
def get_cost(self):
return self.price * self.quantity
admin.py
from django.contrib import admin
from mptt.admin import MPTTModelAdmin # для древовидной структуры категорий
from shop.models import Product, ProductCategory, ProductImages, Supplier, Order, OrderItem
# Register your models here.
# admin.site.register(Product)
class SupplierAdmin(admin.ModelAdmin):
class Meta:
verbose_name = 'Товар'
verbose_name_plural = 'Товары'
admin.site.register(Supplier, SupplierAdmin)
class ProductImagesAdmin(admin.ModelAdmin):
class Meta:
verbose_name = 'Изображение'
verbose_name_plural = 'Изображения'
admin.site.register(ProductImages, ProductImagesAdmin)
class ProductAdminInline(admin.TabularInline):
model = ProductImages
extra = 0
class ProductCategoryAdmin(MPTTModelAdmin):
prepopulated_fields = {'slug': ('title',)}
admin.site.register(ProductCategory, ProductCategoryAdmin)
class ProductAdmin(admin.ModelAdmin):
list_display = ['title', 'price', 'sale_price', 'quantity', 'publish', 'created', 'updated']
list_filter = ['publish', 'created']
list_editable = ['quantity', 'publish']
prepopulated_fields = {'slug': ('title',)}
inlines = [ProductAdminInline]
""" https://russianblogs.com/article/6525692725/
Атрибут prepopulated_fields используется для указания полей, которые используют значения других полей для
автоматической генерации значений. Как мы видели ранее, это простой способ генерировать слагов. Используйте
свойство list_editable в классе ProductAdmin, чтобы задать поля, которые можно изменить на странице отображения
списка на веб-сайте администратора. Таким образом, вы можете редактировать несколько строк одновременно.
Так как редактируется только отображаемый контент, любое поле list_editable должно быть в list_display.
"""
class Meta:
verbose_name = 'Товар'
verbose_name_plural = 'Товары'
admin.site.register(Product, ProductAdmin)
# Register your models here.
class OrderItemInline(admin.TabularInline):
model = OrderItem
raw_id_fields = ['product']
class OrderAdmin(admin.ModelAdmin):
list_display = ['id', 'first_name', 'last_name', 'email', 'address',
'postal_code', 'city', 'paid', 'created', 'updated']
list_filter = ['paid', 'created', 'updated']
inlines = [OrderItemInline]
admin.site.register(Order, OrderAdmin)