id– первичный ключ (PK)username– имя пользователяemail– электронная почта (уникальное поле)password– хешированный парольrole– роль пользователя (например, "admin", "customer")
id– первичный ключ (PK)user– внешний ключ (FK) на User
id– первичный ключ (PK)title– название устройстваdescription– описаниеprice– ценаrating– средний рейтинг устройстваtype– внешний ключ (FK) на Typebrand– внешний ключ (FK) на Brand
id– первичный ключ (PK)basket– внешний ключ (FK) на Basketdevice– внешний ключ (FK) на Device
id– первичный ключ (PK)user– внешний ключ (FK) на Userdevice– внешний ключ (FK) на Devicerate– оценка (1-5)
id– первичный ключ (PK)name– название типа
id– первичный ключ (PK)name– название бренда
techstore_api/
│── manage.py
│── techstore_api/ # Основной конфиг Django
│
├── users/ # Приложение для пользователей
│ ├── models.py # User
│ ├── views.py # Регистрация, авторизация
│ ├── serializers.py # DRF-сериализаторы
│ ├── urls.py # API-маршруты
│
├── devices/ # Приложение для работы с товарами
│ ├── models.py # Device, Brand, Type
│ ├── views.py # CRUD для устройств
│ ├── serializers.py # DRF-сериализаторы
│ ├── urls.py # API-маршруты
│
├── baskets/ # Приложение для корзины
│ ├── models.py # Basket, BasketDevice
│ ├── views.py # Логика добавления/удаления
│ ├── serializers.py # DRF-сериализаторы
│ ├── urls.py # API-маршруты
│
├── ratings/ # Приложение для оценок
│ ├── models.py # Rating
│ ├── views.py # CRUD для оценок
│ ├── serializers.py # DRF-сериализаторы
│ ├── urls.py # API-маршруты- Определяешь классы моделей
- Связываешь через
ForeignKey,OneToOneField, ManyTo`ManyField, если нужно - Обязательно реализуешь
__str__для читабельности
- Регистрируешь модель через
admin.site.register() - При необходимости кастомизируешь отображение (
list_display,search_fields,readonly_fieldsи т.д.)
python manage.py makemigrations <название_приложения>
python manage.py migrate- Описываешь сериализаторы для моделей
- В случае вложенных данных (например, у устройства есть бренд и тип) — используешь вложенные сериализаторы (Nested Serializer)
- Если простая
CRUD-операция— используешьModelViewSetилиGenericAPIView - Если нужно что-то
кастомное— пишешьсобственные методы(post,get,put,delete) - Для
авторизации/регистрации— отдельные классы представлений
- Прописываешь
urlpatterns - Подключаешь через
routerдляViewSet'ов - Или через
path()для функций/классов с базовымAPIView - В основном роутере (
techstore_api/urls.py) подключаешь все мини-роуты приложения
permissions.py: Ограничение доступа к эндпоинтам по ролям (IsAdminUser, кастомные права)pagination.py: Кастомная пагинация для списковfilters.py: Фильтрация по полям (например, устройства по бренду/типу)validators.py: Валидация полей, если нужно что-то специфичноеutils.py: Вспомогательные функции, если проект начнёт разрастаться
python -m venv venv
source venv/bin/activate # Linux/Mac
venv\Scripts\activate # Windowspip install django djangorestframeworkdjango-admin startproject techstore_api
cd techstore_apipython manage.py startapp usersДобавить приложения в INSTALLED_APPS:
INSTALLED_APPS = [
...
'rest_framework',
'users',
]REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
}from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/users/', include('users.urls')), # Подключаем роуты пользователей
]from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
# Расширяем стандартную модель пользователя
ROLE_CHOICES = (
('admin', 'Admin'),
('customer', 'Customer'),
)
role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='customer')
def __str__(self):
return self.username- Наследуемся от
AbstractUser, чтобы использовать уже готовую авторизацию. - Добавляем новое поле
role. __str__определяем для красивого отображения имени в админке.
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from .models import User
@admin.register(User)
class UserAdmin(BaseUserAdmin):
# Расширяем стандартную админку
fieldsets = BaseUserAdmin.fieldsets + (
(None, {'fields': ('role',)}),
)
list_display = ('username', 'email', 'first_name', 'last_name', 'role', 'is_staff')- Расширяем стандартную
админ-панельпользователя. - Добавляем отображение роли (
role) в списке пользователей.
В settings.py укажи свою кастомную модель пользователя:
AUTH_USER_MODEL = 'users.User'Далее команды в терминале:
python manage.py makemigrations users
python manage.py migrate- Без
AUTH_USER_MODELвсё упадёт — Django должен знать о новой модели до первой миграции. - Миграции создадут таблицу пользователей.
from rest_framework import serializers
from .models import User
class UserSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True)
class Meta:
model = User
fields = ['id', 'username', 'email', 'password', 'role']
def create(self, validated_data):
# Хэшируем пароль правильно через create_user
user = User.objects.create_user(
username=validated_data['username'],
email=validated_data['email'],
password=validated_data['password'],
role=validated_data.get('role', 'customer') # Если роль не указана - ставим customer
)
return user- password скрываем через
write_only. - При создании пользователя пароль хэшируется через
create_user().
from rest_framework import generics
from .models import User
from .serializers import UserSerializer
from rest_framework.permissions import AllowAny
class UserRegisterView(generics.CreateAPIView):
"""
Представление для регистрации нового пользователя.
Доступно всем без авторизации.
"""
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = [AllowAny]- Используем
CreateAPIViewдля регистрации новых пользователей. AllowAny— доступ открыт всем (иначе зарегистрироваться будет нельзя без токена).
from django.urls import path
from .views import UserRegisterView
urlpatterns = [
path('register/', UserRegisterView.as_view(), name='user-register'),
]- Создаём роут
/api/users/register/ - Через него будет проходить регистрация новых пользователей.
pip install djangorestframework-simplejwtДобавить настройки для SimpleJWT:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
}- Теперь всё приложение по умолчанию требует авторизацию по JWT токену.
Расширяем urls.py в приложении users:
Копировать
Редактировать
from django.urls import path
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
from .views import UserRegisterView
urlpatterns = [
path('register/', UserRegisterView.as_view(), name='user-register'),
path('login/', TokenObtainPairView.as_view(), name='token-obtain-pair'), # Логин
path('refresh/', TokenRefreshView.as_view(), name='token-refresh'), # Обновление токена
]/api/users/login/— для полученияaccessиrefreshтокенов./api/users/refresh/— для обновленияaccessтокена, если старый истёк.
Можно дополнить стандартный логин-эндпоинт своим выводом. Например, чтобы возвращать ещё и роль пользователя.
# users/serializers.py
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
@classmethod
def get_token(cls, user):
token = super().get_token(user)
# Добавляем кастомные данные в payload токена
token['username'] = user.username
token['role'] = user.role
return token# users/views.py
from rest_framework_simplejwt.views import TokenObtainPairView
from .serializers import CustomTokenObtainPairSerializer
class CustomTokenObtainPairView(TokenObtainPairView):
serializer_class = CustomTokenObtainPairSerializerfrom .views import UserRegisterView, CustomTokenObtainPairView
urlpatterns = [
path('register/', UserRegisterView.as_view(), name='user-register'),
path('login/', CustomTokenObtainPairView.as_view(), name='token-obtain-pair'), # Наш кастомный логин
path('refresh/', TokenRefreshView.as_view(), name='token-refresh'),
]Теперь токен будет содержать ещё username и role — это удобно на фронте для быстрого понимания, кто вошёл.
тело запроса:
{
"username": "newuser",
"email": "user@example.com",
"password": "secret123",
"role": "customer"
}тело запроса:
{
"username": "newuser",
"password": "secret123"
}ответ:
{
"refresh": "long_refresh_token_here",
"access": "short_access_token_here"
}тело запроса:
{
"refresh": "long_refresh_token_here"
}ответ:
{
"access": "new_short_access_token_here"
}# users/serializers.py
from rest_framework import serializers
from .models import User
class UserProfileSerializer(serializers.ModelSerializer):
"""Сериализатор для отображения профиля текущего пользователя."""
class Meta:
model = User
fields = ('id', 'username', 'email', 'role')- Мы не возвращаем пароль и никакие лишние данные.
# users/views.py
from rest_framework import generics
from rest_framework.permissions import IsAuthenticated
from .serializers import UserProfileSerializer
from .models import User
class UserProfileView(generics.RetrieveAPIView):
"""Представление для получения данных текущего пользователя."""
serializer_class = UserProfileSerializer
permission_classes = [IsAuthenticated]
def get_object(self):
# Возвращаем текущего пользователя
return self.request.userRetrieveAPIView— стандартная вьюшка для получения одного объекта.get_object()возвращает пользователя, который сделал запрос по токену.- Никаких запросов типа
User.objects.get(pk=self.request.user.id)— данные берутся напрямую изrequest.user, которыйDRFавтоматически подтягивает при проверке токена.
from .views import UserRegisterView, CustomTokenObtainPairView, UserProfileView
urlpatterns = [
path('register/', UserRegisterView.as_view(), name='user-register'),
path('login/', CustomTokenObtainPairView.as_view(), name='token-obtain-pair'),
path('refresh/', TokenRefreshView.as_view(), name='token-refresh'),
path('me/', UserProfileView.as_view(), name='user-profile'),
]-
Заголовок:
Authorization: Bearer <access_token> -
Ответ пример:
{
"id": 1,
"username": "newuser",
"email": "user@example.com",
"role": "customer"
}- Без токена вернёт 401 (Unauthorized).
- Никаких ID в URL — всё автоматически определяется на основе авторизации!
from django.db import models
class Type(models.Model):
"""
Модель категории устройства, например 'Смартфон', 'Ноутбук' и т.д.
"""
name = models.CharField(max_length=255, unique=True)
def __str__(self):
return self.name
# Для удобного отображения в админке и в консоли будет выводиться название типа
class Brand(models.Model):
"""
Модель бренда устройства, например 'Apple', 'Samsung' и т.д.
"""
name = models.CharField(max_length=255, unique=True)
def __str__(self):
return self.name
# Аналогично — для красивого вывода имени бренда
class Device(models.Model):
"""
Модель устройства, которое продается в магазине.
"""
title = models.CharField(max_length=255)
description = models.TextField(blank=True) # Описание можно оставить пустым
price = models.DecimalField(max_digits=10, decimal_places=2)
rating = models.FloatField(default=0) # Средний рейтинг устройства (по умолчанию 0)
type = models.ForeignKey(Type, on_delete=models.CASCADE, related_name='devices')
# Внешний ключ на таблицу Type (категория устройства)
# Если удалится категория — все устройства этой категории тоже удалятся
brand = models.ForeignKey(Brand, on_delete=models.CASCADE, related_name='devices')
# Внешний ключ на таблицу Brand (бренд устройства)
# При удалении бренда также удаляются устройства этого бренда
def __str__(self):
return self.title
# При выводе устройства будет показываться его заголовокname в Type и Brand: unique=True, чтобы не было двух типов или брендов с одинаковыми названиями.price: Используем DecimalField, а не FloatField, потому что цена требует высокой точности (особенно для финансов).rating: Плавающее число от 0 до 5 (будем обновлять при новых оценках).ForeignKey: Настроен с on_delete=models.CASCADE, чтобы связанные устройства удалялись вместе с категорией или брендом.related_name='devices': Это позволяет потом удобно обращаться к устройствам типа или бренда (some_type.devices.all()).
# devices/admin.py
from django.contrib import admin
from .models import Type, Brand, Device
@admin.register(Type)
class TypeAdmin(admin.ModelAdmin):
list_display = ('id', 'name') # Какие поля будут отображаться в списке в админке
search_fields = ('name',) # По каким полям можно искать
ordering = ('id',) # Сортировка по id
@admin.register(Brand)
class BrandAdmin(admin.ModelAdmin):
list_display = ('id', 'name')
search_fields = ('name',)
ordering = ('id',)
@admin.register(Device)
class DeviceAdmin(admin.ModelAdmin):
list_display = ('id', 'title', 'price', 'rating', 'type', 'brand')
search_fields = ('title', 'description')
list_filter = ('type', 'brand') # Фильтрация по типу и бренду
ordering = ('id',)@admin.register(...): Удобный современный способ регистрации моделей вместо admin.site.register().list_display: Управляем, какие поля будут показываться в списке объектов в админке.search_fields: Позволяет искать объекты по указанным полям через панель поиска.list_filter: Фильтрация справа в админке по выбранным полям (например, быстро выбрать устройства определенного бренда).ordering: Стандартная сортировка записей в списке.
INSTALLED_APPS = [
...
'rest_framework',
'users',
'devices'
]python manage.py makemigrations devices- Команда создаст файл миграции для приложения
devices/. - Он будет содержать инструкции, как создать таблицы для
Type,Brand,Device.
python manage.py migrate- Теперь таблицы реально создадутся в базе данных.
# devices/serializers.py
from rest_framework import serializers
from .models import Type, Brand, Device
# Сериализатор для модели Type
class TypeSerializer(serializers.ModelSerializer):
class Meta:
model = Type
fields = ['id', 'name'] # Явно указываем, какие поля хотим сериализовать
# Сериализатор для модели Brand
class BrandSerializer(serializers.ModelSerializer):
class Meta:
model = Brand
fields = ['id', 'name']
# Сериализатор для модели Device
class DeviceSerializer(serializers.ModelSerializer):
# Чтобы вместо id-шников выводить нормальные данные о типе и бренде
type = TypeSerializer(read_only=True) # Вложенный сериализатор
brand = BrandSerializer(read_only=True)
# Чтобы при создании передавать именно id-шники (а не всё тело вложенных объектов)
type_id = serializers.PrimaryKeyRelatedField(
queryset=Type.objects.all(),
source='type',
write_only=True
)
brand_id = serializers.PrimaryKeyRelatedField(
queryset=Brand.objects.all(),
source='brand',
write_only=True
)
class Meta:
model = Device
fields = [
'id',
'title',
'description',
'price',
'rating',
'type',
'brand',
'type_id',
'brand_id'
]- Созданы сериализаторы
TypeSerializerиBrandSerializer: Чтобы управлять сериализацией моделей Type и Brand отдельно. - В
DeviceSerializerдобавлены вложенные сериализаторы (TypeSerializerиBrandSerializer): Чтобы в ответе API видеть всю информацию о типе и бренде, а не только их id. - Добавлены поля
type_idиbrand_id: Чтобы при создании/обновлении устройства в запросе можно было отправлять только id типа и бренда, а не весь объект. source='type'иsource='brand': Указываем, что эти поля маппятся на реальные связи модели Device.
{
"id": 5,
"title": "iPhone 15",
"description": "Новейший iPhone 15 от Apple",
"price": 999.99,
"rating": 4.8,
"type": {
"id": 1,
"name": "Смартфоны"
},
"brand": {
"id": 2,
"name": "Apple"
}
}{
"title": "Galaxy S24",
"description": "Флагман Samsung",
"price": 899.99,
"rating": 4.7,
"type_id": 1,
"brand_id": 3
}# devices/views.py
from rest_framework import generics
from .models import Type, Brand, Device
from .serializers import TypeSerializer, BrandSerializer, DeviceSerializer
# === Views для Type ===
# Получение списка всех типов и создание нового
class TypeListCreateAPIView(generics.ListCreateAPIView):
queryset = Type.objects.all()
serializer_class = TypeSerializer
# Получение, обновление и удаление одного типа по id
class TypeRetrieveUpdateDestroyAPIView(generics.RetrieveUpdateDestroyAPIView):
queryset = Type.objects.all()
serializer_class = TypeSerializer
# === Views для Brand ===
# Получение списка всех брендов и создание нового
class BrandListCreateAPIView(generics.ListCreateAPIView):
queryset = Brand.objects.all()
serializer_class = BrandSerializer
# Получение, обновление и удаление одного бренда по id
class BrandRetrieveUpdateDestroyAPIView(generics.RetrieveUpdateDestroyAPIView):
queryset = Brand.objects.all()
serializer_class = BrandSerializer
# === Views для Device ===
# Получение списка всех устройств и создание нового устройства
class DeviceListCreateAPIView(generics.ListCreateAPIView):
queryset = Device.objects.all()
serializer_class = DeviceSerializer
# Получение, обновление и удаление одного устройства по id
class DeviceRetrieveUpdateDestroyAPIView(generics.RetrieveUpdateDestroyAPIView):
queryset = Device.objects.all()
serializer_class = DeviceSerializerListCreateAPIView: Позволяет получить список всех объектов и создать новый. Автоматически обрабатывает методы GET и POST.RetrieveUpdateDestroyAPIView: Позволяет получить объект по id, изменить его (PUT/PATCH) или удалить (DELETE).
# devices/urls.py
from django.urls import path
from .views import (
TypeListCreateAPIView, TypeRetrieveUpdateDestroyAPIView,
BrandListCreateAPIView, BrandRetrieveUpdateDestroyAPIView,
DeviceListCreateAPIView, DeviceRetrieveUpdateDestroyAPIView
)
urlpatterns = [
# === Типы устройств (Type) ===
path('types/', TypeListCreateAPIView.as_view(), name='type-list-create'), # GET, POST
path('types/<int:pk>/', TypeRetrieveUpdateDestroyAPIView.as_view(), name='type-detail'), # GET, PUT/PATCH, DELETE
# === Бренды устройств (Brand) ===
path('brands/', BrandListCreateAPIView.as_view(), name='brand-list-create'), # GET, POST
path('brands/<int:pk>/', BrandRetrieveUpdateDestroyAPIView.as_view(), name='brand-detail'), # GET, PUT/PATCH, DELETE
# === Устройства (Device) ===
path('devices/', DeviceListCreateAPIView.as_view(), name='device-list-create'), # GET, POST
path('devices/<int:pk>/', DeviceRetrieveUpdateDestroyAPIView.as_view(), name='device-detail'), # GET, PUT/PATCH, DELETE
]# techstore_api/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/users/', include('users.urls')), # Пользователи
path('api/', include('devices.urls')), # Устройства, бренды, типы
]/api/types/ GET Получить все типы
/api/types/ POST Создать новый тип
/api/types/<id>/ GET Получить тип по id
/api/types/<id>/ PUT/PATCH Обновить тип
/api/types/<id>/ DELETE Удалить тип
/api/brands/ GET/POST То же самое для брендов
/api/devices/ GET/POST То же самое для устройств
from django.db import models
from django.conf import settings
from devices.models import Device # Импортируем модель устройств
# === Модель Корзины пользователя ===
class Basket(models.Model):
user = models.OneToOneField(
settings.AUTH_USER_MODEL, # Ссылка на модель пользователя
on_delete=models.CASCADE, # При удалении пользователя - удаляется корзина
related_name='basket' # Позволяет из пользователя получить его корзину: user.basket
)
def __str__(self):
return f'Корзина пользователя {self.user.username}'
# === Промежуточная модель "Корзина - Устройства" ===
class BasketDevice(models.Model):
basket = models.ForeignKey(
Basket,
on_delete=models.CASCADE,
related_name='basket_devices' # Доступ к устройствам корзины: basket.basket_devices.all()
)
device = models.ForeignKey(
Device,
on_delete=models.CASCADE,
related_name='basket_devices' # Устройства могут быть в разных корзинах
)
quantity = models.PositiveIntegerField(default=1) # Количество одного товара в корзине
def __str__(self):
return f'{self.device.title} (x{self.quantity}) в корзине {self.basket.user.username}'Basket:- Привязывается Один к одному (
OneToOne) к пользователю. - Логика: у каждого пользователя своя личная корзина.
- Если пользователь удаляется — его корзина удаляется тоже (
CASCADE).
- Привязывается Один к одному (
BasketDevice:- Связывает корзину и устройство (многие ко многим через промежуточную модель).
- Можно хранить количество товара (
quantity), чтобы, например, заказать сразу 3 телефона.
- Мы сразу добавили нормальные
__str__методы, чтобы было красиво видно записи в админке.
user = models.OneToOneField(User, on_delete=models.CASCADE)- Что это значит:
- Один пользователь может иметь только одну корзину. (🧺
1 Basket = 1 User) - И наоборот — каждая корзина принадлежит только одному пользователю.
- Один пользователь может иметь только одну корзину. (🧺
- Отличие от
ForeignKey:ForeignKeyсоздает связь"много к одному"(один пользователь может иметь много корзин).OneToOneFieldсоздает"один к одному"(одна корзина на одного пользователя и всё).
- Почему так сделано для корзины:
- Логика интернет-магазина: у пользователя должна быть одна активная корзина, куда он складывает товары.
- Нет смысла позволять одному юзеру иметь несколько разных корзин одновременно (если бы были "отложенные корзины" или "список желаний" — тогда можно было бы сделать ForeignKey).
User 1 — 1 Basket
Basket 1 — N BasketDevice
BasketDevice 1 — 1 DeviceINSTALLED_APPS = [
...
'baskets',
]python manage.py makemigrations baskets
python manage.py migrate# baskets/admin.py
from django.contrib import admin
from .models import Basket, BasketDevice
# === Настройка отображения устройств в корзине ===
class BasketDeviceInline(admin.TabularInline):
model = BasketDevice
extra = 1 # Количество пустых форм для добавления новых товаров
readonly_fields = ('device', 'quantity') # Только для просмотра в корзине, без изменения
# === Админка для корзины пользователя ===
@admin.register(Basket)
class BasketAdmin(admin.ModelAdmin):
list_display = ('id', 'user') # Какие поля показывать в списке
search_fields = ('user__username', 'user__email') # Поиск по имени пользователя и почте
inlines = [BasketDeviceInline] # Показываем устройства в корзине прямо внутри корзины
# === Админка для связи корзина-устройства ===
@admin.register(BasketDevice)
class BasketDeviceAdmin(admin.ModelAdmin):
list_display = ('id', 'basket', 'device', 'quantity') # Какие поля отображать
list_filter = ('basket', 'device') # Возможность фильтрации
search_fields = ('basket__user__username', 'device__title') # Поиск по пользователю и устройствуBasketDeviceInline:- Это
"встроенная таблица"товаров внутри корзины. - Когда заходишь в корзину пользователя — сразу видишь, что у него лежит.
- Это
BasketAdmin:- В списке корзин показываем
IDипользователя. - Можно искать корзины по
имениилипочтепользователя. - Устройства в корзине показываются через
inlines.
- В списке корзин показываем
BasketDeviceAdmin:- Если нужно отдельно работать со связями корзина-товар — доступна отдельная вкладка.
- Можно искать по названию устройства или по пользователю корзины.
# baskets/serializers.py
from rest_framework import serializers
from .models import Basket, BasketDevice
from devices.models import Device # Чтобы подтянуть данные о товарах
# === Сериализатор для устройств в корзине ===
class BasketDeviceSerializer(serializers.ModelSerializer):
device_title = serializers.CharField(source='device.title', read_only=True)
device_price = serializers.DecimalField(source='device.price', max_digits=10, decimal_places=2, read_only=True)
class Meta:
model = BasketDevice
fields = ['id', 'device', 'device_title', 'device_price', 'quantity']
read_only_fields = ['id', 'device_title', 'device_price']
# === Сериализатор для полной информации о корзине ===
class BasketSerializer(serializers.ModelSerializer):
devices = BasketDeviceSerializer(source='basket_devices', many=True, read_only=True)
class Meta:
model = Basket
fields = ['id', 'user', 'devices']
read_only_fields = ['id', 'user', 'devices']
# === Сериализатор для добавления устройства в корзину ===
class AddDeviceToBasketSerializer(serializers.Serializer):
device_id = serializers.IntegerField()
quantity = serializers.IntegerField(min_value=1)
def validate_device_id(self, value):
# Проверяем, что устройство существует
from devices.models import Device
if not Device.objects.filter(id=value).exists():
raise serializers.ValidationError('Device with given ID does not exist.')
return valueBasketDeviceSerializer:- Отвечает за отображение информации об одном устройстве в корзине.
- Автоматически показывает название и цену устройства (
read_only).
BasketSerializer:- Отображает корзину с пользователем и всеми его устройствами.
- Подключает к корзине все связанные объекты
BasketDevice.
AddDeviceToBasketSerializer:- Специальный сериализатор для добавления товара в корзину через
API. - Позволяет передать
device_idиquantity. - При валидации проверяет, существует ли устройство с таким
ID.
- Специальный сериализатор для добавления товара в корзину через
class BasketDeviceSerializer(serializers.ModelSerializer):
device_title = serializers.CharField(source='device.title', read_only=True)
device_price = serializers.DecimalField(source='device.price', max_digits=10, decimal_places=2, read_only=True)- Что делает:
device_title: показывает название устройства прямо в ответе (чтобы не приходилось руками доставать device.id и еще один запрос делать).device_price: показывает цену устройства.
- Параметры:
source='device.title'— говорит сериализатору: "бери полеtitleу связанного объектаdevice".read_only=True— поле доступно только на чтение, пользователь его не может изменить через API.
- fields:
id— id записи в таблицеBasketDevicedevice— id устройстваdevice_title— название устройстваdevice_price— цена устройстваquantity— количество единиц устройства в корзине
class BasketSerializer(serializers.ModelSerializer):
devices = BasketDeviceSerializer(source='basket_devices', many=True, read_only=True)- Что делает:
- В корзине показываются все устройства (BasketDevice), которые в ней есть.
- Параметры:
source='basket_devices'— переопределил стандартное имяbasketdevice_set. Когда уForeignKeyуказан related_name, то Django больше НЕ создаётbasketdevice_set. Он создает то, что ты явно написал.many=True— потому что у одной корзины может быть много товаров.read_only=True— пользователь не может менять список товаров через этот сериализатор (только через отдельные запросы на добавление/удаление).
- fields:
id— id корзиныuser— пользователь (id)devices— список устройств в корзине (используя BasketDeviceSerializer)
class AddDeviceToBasketSerializer(serializers.Serializer):
device_id = serializers.IntegerField()
quantity = serializers.IntegerField(min_value=1)- Что делает:
- **Принимает на вход **
device_idиquantity. - Проверяет, существует ли устройство.
- **Принимает на вход **
- Параметры:
device_id— ID устройства, которое хотим добавить в корзину.quantity— сколько штук хотим положить.validate_device_id— кастомная проверка: если такого устройства нет, ошибка.
6. Опишем views для работы с корзиной: просмотр содержимого, добавление устройства и удаление устройства.
# baskets/views.py
from rest_framework import generics, status
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from .models import Basket, BasketDevice
from devices.models import Device
from .serializers import BasketSerializer, BasketDeviceSerializer, AddDeviceToBasketSerializer
# === View для получения корзины пользователя ===
class BasketDetailView(generics.RetrieveAPIView):
permission_classes = [IsAuthenticated]
serializer_class = BasketSerializer
def get_object(self):
# Ищем корзину текущего пользователя, если нет - создаём
basket, created = Basket.objects.get_or_create(user=self.request.user)
return basket
# === View для добавления устройства в корзину ===
class AddDeviceToBasketView(generics.GenericAPIView):
permission_classes = [IsAuthenticated]
serializer_class = AddDeviceToBasketSerializer
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
device_id = serializer.validated_data['device_id']
quantity = serializer.validated_data['quantity']
basket, created = Basket.objects.get_or_create(user=request.user)
device = Device.objects.get(id=device_id)
# Проверяем, есть ли уже такое устройство в корзине
basket_device, created = BasketDevice.objects.get_or_create(basket=basket, device=device)
if not created:
# Если уже есть, увеличиваем количество
basket_device.quantity += quantity
basket_device.save()
else:
# Иначе сохраняем с переданным количеством
basket_device.quantity = quantity
basket_device.save()
return Response({'message': 'Device added to basket successfully.'}, status=status.HTTP_200_OK)
# === View для удаления устройства из корзины ===
class RemoveDeviceFromBasketView(generics.DestroyAPIView):
permission_classes = [IsAuthenticated]
lookup_url_kwarg = 'basket_device_id' # Параметр в URL
def delete(self, request, *args, **kwargs):
basket_device_id = kwargs.get(self.lookup_url_kwarg)
basket = Basket.objects.filter(user=request.user).first()
if not basket:
return Response({'error': 'Basket not found.'}, status=status.HTTP_404_NOT_FOUND)
basket_device = BasketDevice.objects.filter(id=basket_device_id, basket=basket).first()
if not basket_device:
return Response({'error': 'Device not found in your basket.'}, status=status.HTTP_404_NOT_FOUND)
basket_device.delete()
return Response({'message': 'Device removed from basket successfully.'}, status=status.HTTP_200_OK)BasketDetailView:- Возвращает полную корзину пользователя (список всех товаров).
- Если корзины нет — создаёт новую пустую корзину автоматически.
AddDeviceToBasketView:- Принимает
device_idиquantity. - Если устройство уже есть в корзине — увеличивает количество.
- Если нет — добавляет новое устройство в корзину.
- Принимает
RemoveDeviceFromBasketView:- Удаляет конкретное устройство из корзины по его
basket_device_id. - Защита: удалять можно только свои устройства.
- Удаляет конкретное устройство из корзины по его
basket, created = Basket.objects.get_or_create(user=self.request.user)- Что делает:
- Ищет корзину пользователя по его
user_id. - Если корзины нет, автоматически создает новую пустую корзину для этого пользователя.
- Это гарантирует, что каждый юзер всегда имеет актуальную корзину при первом запросе.
- Ищет корзину пользователя по его
- Возвращает:
- Корзину, чтобы отдать её сериализованную через
BasketSerializer.
- Корзину, чтобы отдать её сериализованную через
-
Валидация запроса:
serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True)
- Проверяем, что переданы корректные
device_idиquantity.
- Проверяем, что переданы корректные
-
Извлечение данных:
device_id = serializer.validated_data['device_id'] quantity = serializer.validated_data['quantity']
-
Получение или создание корзины:
basket, created = Basket.objects.get_or_create(user=request.user)
-
Получение устройства по
id:device = Device.objects.get(id=device_id)
-
Добавление или обновление товара в корзине:
basket_device, created = BasketDevice.objects.get_or_create(basket=basket, device=device) if not created: basket_device.quantity += quantity basket_device.save() else: basket_device.quantity = quantity basket_device.save()
- Если устройство уже в корзине — увеличиваем количество.
- Если нет — добавляем новое.
-
Ответ:
return Response({'message': 'Device added to basket successfully.'}, status=status.HTTP_200_OK)
lookup_url_kwarg = 'basket_device_id'-
Что это такое:
- Указывает, что в URL запроса будет переменная
basket_device_id. - Через неё мы будем искать какой конкретно товар удалять из корзины.
- Указывает, что в URL запроса будет переменная
-
Метод
delete:-
Получаем id товара из URL:
basket_device_id = kwargs.get(self.lookup_url_kwarg)
-
Находим корзину пользователя:
basket = Basket.objects.filter(user=request.user).first()
-
Находим нужный товар в корзине:
basket_device = BasketDevice.objects.filter(id=basket_device_id, basket=basket).first()
-
Если товара нет —
ошибка 404. -
Если товар найден — удаляем его:
basket_device.delete()
-
Отправляем сообщение об успешном удалении.
-
# baskets/urls.py
from django.urls import path
from .views import BasketDetailView, AddDeviceToBasketView, RemoveDeviceFromBasketView
urlpatterns = [
# Получить свою корзину
path('basket/', BasketDetailView.as_view(), name='basket-detail'),
# Добавить устройство в корзину
path('basket/add/', AddDeviceToBasketView.as_view(), name='basket-add-device'),
# Удалить устройство из корзины (по id записи BasketDevice)
path('basket/remove/<int:basket_device_id>/', RemoveDeviceFromBasketView.as_view(), name='basket-remove-device'),
]/api/basket/ GET Получить содержимое своей корзины
/api/basket/add/ POST Добавить устройство в корзину
/api/basket/remove/<id>/ DELETE Удалить устройство из корзины# techstore_api/urls.py
from django.urls import path, include
urlpatterns = [
# другие приложения...
path('api/', include('baskets.urls')),
]-
Запрос:
- Метод: POST
- URL: http://localhost:8000/api/basket/add/
-
Headers:
- Authorization: Bearer <твой токен>
- Content-Type: application/json
-
Body (JSON):
{ "device_id": 2 }- Здесь device_id — это ID устройства (например, iPhone 15 имеет id = 2)
-
Ответ (успех):
{ "message": "Device added to basket successfully." }
-
Запрос:
- Метод: GET
- URL: http://localhost:8000/api/basket/
-
Headers:
- Authorization: Bearer <твой токен>
-
Ответ (пример):
{ "id": 1, "user": 2, "devices": [ { "id": 1, "title": "Galaxy S24", "description": "Флагман Samsung", "price": "899.99", "rating": 0.0, "type": 1, "brand": 2 } ] }
-
Запрос:
- Метод: POST
- URL: http://localhost:8000/api/basket/remove/
-
Headers:
- Authorization: Bearer <твой токен>
- Content-Type: application/json
-
Body (JSON):
{ "device_id": 2 } -
Ответ (успех):
{ "message": "Device removed from basket successfully." }