diff --git a/.github/workflows/test_and_coverage.yml b/.github/workflows/test_and_coverage.yml
new file mode 100644
index 000000000..0a546af0a
--- /dev/null
+++ b/.github/workflows/test_and_coverage.yml
@@ -0,0 +1,75 @@
+name: 'coverage'
+on:
+ push:
+ branches:
+ - master
+ - main
+ pull_request:
+ branches:
+ - master
+ - main
+jobs:
+ coverage:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Get Cover
+ uses: orgoro/coverage@v3.2
+ with:
+ coverageFile: python-final-diplom/reference/netology_pd_diplom/.coverage.xml
+ token: ${{ secrets.GITHUB_TOKEN }}
+
+# name: 'Тестирование и покрытие кода'
+
+# on:
+# push:
+# branches:
+# - master
+# - main
+# pull_request:
+
+# jobs:
+# build:
+# runs-on: ubuntu-latest
+
+# steps:
+# - name: Checkout repository
+# uses: actions/checkout@v3
+
+# - name: Setup Python 3.10
+# uses: actions/setup-python@v3
+# with:
+# python-version: '3.10'
+
+# - name: Install dependencies
+# run: |
+# python -m pip install --upgrade pip
+# pip install -r requirements.txt
+# pip install pytest pytest-cov coverage
+
+# - name: Show project directory structure
+# run: |
+# echo "Current directory: $(pwd)"
+# ls -la
+# ls -la reference
+
+# - name: Run tests with coverage
+# run: |
+# export DJANGO_SETTINGS_MODULE=reference.netology_pd_diplom.netology_pd_diplom.settings
+# cd reference
+# pytest --cov=netology_pd_diplom --cov-report=xml:coverage.xml tests/
+# env:
+# PYTHONPATH: ${{ github.workspace }}/reference
+# DJANGO_SETTINGS_MODULE: reference.netology_pd_diplom.netology_pd_diplom.settings
+
+# - name: Upload coverage to Codecov
+# uses: codecov/codecov-action@v3
+# with:
+# token: ${{ secrets.CODECOV_TOKEN }}
+# files: coverage.xml
+# flags: unittests
+# fail_ci_if_error: true
+
+
+
+
+
diff --git a/.gitignore b/.gitignore
index e1abce054..89ff7cd0f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -117,3 +117,6 @@ venv.bak/
# mypy
.mypy_cache/
+
+/Users/apple/Documents/Full-stack-Python-developer/my_diplom/redis-7.2.7
+reference/netology_pd_diplom/dump.rdb
diff --git a/README.md b/README.md
index 6f5ef53e8..b81d1cfdb 100644
--- a/README.md
+++ b/README.md
@@ -230,3 +230,46 @@ _Важно: не нарушайте дедлайн сдачи, возника
* выполненная базовая часть проекта,
* наличие собственных комментариев к коду,
* использование сторонних библиотек и фреймворков.
+
+## Дополнительное задание
+> Отказ от django signals, с дальнейшей интеграцией в проект фреймворка Celery для по-настоящему асинхронных задач
+Рекомендую начать с использования любой из нижеследующих инструкций.
+- https://realpython.com/asynchronous-tasks-with-django-and-celery/
+
+или
+- https://stackabuse.com/asynchronous-tasks-in-django-with-redis-and-celery/
+
+> Вижу вы покрыли код тестами, предлагаю добавить красивую кнопку с текущим покрытием у себя в Гитхабе
+https://github.com/marketplace/actions/python-coverage
+
+> Опробовать автогенерацию документации Open API в рамках пакета DRF-Spectacular. После внедрения обязательно внимательно просмотрите страницу Swagger. Тут сразу можно будет оценить для себя разницу с Postman и огромный потенциал для плодотворной работы в команде с несколькими разработчиками. P.S. Используйте чаще docstring к классам и функциям
+https://drf-spectacular.readthedocs.io/en/latest/readme.html
+
+> Попробовать простой DRF тротлинг, и можно проверить его работу добавление отдельного TestCase
+https://www.django-rest-framework.org/api-guide/throttling/
+
+> Добавить возможность авторизации с 1-2 социальных сетей, тут можно ознакомится с такой крутой библиотекой
+https://github.com/python-social-auth/social-app-django
+
+> Наверняка пользовались админкой в ходе создания проекта, предлагаю сделать ей мощный тюнинг через эти библиотеки на выбор:
+https://github.com/otto-torino/django-baton
+https://django-jet-reboot.readthedocs.io/en/latest/
+
+> После добавления Celery можно добавить загрузку аватаров пользователей и картинок товаров с последующей асинхронной обработкой их в фоновом режиме, например создание миниатюр различного размера для быстрой загрузки. Для этого есть множество библиотек на выбор:
+https://easy-thumbnails.readthedocs.io/en/latest/
+https://django-versatileimagefield.readthedocs.io/en/latest/
+https://django-imagekit.readthedocs.io/en/latest/
+
+> Очень часто в коммерческих проектах для перехвата ошибок, которые возникают у пользователей внедряют Sentry или Rollbar. Он позволяет быть в курсе насколько стабильно работает проект и к тому же хранит полные traceback ошибок, так чтобы можно было их исследовать. Будет большой плюс в резюме от наличия знаний по этой технологии. Попробуйте внедрить ее на базовом уровне и создать APIView, который вызывает исключение, что увидеть его в консоли Sentry или RollBar
+https://blog.sentry.io/monitoring-performance-and-errors-in-a-django-application-with-sentry/
+https://docs.sentry.io/platforms/python/integrations/django/
+RollBar
+https://docs.rollbar.com/docs/django
+https://docs.rollbar.com/docs/celery
+
+> внедрить кэширование запросов к БД с использованием Redis, посмотреть насколько уменьшится время отклика системы. Вот эти библиотеки на выбор могут помочь с кэшированием запросов к БД:
+https://github.com/noripyt/django-cachalot
+https://github.com/Suor/django-cacheops
+
+> и последнее только по желанию, часто в проектах бывают задачи поиска причин медленной работы запроса. Например, из-за неправильного ORM запроса к БД. Рекомендую ознакомиться с пакетом https://github.com/jazzband/django-silk
+проанализировать запросы в Postman и посмотреть какие из них вызывают каскад вторичных запросов, которые перегружают СУБД. Лично у меня вызывает удовлетворение, если ранее запрос выполнялся 400-500 мс, а сейчас за 3-4 мс, после того как я выявил проблему через Silk и оптимизировал запрос. Возможно, и Вам понравится)
diff --git a/reference/netology_pd_diplom/.coveragrc b/reference/netology_pd_diplom/.coveragrc
new file mode 100644
index 000000000..a197a65da
--- /dev/null
+++ b/reference/netology_pd_diplom/.coveragrc
@@ -0,0 +1,8 @@
+[run]
+branch = True # Включаем измерение ветвления (условия if/else)
+omit = tests/* # Исключаем папку с тестами из анализа покрытия
+
+[report]
+show_missing = True # Показывать непокрытые участки кода
+skip_covered = False # Отображать покрытый код
+precision = 2 # Точность отчета — два знака после запятой
\ No newline at end of file
diff --git a/reference/netology_pd_diplom/.dockerignore b/reference/netology_pd_diplom/.dockerignore
new file mode 100644
index 000000000..2ca324a44
--- /dev/null
+++ b/reference/netology_pd_diplom/.dockerignore
@@ -0,0 +1,2 @@
+.env
+enviroments.sh
\ No newline at end of file
diff --git a/reference/netology_pd_diplom/.gitignore b/reference/netology_pd_diplom/.gitignore
index ed02b07e1..a89e175cf 100644
--- a/reference/netology_pd_diplom/.gitignore
+++ b/reference/netology_pd_diplom/.gitignore
@@ -104,3 +104,5 @@ venv.bak/
# mypy
.mypy_cache/
+
+enviroments.sh
\ No newline at end of file
diff --git a/reference/netology_pd_diplom/Dockerfile b/reference/netology_pd_diplom/Dockerfile
new file mode 100644
index 000000000..ddd495b8a
--- /dev/null
+++ b/reference/netology_pd_diplom/Dockerfile
@@ -0,0 +1,23 @@
+# Устанавливаем базовый образ Python и Alpine Linux
+FROM python:3.10-alpine
+
+# Установка компилятора gcc для Alpine Linux
+RUN apk add --no-cache gcc musl-dev
+
+# Устанавливаем рабочую директорию в контейнере
+WORKDIR /app
+
+# Копируем файл requirements.txt локального проекта в WORKDIR контейнера
+COPY ./requirements.txt ./requirements.txt
+
+# Устанавливаем зависимости
+RUN pip install -r requirements.txt
+
+# Указываем порт, который будет слушать сервер
+EXPOSE 8000
+
+# Копируем файлы локального проекта в WORKDIR контейнера
+COPY . .
+
+# Команда на запуск сервера
+CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
diff --git a/reference/netology_pd_diplom/README.md b/reference/netology_pd_diplom/README.md
index 44f17da71..fdad3bdaa 100644
--- a/reference/netology_pd_diplom/README.md
+++ b/reference/netology_pd_diplom/README.md
@@ -1,9 +1,151 @@
# Пример API-сервиса для магазина
-[Документация по запросам в PostMan](https://documenter.getpostman.com/view/5037826/SVfJUrSc)
+[Исходная документация по запросам в PostMan](https://documenter.getpostman.com/view/5037826/SVfJUrSc)
+## Разворачиваем проект
+более подробно:
+https://realpython.com/asynchronous-tasks-with-django-and-celery/
+https://stackabuse.com/asynchronous-tasks-in-django-with-redis-and-celery/
+### Установка Redis на Windows
+
+- [Загрузите zip-файл Redis](https://github.com/microsoftarchive/redis/releases/tag/win-3.2.100) и распакуйте его в какую-нибудь директорию.
+- Найдите файл с именем `redis-server.exe` и дважды щелкните по нему, чтобы запустить сервер в командном окне.
+- Аналогично найдите другой файл с именем `redis-cli.exe` и дважды щелкните по нему, чтобы открыть программу в отдельном командном окне.
+- В командном окне, запустив клиент CLI, проверьте, может ли клиент взаимодействовать с сервером, выполнив команду, и если все пройдет хорошо , должен быть возвращен pingответ `PONG`
+
+### Установка Redis на Mac OSX / Linux
+
+- [Загрузите файл tarball](https://redis.io/download) Redis и распакуйте его в какой-нибудь каталог.
+- Запустите `makefile`, чтобы собрать программу
+```bash
+make install
+```
+- Откройте окно терминала и выполните
+```bash
+redis-server
+```
+- В другом окне терминала запустите клиент CLI
+```bash
+redis-cli
+```
+проверьте, может ли клиент взаимодействовать с сервером, выполнив команду `ping`, и если все пройдет хорошо , должен быть возвращен ответ `PONG`
+```bash
+127.0.0.1:6379> ping
+```
+
+### Установка Redis с помощью docer-compose
+Создайте `.yml` файл следующего содержания
+```
+version: '3.1'
+
+services:
+
+ redis:
+ image: redis
+ ports:
+ - "6379:6379"
+```
+
+Запустить сборку
+```
+docker-compose up -d
+```
+
+1. Установка виртуального окружения Python и зависимостей requirements.txt, в том числе и `Celery`
+
+2. Откройте три отдельных окна терминала перейдите в папку в которой находится `manage.py` и запустите почередно:
+```bash
+(venv) $ python manage.py runserver
+$ redis-server # — единственный из трех, который вы можете запустить за пределами своей виртуальной среды
+(venv) $ python -m celery -A netology_pd_diplom worker
+```
+
+## Разворачиваем проект с помощью Docker и Docker-compose
+
+1. Клонировать приоект
+
+2. Перейти в рабочую дерриктороию
+
+3. Загрузить все переменные сред
+```bash
+export DEBUG=True
+export SECRET_KEY="your_secret_key_here" # Отправил почтой
+export ALLOWED_HOSTS=localhost,127.0.0.1 # Или * - доступ с любого адреса
+export DB_ENGINE=django.db.backends.sqlite3
+export DB_NAME=db.sqlite3
+export EMAIL_HOST_PASSWORD="your_email_password" # Пароль привязанный к почтовому сервису (Настройки для mail.ru, справка https://help.mail.ru/mail/security/protection/external/) Отправил почтой
+```
+
+для PostgreSQl
+```bash
+export DEBUG=True
+export SECRET_KEY="your_secret_key_here"
+export ALLOWED_HOSTS=localhost,127.0.0.1
+export DB_ENGINE=django.db.backends.postgresql
+export DB_NAME="your_postgres_db_name"
+export POSTGRES_USER="your_postgres_db_user"
+export POSTGRES_PASSWORD="your_postgres_db_pass"
+export EMAIL_HOST_PASSWORD="your_email_host_password"
+```
+
+Кроме того, если видишь сообщение Compose can now delegate builds to bake for better performance,
+можешь включить использование нового инструмента Bake для сборки образов, установив переменную окружения:
+```bash
+export COMPOSE_BAKE=true
+```
+
+5. Запустить сборку (--build для пересборки)
+```bash
+docker-compose up -d --build
+```
+
+---
+## Дополнительное задание
+> Отказ от django signals, с дальнейшей интеграцией в проект фреймворка Celery для по-настоящему асинхронных задач
+Рекомендую начать с использования любой из нижеследующих инструкций.
+- https://realpython.com/asynchronous-tasks-with-django-and-celery/
+
+или
+- https://stackabuse.com/asynchronous-tasks-in-django-with-redis-and-celery/
+
+> Вижу вы покрыли код тестами, предлагаю добавить красивую кнопку с текущим покрытием у себя в Гитхабе
+https://github.com/marketplace/actions/python-coverage
+
+> Опробовать автогенерацию документации Open API в рамках пакета DRF-Spectacular. После внедрения обязательно внимательно просмотрите страницу Swagger. Тут сразу можно будет оценить для себя разницу с Postman и огромный потенциал для плодотворной работы в команде с несколькими разработчиками. P.S. Используйте чаще docstring к классам и функциям
+https://drf-spectacular.readthedocs.io/en/latest/readme.html
+
+> Попробовать простой DRF тротлинг, и можно проверить его работу добавление отдельного TestCase
+https://www.django-rest-framework.org/api-guide/throttling/
+
+> Добавить возможность авторизации с 1-2 социальных сетей, тут можно ознакомится с такой крутой библиотекой
+https://github.com/python-social-auth/social-app-django
+
+> Наверняка пользовались админкой в ходе создания проекта, предлагаю сделать ей мощный тюнинг через эти библиотеки на выбор:
+https://github.com/otto-torino/django-baton
+https://django-jet-reboot.readthedocs.io/en/latest/
+
+> После добавления Celery можно добавить загрузку аватаров пользователей и картинок товаров с последующей асинхронной обработкой их в фоновом режиме, например создание миниатюр различного размера для быстрой загрузки. Для этого есть множество библиотек на выбор:
+https://easy-thumbnails.readthedocs.io/en/latest/
+https://django-versatileimagefield.readthedocs.io/en/latest/
+https://django-imagekit.readthedocs.io/en/latest/
+
+> Очень часто в коммерческих проектах для перехвата ошибок, которые возникают у пользователей внедряют Sentry или Rollbar. Он позволяет быть в курсе насколько стабильно работает проект и к тому же хранит полные traceback ошибок, так чтобы можно было их исследовать. Будет большой плюс в резюме от наличия знаний по этой технологии. Попробуйте внедрить ее на базовом уровне и создать APIView, который вызывает исключение, что увидеть его в консоли Sentry или RollBar
+https://blog.sentry.io/monitoring-performance-and-errors-in-a-django-application-with-sentry/
+https://docs.sentry.io/platforms/python/integrations/django/
+RollBar
+https://docs.rollbar.com/docs/django
+https://docs.rollbar.com/docs/celery
+
+> внедрить кэширование запросов к БД с использованием Redis, посмотреть насколько уменьшится время отклика системы. Вот эти библиотеки на выбор могут помочь с кэшированием запросов к БД:
+https://github.com/noripyt/django-cachalot
+https://github.com/Suor/django-cacheops
+
+> и последнее только по желанию, часто в проектах бывают задачи поиска причин медленной работы запроса. Например, из-за неправильного ORM запроса к БД. Рекомендую ознакомиться с пакетом https://github.com/jazzband/django-silk
+проанализировать запросы в Postman и посмотреть какие из них вызывают каскад вторичных запросов, которые перегружают СУБД. Лично у меня вызывает удовлетворение, если ранее запрос выполнялся 400-500 мс, а сейчас за 3-4 мс, после того как я выявил проблему через Silk и оптимизировал запрос. Возможно, и Вам понравится)
+
+---
## **Получить исходный код**
diff --git a/reference/netology_pd_diplom/UserFlow.md b/reference/netology_pd_diplom/UserFlow.md
new file mode 100644
index 000000000..a48f5ae1c
--- /dev/null
+++ b/reference/netology_pd_diplom/UserFlow.md
@@ -0,0 +1,888 @@
+# Юзер флоу с ссылками на эндпоинты:
+### *Ниже представлен уточненный юзер флоу с привязкой к эндпоинтам. Данный юзер флоу охватывает полное взаимодействие пользователя с интернет-магазином, включая регистрацию, авторизацию, управление контактами, работу с заказами и управление магазинами, а также события, которые инициируют отправку уведомлений по электронной почте, улучшая пользовательский опыт и обеспечивая ясность в коммуникации.*
+---
+
+[Документация в Postman](https://www.postman.com/martian-meadow-988006/workspace/my-diplom/collection/24194477-dd78304f-62a3-4f96-b0f3-aacd6094d18e?action=share&creator=24194477)
+
+---
+
+### 1. **Регистрация пользователя**
+- Эндпоинт: **POST `/user/register`**
+ - Новый пользователь регистрируется, предоставляя уникальный email, пароль и дополнительные данные о компании. Пользователь может зарегистрироваться как покупатель buyer (по умолчанию) или магазин shop (в административной панеле сервиса).
+
+```bash
+curl --location --request POST 'http://localhost:8000/api/v1/user/register' \
+--form 'first_name="Константин"' \
+--form 'last_name="Фещук"' \
+--form 'email="dilmah949dilma@gmail.com"' \
+--form 'password="qwer1234A"' \
+--form 'company=""' \
+--form 'position=""'
+```
+```bash
+{
+ "Status": true
+}
+```
+- **Событие:** При создании нового пользователя отправляется e-mail с токеном подтверждения для активации учетной записи.
+
+- Подтверждение email:
+ - Эндпоинт: **POST `/user/register/confirm`**
+ - Пользователь подтверждает свою почту, передавая в теле запроса токен подтверждения, чтобы активировать учетную запись (`is_active`). При этом токен, в целях безопасности, удаляется из базы данных.
+
+```bash
+curl --location --request POST 'http://localhost:8000/api/v1/user/register/confirm' \
+--form 'email="dilmah949dilma@gmail.com"' \
+--form 'token="c335efb7aef8e3d3f0df5951450"'
+```
+```bash
+{
+ "Status": true
+}
+```
+
+---
+
+### 2. **Авторизация**
+- Эндпоинт: **POST `/user/login`**
+ - Пользователь вводит логин (email) и пароль для входа в систему. После успешного входа система проверяет активность учетной записи (`is_active`) и формирует токен для аутентификации. Этот токен далее передается в заголовке `Authorization` во всех последующих запросах, чтобы идентифицировать пользователя.
+
+```bash
+curl --location --request POST 'http://localhost:8000/api/v1/user/login' \
+--form 'email="dilmah949dilma@gmail.com"' \
+--form 'password="qwer1234A"'
+```
+```bash
+{
+ "Status": true,
+ "Token": "e77fbc201870a229252b67a35ed1600e6a60bbd5"
+}
+```
+---
+
+### 3. **Просмотр и редактирование данных пользователя**
+- Эндпоинт: **GET, POST `/user/details`**
+ - Зарегистрированный пользователь получает информацию о своем профиле и может редактировать ее методом POST.
+
+**Просмотр данных пользователя о своем профиле**
+Ключ `contacts` содержит список контактных данных пользователя и заполняется после добавления пользователем своего контакта (см. пункт 4)
+```bash
+curl --location --request GET 'http://localhost:8000/api/v1/user/details' \
+--header 'Authorization: Token e77fbc201870a229252b67a35ed1600e6a60bbd5'
+```
+```bash
+{
+ "id": 24,
+ "first_name": "Костя",
+ "last_name": "Фещук",
+ "email": "dilmah949dilma@gmail.com",
+ "company": "",
+ "position": "",
+ "contacts": [
+ {
+ "id": 3,
+ "city": "Калининград",
+ "street": "Бахчисарайска",
+ "house": "26",
+ "structure": "4",
+ "building": "",
+ "apartment": "",
+ "phone": "89210088233"
+ }
+ ]
+}
+```
+
+**Редактирование данных пользователя о своем профиле методом POST**
+
+```bash
+curl --location --request POST 'http://localhost:8000/api/v1/user/details' \
+--header 'Authorization: Token e77fbc201870a229252b67a35ed1600e6a60bbd5' \
+--form 'first_name="Константин Николаевич"' \
+--form 'last_name="Фещук"' \
+--form 'email="dilmah949dilma@gmail.com"' \
+--form 'password="qwer1234A"' \
+--form 'company="mycompany"' \
+--form 'position="myposition"'
+```
+```bash
+{
+ "Status": true
+}
+```
+---
+
+### 4. **Управление контактами пользователя**
+- Эндпоинт: **POST/GET/PUT/DELETE `/user/contact`**
+ - Пользователь может добавлять, изменять или удалять контактную информацию (например, адрес и телефон), используемую для выполнения заказов.
+
+**Добавление контактной информации**
+```bash
+curl --location --request POST 'http://localhost:8000/api/v1/user/contact' \
+--header 'Authorization: Token e77fbc201870a229252b67a35ed1600e6a60bbd5' \
+--form 'city="Калининград"' \
+--form 'street="Бахчисарайская"' \
+--form 'house="26"' \
+--form 'structure="4"' \
+--form 'building="123"' \
+--form 'apartment="123"' \
+--form 'phone="+49564563242"'
+```
+```bash
+{
+ "Status": true
+}
+```
+
+**Просмотр контактной информации**
+```bash
+curl --location --request GET 'http://localhost:8000/api/v1/user/contact' \
+--header 'Authorization: Token e77fbc201870a229252b67a35ed1600e6a60bbd5'
+```
+```bash
+[
+ {
+ "id": 1,
+ "city": "Калининград",
+ "street": "Бахчисарайская",
+ "house": "26",
+ "structure": "4",
+ "building": "123",
+ "apartment": "123",
+ "phone": "+49564563242"
+ }
+]
+```
+
+**Удаление контактной информации**
+```bash
+curl --location --request DELETE 'http://localhost:8000/api/v1/user/contact' \
+--header 'Content-Type: application/x-www-form-urlencoded' \
+--header 'Authorization: Token e77fbc201870a229252b67a35ed1600e6a60bbd5' \
+--form 'items="1,2"'
+```
+```bash
+{
+ "Status": true,
+ "Удалено объектов": 2
+}
+```
+
+**Редактирование контактной информации**
+```bash
+curl --location --request PUT 'http://localhost:8000/api/v1/user/contact' \
+--header 'Authorization: Token e77fbc201870a229252b67a35ed1600e6a60bbd5' \
+--form 'city="Калининград"' \
+--form 'street="Бахчисарайска"' \
+--form 'house="26"' \
+--form 'structure="4"' \
+--form 'building=""' \
+--form 'apartment=""' \
+--form 'id="3"' \
+--form 'phone="89210088233"'
+```
+```bash
+{
+ "Status": true
+}
+```
+---
+
+### 5. **Работа покупателей**
+
+#### 5.1. **Просмотр магазинов**
+- Эндпоинт: **GET `/shops`**
+ - Покупатель получает список активных магазинов, готовых к приему заказов (`Shop.state = True`).
+```bash
+curl --location --request GET 'http://localhost:8000/api/v1/shops'
+```
+```bash
+{
+ "count": 1,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 1,
+ "name": "Связной",
+ "state": true
+ }
+ ]
+}
+```
+#### 5.2. **Просмотр категорий товаров**
+- Эндпоинт: **GET `/categories`**
+ - Пользователь видит список категорий товаров, доступных для покупки.
+```bash
+curl --location --request GET 'http://localhost:8000/api/v1/categories'
+```
+```bash
+{
+ "count": 4,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 5,
+ "name": "Телевизоры"
+ },
+ {
+ "id": 224,
+ "name": "Смартфоны"
+ },
+ {
+ "id": 15,
+ "name": "Аксессуары"
+ },
+ {
+ "id": 1,
+ "name": "Flash-накопители"
+ }
+ ]
+}
+```
+#### 5.3. **Просмотр товаров магазина**
+- Эндпоинт: **GET `/products?shop_id=1&category_id=224`**
+ - Покупатель видит доступные товары из каталога магазина, включая их характеристики и наличие.
+```bash
+curl --location --request GET 'http://localhost:8000/api/v1/products?shop_id=1&category_id=224'
+```
+
+Нажмите, чтобы развернуть
+
+```bash
+[
+ {
+ "id": 15,
+ "model": "apple/iphone/xs-max",
+ "product": {
+ "name": "Смартфон Apple iPhone XS Max 512GB (золотистый)",
+ "category": "Смартфоны"
+ },
+ "shop": 1,
+ "quantity": 14,
+ "price": 110000,
+ "price_rrc": 116990,
+ "product_parameters": [
+ {
+ "parameter": "Диагональ (дюйм)",
+ "value": "6.5"
+ },
+ {
+ "parameter": "Разрешение (пикс)",
+ "value": "2688x1242"
+ },
+ {
+ "parameter": "Встроенная память (Гб)",
+ "value": "512"
+ },
+ {
+ "parameter": "Цвет",
+ "value": "золотистый"
+ }
+ ]
+ },
+ {
+ "id": 16,
+ "model": "apple/iphone/xr",
+ "product": {
+ "name": "Смартфон Apple iPhone XR 256GB (красный)",
+ "category": "Смартфоны"
+ },
+ "shop": 1,
+ "quantity": 9,
+ "price": 65000,
+ "price_rrc": 69990,
+ "product_parameters": [
+ {
+ "parameter": "Диагональ (дюйм)",
+ "value": "6.1"
+ },
+ {
+ "parameter": "Разрешение (пикс)",
+ "value": "1792x828"
+ },
+ {
+ "parameter": "Встроенная память (Гб)",
+ "value": "256"
+ },
+ {
+ "parameter": "Цвет",
+ "value": "красный"
+ }
+ ]
+ },
+ {
+ "id": 17,
+ "model": "apple/iphone/xr",
+ "product": {
+ "name": "Смартфон Apple iPhone XR 256GB (черный)",
+ "category": "Смартфоны"
+ },
+ "shop": 1,
+ "quantity": 5,
+ "price": 65000,
+ "price_rrc": 69990,
+ "product_parameters": [
+ {
+ "parameter": "Диагональ (дюйм)",
+ "value": "6.1"
+ },
+ {
+ "parameter": "Разрешение (пикс)",
+ "value": "1792x828"
+ },
+ {
+ "parameter": "Встроенная память (Гб)",
+ "value": "256"
+ },
+ {
+ "parameter": "Цвет",
+ "value": "черный"
+ }
+ ]
+ },
+ {
+ "id": 18,
+ "model": "apple/iphone/xr",
+ "product": {
+ "name": "Смартфон Apple iPhone XR 128GB (синий)",
+ "category": "Смартфоны"
+ },
+ "shop": 1,
+ "quantity": 7,
+ "price": 60000,
+ "price_rrc": 64990,
+ "product_parameters": [
+ {
+ "parameter": "Диагональ (дюйм)",
+ "value": "6.1"
+ },
+ {
+ "parameter": "Разрешение (пикс)",
+ "value": "1792x828"
+ },
+ {
+ "parameter": "Встроенная память (Гб)",
+ "value": "256"
+ },
+ {
+ "parameter": "Цвет",
+ "value": "синий"
+ }
+ ]
+ },
+ {
+ "id": 23,
+ "model": "xiaomi/mi-10t-pro",
+ "product": {
+ "name": "Smartphone Xiaomi Mi 10T Pro 256GB (cosmic black)",
+ "category": "Смартфоны"
+ },
+ "shop": 1,
+ "quantity": 6,
+ "price": 70000,
+ "price_rrc": 74990,
+ "product_parameters": [
+ {
+ "parameter": "Screen Size (inches)",
+ "value": "6.67"
+ },
+ {
+ "parameter": "Resolution (pixels)",
+ "value": "2400x1080"
+ },
+ {
+ "parameter": "Internal Memory (GB)",
+ "value": "256"
+ },
+ {
+ "parameter": "Color",
+ "value": "cosmic black"
+ }
+ ]
+ }
+]
+```
+
+
+#### 5.4. **Добавление товаров в корзину. Просмотр и изменение ее содержимого**
+- Эндпоинт: **POST/GET/PUT/DELETE `/basket`**
+ - Авторизиролванный как пркупатель пользователь может добавлять товары и их количество в корзину, редактировать их количество или удалять товары. На этом этапе создается заказ `Order` с полем `state="basket"` (статус - корзина).
+ Также покупатель может просмотреть товары находящиеся в корзине.
+
+**Добавление товара в корзину**
+```bash
+curl --location --request POST 'http://localhost:8000/api/v1/basket' \
+--header 'Content-Type: application/x-www-form-urlencoded ' \
+--header 'Authorization: Token e77fbc201870a229252b67a35ed1600e6a60bbd5' \
+--form 'items="[
+ {
+ \"product_info\": 25,
+ \"quantity\": 13
+ },
+ {
+ \"product_info\": 26,
+ \"quantity\": 12
+ }
+ ]"'
+```
+
+```bash
+{
+ "Status": true,
+ "Создано объектов": 2
+}
+```
+
+
+**Просмотр корзины**
+```bash
+curl --location --request GET 'http://localhost:8000/api/v1/basket' \
+--header 'Content-Type: application/x-www-form-urlencoded' \
+--header 'Authorization: Token e77fbc201870a229252b67a35ed1600e6a60bbd5'
+```
+
+
+Нажмите, чтобы развернуть
+
+```bash
+[
+ {
+ "id": 1,
+ "ordered_items": [
+ {
+ "id": 3,
+ "product_info": {
+ "id": 25,
+ "model": "lg/oled-cx",
+ "product": {
+ "name": "LG OLED CX 55\" 4K UHD Smart TV",
+ "category": "Телевизоры"
+ },
+ "shop": 1,
+ "quantity": 7,
+ "price": 1800,
+ "price_rrc": 1999,
+ "product_parameters": [
+ {
+ "parameter": "Screen Size (inches)",
+ "value": "55"
+ },
+ {
+ "parameter": "Resolution (pixels)",
+ "value": "3840x2160"
+ },
+ {
+ "parameter": "Smart TV",
+ "value": "True"
+ }
+ ]
+ },
+ "quantity": 13
+ },
+ {
+ "id": 4,
+ "product_info": {
+ "id": 26,
+ "model": "sony/bravia-x900h",
+ "product": {
+ "name": "Sony Bravia X900H 75\" 4K UHD Smart TV",
+ "category": "Телевизоры"
+ },
+ "shop": 1,
+ "quantity": 3,
+ "price": 3000,
+ "price_rrc": 3499,
+ "product_parameters": [
+ {
+ "parameter": "Screen Size (inches)",
+ "value": "75"
+ },
+ {
+ "parameter": "Resolution (pixels)",
+ "value": "3840x2160"
+ },
+ {
+ "parameter": "Smart TV",
+ "value": "True"
+ }
+ ]
+ },
+ "quantity": 12
+ }
+ ],
+ "state": "basket",
+ "dt": "2025-02-28T21:05:14.051909Z",
+ "total_sum": 59400,
+ "contact": null # Заполнить при добавлении контакта
+ }
+]
+```
+
+
+**Удаление выбранных товаров из корзины**
+```bash
+curl --location --request DELETE 'http://localhost:8000/api/v1/basket' \
+--header 'Content-Type: application/x-www-form-urlencoded' \
+--header 'Authorization: Token e77fbc201870a229252b67a35ed1600e6a60bbd5' \
+--form 'items="1,2"'
+```
+
+```bash
+{
+ "Status": true,
+ "Удалено объектов": 2
+}
+```
+
+**Редактирование количество товаров в корзине**
+```bash
+curl --location --request PUT 'http://localhost:8000/api/v1/basket' \
+--header 'Content-Type: application/x-www-form-urlencoded' \
+--header 'Authorization: Token e77fbc201870a229252b67a35ed1600e6a60bbd5' \
+--form 'items="[
+ {
+ \"id\": 5,
+ \"quantity\": 3
+ },
+ {
+ \"id\": 6,
+ \"quantity\": 1
+ }
+ ]"'
+```
+
+```bash
+{
+ "Status": true,
+ "Обновлено объектов": 2
+}
+```
+
+#### 5.5. **Оформление заказа**
+- Эндпоинт: **POST `/order`**
+ - Покупатель отправляет форму с контактными данными (`id` - id заказа и `contact` - id контактной информации). Заказ меняет статус на `state="new"` и становится доступен для обработки магазином.
+
+- Событие: При изменении состояния заказа отправляется e-mail магазину с уведомлением об изменении статуса заказа с сообщением ЭТО NEW!!! >>> "Статус заказа изменен на: Новый" <<< ЭТО NEW!!!.
+
+```bash
+curl --location --request POST 'http://localhost:8000/api/v1/order' \
+--header 'Content-Type: application/x-www-form-urlencoded' \
+--header 'Authorization: Token e77fbc201870a229252b67a35ed1600e6a60bbd5' \
+--form 'id="1"' \
+--form 'contact="3"'
+```
+
+```bash
+{
+ "Status": true
+}
+```
+---
+
+### 6. **Работа с заказами**
+
+#### 6.1. **Просмотр своих заказов (покупатель)**
+- Эндпоинт: **GET `/order`**
+ - Покупатель видит список своих заказов, их статусы (`new`, `confirmed`, `sent`, `delivered`, `canceled`) и детали.
+
+```bash
+curl --location --request GET 'http://localhost:8000/api/v1/order' \
+--header 'Content-Type: application/x-www-form-urlencoded' \
+--header 'Authorization: Token e77fbc201870a229252b67a35ed1600e6a60bbd5'
+```
+
+
+Нажмите, чтобы развернуть
+
+```bash
+[
+ {
+ "id": 1,
+ "ordered_items": [
+ {
+ "id": 5,
+ "product_info": {
+ "id": 25,
+ "model": "lg/oled-cx",
+ "product": {
+ "name": "LG OLED CX 55\" 4K UHD Smart TV",
+ "category": "Телевизоры"
+ },
+ "shop": 1,
+ "quantity": 7,
+ "price": 1800,
+ "price_rrc": 1999,
+ "product_parameters": [
+ {
+ "parameter": "Screen Size (inches)",
+ "value": "55"
+ },
+ {
+ "parameter": "Resolution (pixels)",
+ "value": "3840x2160"
+ },
+ {
+ "parameter": "Smart TV",
+ "value": "True"
+ }
+ ]
+ },
+ "quantity": 3
+ },
+ {
+ "id": 6,
+ "product_info": {
+ "id": 26,
+ "model": "sony/bravia-x900h",
+ "product": {
+ "name": "Sony Bravia X900H 75\" 4K UHD Smart TV",
+ "category": "Телевизоры"
+ },
+ "shop": 1,
+ "quantity": 3,
+ "price": 3000,
+ "price_rrc": 3499,
+ "product_parameters": [
+ {
+ "parameter": "Screen Size (inches)",
+ "value": "75"
+ },
+ {
+ "parameter": "Resolution (pixels)",
+ "value": "3840x2160"
+ },
+ {
+ "parameter": "Smart TV",
+ "value": "True"
+ }
+ ]
+ },
+ "quantity": 1
+ }
+ ],
+ "state": "new",
+ "dt": "2025-02-28T21:05:14.051909Z",
+ "total_sum": 8400,
+ "contact": {
+ "id": 3,
+ "city": "Калининград",
+ "street": "Бахчисарайска",
+ "house": "26",
+ "structure": "4",
+ "building": "",
+ "apartment": "",
+ "phone": "89210088233"
+ }
+ }
+]
+```
+
+
+#### 6.2. **Получение заказов (магазин)**
+- Эндпоинт: **GET `/partner/orders`**
+ - Магазин получает список новых заказов, поступивших от покупателей, для обработки.
+
+```bash
+curl --location --request GET 'http://localhost:8000/api/v1/partner/orders' \
+--header 'Content-Type: application/x-www-form-urlencoded' \
+--header 'Authorization: Token 53a4742c02627553d1dbee0268815f72d1c40b5b'
+```
+
+
+Нажмите, чтобы развернуть
+
+```bash
+[
+ {
+ "id": 1,
+ "ordered_items": [
+ {
+ "id": 5,
+ "product_info": {
+ "id": 25,
+ "model": "lg/oled-cx",
+ "product": {
+ "name": "LG OLED CX 55\" 4K UHD Smart TV",
+ "category": "Телевизоры"
+ },
+ "shop": 1,
+ "quantity": 7,
+ "price": 1800,
+ "price_rrc": 1999,
+ "product_parameters": [
+ {
+ "parameter": "Screen Size (inches)",
+ "value": "55"
+ },
+ {
+ "parameter": "Resolution (pixels)",
+ "value": "3840x2160"
+ },
+ {
+ "parameter": "Smart TV",
+ "value": "True"
+ }
+ ]
+ },
+ "quantity": 3
+ },
+ {
+ "id": 6,
+ "product_info": {
+ "id": 26,
+ "model": "sony/bravia-x900h",
+ "product": {
+ "name": "Sony Bravia X900H 75\" 4K UHD Smart TV",
+ "category": "Телевизоры"
+ },
+ "shop": 1,
+ "quantity": 3,
+ "price": 3000,
+ "price_rrc": 3499,
+ "product_parameters": [
+ {
+ "parameter": "Screen Size (inches)",
+ "value": "75"
+ },
+ {
+ "parameter": "Resolution (pixels)",
+ "value": "3840x2160"
+ },
+ {
+ "parameter": "Smart TV",
+ "value": "True"
+ }
+ ]
+ },
+ "quantity": 1
+ }
+ ],
+ "state": "new",
+ "dt": "2025-02-28T21:05:14.051909Z",
+ "total_sum": 8400,
+ "contact": {
+ "id": 3,
+ "city": "Калининград",
+ "street": "Бахчисарайска",
+ "house": "26",
+ "structure": "4",
+ "building": "",
+ "apartment": "",
+ "phone": "89210088233"
+ }
+ }
+]
+```
+
+
+#### 6.3. **Обновление состояния заказа** (NEW!)
+- Эндпоинт: **POST `/partner/orders`**
+ - Магазин меняет статус заказа на значения `STATE_CHOICES` соответствующие `"confirmed"`, `"assembled"`, `"sent"`, `"delivered"` или `"canceled"` в соответствии с этапами его обработки.
+
+```bash
+curl --location --request POST 'http://localhost:8000/api/v1/partner/orders' \
+--header 'Content-Type: application/x-www-form-urlencoded' \
+--header 'Authorization: Token 53a4742c02627553d1dbee0268815f72d1c40b5b' \
+--form 'id="1"' \ # номер заказа
+--form 'state="confirmed"' # статус заказа из STATE_CHOICES
+```
+
+```bash
+{
+ "Status": true,
+ "Message": "Статус заказа успешно изменен на Подтвержден"
+}
+```
+
+- Событие: При изменении состояния заказа отправляется e-mail пользователю с уведомлением об измененном статусе заказа.
+Статус заказа обязательно выбирается из `STATE_CHOICES` и из запроса интегрируется в тело письма (<<< ЭТО NEW!)
+
+---
+
+### 7. **Управление магазином (для владельцев магазинов)**
+
+#### 7.1. **Обновление товаров и данных магазина**
+- Эндпоинт: **POST `/partner/update`**
+ - Магазин добавляет или обновляет информацию о своих товарах, ценах и наличии.
+
+```bash
+curl --location --request POST 'http://localhost:8000/api/v1/partner/update' \
+--header 'Authorization: Token 53a4742c02627553d1dbee0268815f72d1c40b5b' \
+--form 'url="https://raw.githubusercontent.com/netology-code/python-final-diplom/master/data/shop1.yaml"' # это адрес прайса поставщика товаров
+```
+
+```bash
+{
+ "Status": true
+}
+```
+
+#### 7.2. **Изменение статуса магазина**
+- Эндпоинт: **GET, POST `/partner/state`**
+ - Владелец магазина может открывать или закрывать свой магазин, чтобы управлять возможностью принимать новые заказы (`state=True/False`).
+
+**Просмотреть статус магазина**
+
+```bash
+curl --location --request GET 'http://localhost:8000/api/v1/partner/state' \
+--header 'Content-Type: application/x-www-form-urlencoded' \
+--header 'Authorization: Token 53a4742c02627553d1dbee0268815f72d1c40b5b'
+```
+
+```bash
+{
+ "id": 1,
+ "name": "Связной",
+ "state": true
+}
+```
+**Изменить (открыть/закрыть) статус магазина**
+
+```bash
+curl --location --request POST 'http://localhost:8000/api/v1/partner/state' \
+--header 'Authorization: Token 53a4742c02627553d1dbee0268815f72d1c40b5b' \
+--form 'state="off"' # Или state="on"
+```
+
+```bash
+{
+ "Status": true
+}
+```
+
+---
+
+### 8. **Сброс пароля**
+- Эндпоинты:
+ - **POST `/user/password_reset`** — для запроса сброса пароля.
+
+```bash
+curl --location --request POST 'http://localhost:8000/api/v1/user/password_reset' \
+--form 'email="dilmah949dilma@gmail.com"'
+```
+
+```bash
+{
+ "status": "OK"
+}
+```
+
+ - **Событие:** При создании токена сброса пароля отправляется e-mail пользователю с токеном для сброса пароля.
+
+ - **POST `/user/password_reset/confirm`** — для подтверждения сброса и установки нового пароля.
+
+```bash
+curl --location --request POST 'http://localhost:8000/api/v1/user/password_reset/confirm' \
+--form 'email="dilmah949dilma@gmail.com"' \
+--form 'password="qwer1234Anew"' \
+--form 'token="adeaf600a9098d0e730e1025576d70a14ac3c6a"'
+```
+
+```bash
+{
+ "status": "OK"
+}
+```
+
diff --git a/reference/netology_pd_diplom/backend/apps.py b/reference/netology_pd_diplom/backend/apps.py
index 90cbb2cb4..e0101c21c 100644
--- a/reference/netology_pd_diplom/backend/apps.py
+++ b/reference/netology_pd_diplom/backend/apps.py
@@ -1,11 +1,13 @@
from django.apps import AppConfig
-
class BackendConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'backend'
def ready(self):
- """
- импортируем сигналы
- """
+ # Импортируем обработчики сигналов здесь, чтобы избежать циклических импортов
+ from backend.signals import (
+ password_reset_token_created,
+ new_user_registered_signal,
+ new_order_signal
+ )
\ No newline at end of file
diff --git a/reference/netology_pd_diplom/backend/db.sqlite3.png b/reference/netology_pd_diplom/backend/db.sqlite3.png
new file mode 100644
index 000000000..371ac48ba
Binary files /dev/null and b/reference/netology_pd_diplom/backend/db.sqlite3.png differ
diff --git a/reference/netology_pd_diplom/backend/models.py b/reference/netology_pd_diplom/backend/models.py
index d3138bfb9..f515b2793 100644
--- a/reference/netology_pd_diplom/backend/models.py
+++ b/reference/netology_pd_diplom/backend/models.py
@@ -2,18 +2,19 @@
from django.contrib.auth.models import AbstractUser
from django.contrib.auth.validators import UnicodeUsernameValidator
from django.db import models
-from django.utils.translation import gettext_lazy as _
+from django.utils.translation import gettext_lazy as _ # функция интернационализации (i18n)
+# gettext_lazy — "ленивая" версия функции перевода (вычисляется только при рендеринге, а не при загрузке модели).
from django_rest_passwordreset.tokens import get_token_generator
-STATE_CHOICES = (
- ('basket', 'Статус корзины'),
- ('new', 'Новый'),
- ('confirmed', 'Подтвержден'),
- ('assembled', 'Собран'),
- ('sent', 'Отправлен'),
- ('delivered', 'Доставлен'),
- ('canceled', 'Отменен'),
-)
+# STATE_CHOICES = (
+# ('basket', 'Статус корзины'),
+# ('new', 'Новый'),
+# ('confirmed', 'Подтвержден'),
+# ('assembled', 'Собран'),
+# ('sent', 'Отправлен'),
+# ('delivered', 'Доставлен'),
+# ('canceled', 'Отменен'),
+# ) Перемещено в Order
USER_TYPE_CHOICES = (
('shop', 'Магазин'),
@@ -28,31 +29,65 @@
class UserManager(BaseUserManager):
"""
Миксин для управления пользователями
+ Менеджер полностью переопределяет логику работы с username, используя email как основной идентификатор
"""
use_in_migrations = True
def _create_user(self, email, password, **extra_fields):
"""
- Create and save a user with the given username, email, and password.
+ Создайте и сохраните пользователя с указанным именем, адресом электронной почты и паролем.
+ Args:
+ email (str): Обязательное поле. Электронная почта пользователя
+ password (str): Пароль (может быть None для незарегистрированных пользователей)
+ **extra_fields: Дополнительные атрибуты пользователя
+ Returns:
+ User: созданный пользователь
+ Raises:
+ ValueError: если email не указан
"""
+ # Валидация обязательного поля
if not email:
raise ValueError('The given email must be set')
+ # Нормализация email (приведение к нижнему регистру, обрезка пробелов)
email = self.normalize_email(email)
+ # Создание объекта пользователя (использует связанную модель User)
user = self.model(email=email, **extra_fields)
+ # Безопасное хеширование пароля перед сохранением в БД
user.set_password(password)
+ # Сохранение в БД с указанием используемой базы данных
user.save(using=self._db)
return user
+
def create_user(self, email, password=None, **extra_fields):
+ """
+ Создает обычного пользователя с дефолтными правами.
+
+ Особенности:
+ - is_staff: False (нет доступа в админку)
+ - is_superuser: False (нет прав суперпользователя)
+ """
extra_fields.setdefault('is_staff', False)
extra_fields.setdefault('is_superuser', False)
return self._create_user(email, password, **extra_fields)
def create_superuser(self, email, password, **extra_fields):
+ """
+ Создает суперпользователя с расширенными правами.
+
+ Автоматически устанавливает:
+ - is_staff: True (доступ в админку)
+ - is_superuser: True (полные права)
+ - is_active: True (активация без подтверждения email)
+
+ Raises:
+ ValueError: если права не соответствуют суперпользователю
+ """
+ # Установка прав суперпользователя
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
extra_fields.setdefault('is_active', True)
-
+ # Валидация установленных прав
if extra_fields.get('is_staff') is not True:
raise ValueError('Superuser must have is_staff=True.')
if extra_fields.get('is_superuser') is not True:
@@ -64,14 +99,39 @@ def create_superuser(self, email, password, **extra_fields):
class User(AbstractUser):
"""
Стандартная модель пользователей
+ Заменяет стандартный username на email для аутентификации (кастомизация).
"""
+ # Убираем обязательные поля по умолчанию (username не используется для входа)
REQUIRED_FIELDS = []
- objects = UserManager()
+
+ # Менеджер объектов с расширенной логикой создания пользователей, так как
+ # используем email как уникальный идентификатор вместо username
+ objects = UserManager() # теперь при работе с БД через object можно использовать методы из
+ # кастомного менеджера UserManager() - create_user() и create_superuser(), например
+ # user = User.objects.create_user() или user = User.objects.create_superuser()
+
+ # используем email как уникальный идентификатор вместо username
USERNAME_FIELD = 'email'
+
+ # Основное поле для аутентификации (уникальное, обязательно для заполнения)
email = models.EmailField(_('email address'), unique=True)
- company = models.CharField(verbose_name='Компания', max_length=40, blank=True)
- position = models.CharField(verbose_name='Должность', max_length=40, blank=True)
+ # Дополнительные бизнес-поля (необязательные)
+ company = models.CharField(
+ verbose_name=_('Компания'),
+ max_length=40,
+ blank=True, # разрешаем пустое значение
+ help_text=_("Название компании пользователя (для магазинов)"),
+ )
+ position = models.CharField(
+ verbose_name=_('Должность'),
+ max_length=40,
+ blank=True,
+ help_text=_("Должность в компании (для сотрудников магазинов)"),
+ )
+ # Валидатор для username (оставлен для совместимости)
username_validator = UnicodeUsernameValidator()
+ # Поле username оставлено для совместимости с материнским AbstractUser,
+ # но не используется для аутентификации
username = models.CharField(
_('username'),
max_length=150,
@@ -81,53 +141,74 @@ class User(AbstractUser):
'unique': _("A user with that username already exists."),
},
)
+ # Статус активации (по умолчанию неактивен до подтверждения email)
is_active = models.BooleanField(
_('active'),
- default=False,
+ default=False, # активируется после подтверждения email
help_text=_(
'Designates whether this user should be treated as active. '
- 'Unselect this instead of deleting accounts.'
- ),
+ 'Unselect this instead of deleting accounts.'),
+ )
+ # Тип пользователя для разграничения ролей в системе
+ type = models.CharField(
+ verbose_name=_('Тип пользователя'),
+ choices=USER_TYPE_CHOICES, # ('shop', 'Магазин') или ('buyer', 'Покупатель')
+ max_length=5,
+ default='buyer',
+ help_text=_("Тип учетной записи (магазин или покупатель)"),
)
- type = models.CharField(verbose_name='Тип пользователя', choices=USER_TYPE_CHOICES, max_length=5, default='buyer')
def __str__(self):
- return f'{self.first_name} {self.last_name}'
+ """Строковое представление с использованием имени и фамилии"""
+ return f'{self.first_name} {self.last_name}' if self.first_name else self.email
class Meta:
- verbose_name = 'Пользователь'
- verbose_name_plural = "Список пользователей"
- ordering = ('email',)
+ verbose_name = _('Пользователь')
+ verbose_name_plural = _("Список пользователей")
+ ordering = ('email',) # сортировка по email по умолчанию
+ # Дополнительная защита от дубликатов
+ constraints = [
+ models.UniqueConstraint(
+ fields=['email'],
+ name='unique_email'
+ )
+ ]
class Shop(models.Model):
- objects = models.manager.Manager()
- name = models.CharField(max_length=50, verbose_name='Название')
- url = models.URLField(verbose_name='Ссылка', null=True, blank=True)
- user = models.OneToOneField(User, verbose_name='Пользователь',
+
+ objects = models.manager.Manager() # Можно создать кастомный менеджер, который позволит при взаимодействии с базой данных
+ # используя objects применять свои кастомные методы, но здесь используется стандартный менеджер,так как если
+ # провалиться внутрь него мы увидим заглушку pass, ну или какие-то собственные методы определены с помощью .from_queryset
+ # в субклассе QuerySet
+ # см. https://docs.djangoproject.com/en/4.1/topics/db/managers/#managers
+ # см. https://docs.google.com/document/d/1_zO0NaMzGqY6ohgqYxi875pghBdFho9RvKxB5fhbjSc/edit?usp=sharing
+ name = models.CharField(max_length=50, verbose_name=_('Название'))
+ url = models.URLField(verbose_name=_('Ссылка'), null=True, blank=True)
+ user = models.OneToOneField(User, verbose_name=_('Пользователь'),
blank=True, null=True,
on_delete=models.CASCADE)
- state = models.BooleanField(verbose_name='статус получения заказов', default=True)
+ state = models.BooleanField(verbose_name=_('статус получения заказов'), default=True)
# filename
class Meta:
- verbose_name = 'Магазин'
- verbose_name_plural = "Список магазинов"
- ordering = ('-name',)
+ verbose_name = _('Магазин')
+ verbose_name_plural = _('Список магазинов')
+ ordering = ('-name',) # сортировка по названию по убыванию
def __str__(self):
return self.name
class Category(models.Model):
- objects = models.manager.Manager()
- name = models.CharField(max_length=40, verbose_name='Название')
- shops = models.ManyToManyField(Shop, verbose_name='Магазины', related_name='categories', blank=True)
+ objects = models.manager.Manager()
+ name = models.CharField(max_length=40, verbose_name=_('Название'))
+ shops = models.ManyToManyField(Shop, verbose_name=_('Магазины'), related_name='categories', blank=True)
class Meta:
- verbose_name = 'Категория'
- verbose_name_plural = "Список категорий"
+ verbose_name = _('Категория')
+ verbose_name_plural = _('Список категорий')
ordering = ('-name',)
def __str__(self):
@@ -136,13 +217,13 @@ def __str__(self):
class Product(models.Model):
objects = models.manager.Manager()
- name = models.CharField(max_length=80, verbose_name='Название')
- category = models.ForeignKey(Category, verbose_name='Категория', related_name='products', blank=True,
+ name = models.CharField(max_length=80, verbose_name=_('Название'))
+ category = models.ForeignKey(Category, verbose_name=_('Категория'), related_name='products', blank=True,
on_delete=models.CASCADE)
class Meta:
- verbose_name = 'Продукт'
- verbose_name_plural = "Список продуктов"
+ verbose_name = _('Продукт')
+ verbose_name_plural = _('Список продуктов')
ordering = ('-name',)
def __str__(self):
@@ -151,31 +232,31 @@ def __str__(self):
class ProductInfo(models.Model):
objects = models.manager.Manager()
- model = models.CharField(max_length=80, verbose_name='Модель', blank=True)
- external_id = models.PositiveIntegerField(verbose_name='Внешний ИД')
- product = models.ForeignKey(Product, verbose_name='Продукт', related_name='product_infos', blank=True,
+ model = models.CharField(max_length=80, verbose_name=_('Модель'), blank=True)
+ external_id = models.PositiveIntegerField(verbose_name=_('Внешний ИД'))
+ product = models.ForeignKey(Product, verbose_name=_('Продукт'), related_name='product_infos', blank=True,
on_delete=models.CASCADE)
- shop = models.ForeignKey(Shop, verbose_name='Магазин', related_name='product_infos', blank=True,
+ shop = models.ForeignKey(Shop, verbose_name=_('Магазин'), related_name='product_infos', blank=True,
on_delete=models.CASCADE)
- quantity = models.PositiveIntegerField(verbose_name='Количество')
- price = models.PositiveIntegerField(verbose_name='Цена')
- price_rrc = models.PositiveIntegerField(verbose_name='Рекомендуемая розничная цена')
+ quantity = models.PositiveIntegerField(verbose_name=_('Количество'))
+ price = models.PositiveIntegerField(verbose_name=_('Цена'))
+ price_rrc = models.PositiveIntegerField(verbose_name=_('Рекомендуемая розничная цена'))
class Meta:
- verbose_name = 'Информация о продукте'
- verbose_name_plural = "Информационный список о продуктах"
+ verbose_name = _('Информация о продукте')
+ verbose_name_plural = _("Информационный список о продуктах")
constraints = [
models.UniqueConstraint(fields=['product', 'shop', 'external_id'], name='unique_product_info'),
- ]
+ ] # дополнительная защита от дубликатов
class Parameter(models.Model):
objects = models.manager.Manager()
- name = models.CharField(max_length=40, verbose_name='Название')
+ name = models.CharField(max_length=40, verbose_name=_('Название'))
class Meta:
- verbose_name = 'Имя параметра'
- verbose_name_plural = "Список имен параметров"
+ verbose_name = _('Имя параметра')
+ verbose_name_plural = _('Список имен параметров')
ordering = ('-name',)
def __str__(self):
@@ -184,59 +265,75 @@ def __str__(self):
class ProductParameter(models.Model):
objects = models.manager.Manager()
- product_info = models.ForeignKey(ProductInfo, verbose_name='Информация о продукте',
+ product_info = models.ForeignKey(ProductInfo, verbose_name=_('Информация о продукте'),
related_name='product_parameters', blank=True,
on_delete=models.CASCADE)
- parameter = models.ForeignKey(Parameter, verbose_name='Параметр', related_name='product_parameters', blank=True,
+ parameter = models.ForeignKey(Parameter, verbose_name=_('Параметр'), related_name='product_parameters', blank=True,
on_delete=models.CASCADE)
- value = models.CharField(verbose_name='Значение', max_length=100)
+ value = models.CharField(verbose_name=_('Значение'), max_length=100)
class Meta:
- verbose_name = 'Параметр'
- verbose_name_plural = "Список параметров"
+ verbose_name = _('Параметр')
+ verbose_name_plural = _('Список параметров')
constraints = [
models.UniqueConstraint(fields=['product_info', 'parameter'], name='unique_product_parameter'),
]
-
+
class Contact(models.Model):
objects = models.manager.Manager()
- user = models.ForeignKey(User, verbose_name='Пользователь',
+ user = models.ForeignKey(User, verbose_name=_('Пользователь'),
related_name='contacts', blank=True,
on_delete=models.CASCADE)
- city = models.CharField(max_length=50, verbose_name='Город')
- street = models.CharField(max_length=100, verbose_name='Улица')
- house = models.CharField(max_length=15, verbose_name='Дом', blank=True)
- structure = models.CharField(max_length=15, verbose_name='Корпус', blank=True)
- building = models.CharField(max_length=15, verbose_name='Строение', blank=True)
- apartment = models.CharField(max_length=15, verbose_name='Квартира', blank=True)
- phone = models.CharField(max_length=20, verbose_name='Телефон')
+ city = models.CharField(max_length=50, verbose_name=_('Город'))
+ street = models.CharField(max_length=100, verbose_name=_('Улица'))
+ house = models.CharField(max_length=15, verbose_name=_('Дом'), blank=True)
+ structure = models.CharField(max_length=15, verbose_name=_('Корпус'), blank=True)
+ building = models.CharField(max_length=15, verbose_name=_('Строение'), blank=True)
+ apartment = models.CharField(max_length=15, verbose_name=_('Квартира'), blank=True)
+ phone = models.CharField(max_length=20, verbose_name=_('Телефон'))
class Meta:
- verbose_name = 'Контакты пользователя'
- verbose_name_plural = "Список контактов пользователя"
+ verbose_name = _('Контакты пользователя')
+ verbose_name_plural = _("Список контактов пользователя")
def __str__(self):
return f'{self.city} {self.street} {self.house}'
class Order(models.Model):
+ # Все обращения к статусам теперь происходят через модель Order
+ # Соответствующим принципам Django (хранение CHOICES внутри модели)
+ STATE_CHOICES = (
+ ('basket', 'Статус корзины'),
+ ('new', 'Новый'),
+ ('confirmed', 'Подтвержден'),
+ ('assembled', 'Собран'),
+ ('sent', 'Отправлен'),
+ ('delivered', 'Доставлен'),
+ ('canceled', 'Отменен'),
+ )
+
objects = models.manager.Manager()
- user = models.ForeignKey(User, verbose_name='Пользователь',
+ user = models.ForeignKey(User, verbose_name=_('Пользователь'),
related_name='orders', blank=True,
on_delete=models.CASCADE)
dt = models.DateTimeField(auto_now_add=True)
- state = models.CharField(verbose_name='Статус', choices=STATE_CHOICES, max_length=15)
- contact = models.ForeignKey(Contact, verbose_name='Контакт',
+ state = models.CharField(verbose_name=_('Статус'), choices=STATE_CHOICES, max_length=15)
+ contact = models.ForeignKey(Contact, verbose_name=_('Контакт'),
blank=True, null=True,
on_delete=models.CASCADE)
class Meta:
- verbose_name = 'Заказ'
- verbose_name_plural = "Список заказ"
+ verbose_name = _('Заказ')
+ verbose_name_plural = _("Список заказ")
ordering = ('-dt',)
+ # NEW!!! Используется встроенный метод Django для полей с CHOICES (выборами) вместо ручного создания словаря
+ def get_state_display(self):
+ return dict(self.STATE_CHOICES).get(self.state, self.state)
+
def __str__(self):
return str(self.dt)
@@ -245,29 +342,29 @@ def __str__(self):
# return self.ordered_items.aggregate(total=Sum("quantity"))["total"]
-class OrderItem(models.Model):
+class OrderItem(models.Model): # связывает заказ с конкретными товарами (`ProductInfo`) и количеством
objects = models.manager.Manager()
- order = models.ForeignKey(Order, verbose_name='Заказ', related_name='ordered_items', blank=True,
+ order = models.ForeignKey(Order, verbose_name=_('Заказ'), related_name='ordered_items', blank=True,
on_delete=models.CASCADE)
- product_info = models.ForeignKey(ProductInfo, verbose_name='Информация о продукте', related_name='ordered_items',
+ product_info = models.ForeignKey(ProductInfo, verbose_name=_('Информация о продукте'), related_name='ordered_items',
blank=True,
on_delete=models.CASCADE)
- quantity = models.PositiveIntegerField(verbose_name='Количество')
+ quantity = models.PositiveIntegerField(verbose_name=_('Количество'))
class Meta:
- verbose_name = 'Заказанная позиция'
- verbose_name_plural = "Список заказанных позиций"
+ verbose_name = _('Заказанная позиция')
+ verbose_name_plural = _("Список заказанных позиций")
constraints = [
models.UniqueConstraint(fields=['order_id', 'product_info'], name='unique_order_item'),
- ]
+ ]
class ConfirmEmailToken(models.Model):
objects = models.manager.Manager()
class Meta:
- verbose_name = 'Токен подтверждения Email'
- verbose_name_plural = 'Токены подтверждения Email'
+ verbose_name = _('Токен подтверждения Email')
+ verbose_name_plural = _('Токены подтверждения Email')
@staticmethod
def generate_key():
@@ -278,15 +375,15 @@ def generate_key():
User,
related_name='confirm_email_tokens',
on_delete=models.CASCADE,
- verbose_name=_("The User which is associated to this password reset token")
+ verbose_name=_('The User which is associated to this password reset token')
)
created_at = models.DateTimeField(
auto_now_add=True,
- verbose_name=_("When was this token generated")
+ verbose_name=_('When was this token generated')
)
- # Key field, though it is not the primary key of the model
+ # Ключевое поле, хотя оно и не является первичным ключом модели
key = models.CharField(
_("Key"),
max_length=64,
@@ -300,4 +397,4 @@ def save(self, *args, **kwargs):
return super(ConfirmEmailToken, self).save(*args, **kwargs)
def __str__(self):
- return "Password reset token for user {user}".format(user=self.user)
+ return _("Password reset token for user {user}").format(user=self.user)
diff --git a/reference/netology_pd_diplom/backend/serializers.py b/reference/netology_pd_diplom/backend/serializers.py
index 48ec2d54b..cc5dbb538 100644
--- a/reference/netology_pd_diplom/backend/serializers.py
+++ b/reference/netology_pd_diplom/backend/serializers.py
@@ -8,21 +8,20 @@ class ContactSerializer(serializers.ModelSerializer):
class Meta:
model = Contact
fields = ('id', 'city', 'street', 'house', 'structure', 'building', 'apartment', 'user', 'phone')
- read_only_fields = ('id',)
+ read_only_fields = ('id',) # поле будет включено в вывод но не будет изменяться или создаваться пользователем
extra_kwargs = {
- 'user': {'write_only': True}
+ 'user': {'write_only': True} # поле user только для записи не будет возвращаться при сериализации
}
-
class UserSerializer(serializers.ModelSerializer):
- contacts = ContactSerializer(read_only=True, many=True)
+ # Вложенный сериализатор используется для связи "один ко многим" (many=True) между пользователем и его контактами
+ contacts = ContactSerializer(read_only=True, many=True) # contacts доступны только для чтения и через этот сериализатор не изменяются
class Meta:
model = User
fields = ('id', 'first_name', 'last_name', 'email', 'company', 'position', 'contacts')
read_only_fields = ('id',)
-
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
@@ -74,7 +73,7 @@ class Meta:
class OrderItemCreateSerializer(OrderItemSerializer):
- product_info = ProductInfoSerializer(read_only=True)
+ product_info = ProductInfoSerializer(read_only=True)
class OrderSerializer(serializers.ModelSerializer):
diff --git a/reference/netology_pd_diplom/backend/signals.py b/reference/netology_pd_diplom/backend/signals.py
index 0a2c0552e..5964f3177 100644
--- a/reference/netology_pd_diplom/backend/signals.py
+++ b/reference/netology_pd_diplom/backend/signals.py
@@ -6,77 +6,127 @@
from django.dispatch import receiver, Signal
from django_rest_passwordreset.signals import reset_password_token_created
-from backend.models import ConfirmEmailToken, User
+from backend.models import ConfirmEmailToken, User, Order
-new_user_registered = Signal()
+# Сигналы для отслеживания событий, таких как создание нового
+# пользователя и сброс пароля. Каждый раз, когда происходит событие, соответствующий
+# обработчик выполняет действия, например, отправляет электронное письмо.
-new_order = Signal()
+new_user_registered = Signal() # сигнал для регистрации нового пользователя
+new_order = Signal() # сигнал для обновления статуса заказа
@receiver(reset_password_token_created)
-def password_reset_token_created(sender, instance, reset_password_token, **kwargs):
+def password_reset_token_created(sender: Type[User], instance: User, reset_password_token: ConfirmEmailToken, **kwargs) -> None:
"""
- Отправляем письмо с токеном для сброса пароля
- When a token is created, an e-mail needs to be sent to the user
- :param sender: View Class that sent the signal
- :param instance: View Instance that sent the signal
- :param reset_password_token: Token Model Object
- :param kwargs:
- :return:
+ Отправляет электронное письмо пользователю, когда происходит сброс пароля.
+
+ Args:
+ sender: отправитель - модель User
+ instance: пользователь, для которого происходит сброс пароля
+ reset_password_token: токен для сброса пароля
+ **kwargs: дополнительные параметры
"""
- # send an e-mail to the user
-
+ # send an e-mail to the user
msg = EmailMultiAlternatives(
# title:
- f"Password Reset Token for {reset_password_token.user}",
+ f"Токен сброса пароля для {reset_password_token.user}",
# message:
reset_password_token.key,
# from:
settings.EMAIL_HOST_USER,
# to:
[reset_password_token.user.email]
- )
- msg.send()
+ ) # отправляем письмо с токеном для сброса пароля
+ msg.send() # отправляем письмо
@receiver(post_save, sender=User)
-def new_user_registered_signal(sender: Type[User], instance: User, created: bool, **kwargs):
+def new_user_registered_signal(sender: Type[User], instance: User, created: bool, **kwargs) -> None:
"""
- отправляем письмо с подтрердждением почты
+ Отправляет письмо с токеном для подтверждения электронного адреса,
+ когда пользователь регистрируется.
+ Args:
+ sender (Type[User]): отправитель - модель User
+ instance (User): новосозданный пользователь
+ created (bool): флаг, что пользователь был создан
+ **kwargs: дополнительные параметры
"""
- if created and not instance.is_active:
- # send an e-mail to the user
- token, _ = ConfirmEmailToken.objects.get_or_create(user_id=instance.pk)
+ # if created and not instance.is_active: # если пользователь создан и не активен
+ # # создаем токен для подтверждения электронного адреса
+ # token, _ = ConfirmEmailToken.objects.get_or_create(user_id=instance.pk) # возвращает кортеж, содержащий два элемента: сам объект и булево значение,
+ # # указывающее, был ли он создан или уже существовал (заглушка _).
+ # msg = EmailMultiAlternatives(
+ # # title:
+ # f"Токен подтверждения электронного адреса для {instance.email}",
+ # # message:
+ # token.key,
+ # # from:
+ # settings.EMAIL_HOST_USER,
+ # # to:
+ # [instance.email]
+ # )
+ # msg.send()
+ pass # NEW! Закоментировано так как ЭТО СДЕЛАНО НА АСИНХРОНКЕ в tasks.py
- msg = EmailMultiAlternatives(
- # title:
- f"Password Reset Token for {instance.email}",
- # message:
- token.key,
- # from:
- settings.EMAIL_HOST_USER,
- # to:
- [instance.email]
- )
- msg.send()
+# @receiver(new_order)
+# def new_order_signal(user_id: int, **kwargs) -> None:
+# """
+# Отправляет электронное письмо пользователю, когда происходит обновление статуса заказа.
+
+# Args:
+# user_id (int): id пользователя, которому будет отправлено письмо
+# **kwargs: дополнительные параметры
+# """
+# # send an e-mail to the user
+# user = User.objects.get(id=user_id)
+
+# msg = EmailMultiAlternatives(
+# # title:
+# f"Обновление статуса заказа",
+# # message:
+# 'Заказ сформирован',
+# # from:
+# settings.EMAIL_HOST_USER,
+# # to:
+# [user.email]
+# )
+# msg.send()
+
+
+######################### NEW NEW NEW ########################
@receiver(new_order)
-def new_order_signal(user_id, **kwargs):
+def new_order_signal(user_id: int, order: Order, **kwargs) -> None:
"""
- отправяем письмо при изменении статуса заказа
+ Отправляет электронное письмо пользователю при изменении статуса заказа.
+ Динамически цепляет статус заказа из запроса клиента и интегрирует его в тело письма
+
+ Args:
+ user_id (int): ID пользователя
+ order (Order): Объект заказа
"""
- # send an e-mail to the user
- user = User.objects.get(id=user_id)
+ try:
+ user = User.objects.get(id=user_id)
+
+ # Получаем человекочитаемое название статуса
+ state_display = order.get_state_display()
+
+ msg = EmailMultiAlternatives(
+ # Заголовок
+ f"Обновление статуса заказа №{order.id}",
+ # Сообщение с текущим статусом
+ f"Статус заказа изменен на: {state_display}",
+ # Отправитель
+ settings.EMAIL_HOST_USER,
+ # Получатель
+ [user.email]
+ )
+ msg.send()
+ except User.DoesNotExist:
+ print(f"User with id {user_id} not found")
+ except Exception as e:
+ print(f"Error sending email: {str(e)}")
- msg = EmailMultiAlternatives(
- # title:
- f"Обновление статуса заказа",
- # message:
- 'Заказ сформирован',
- # from:
- settings.EMAIL_HOST_USER,
- # to:
- [user.email]
- )
- msg.send()
+######################### NEW NEW NEW ########################
\ No newline at end of file
diff --git a/reference/netology_pd_diplom/backend/tasks.py b/reference/netology_pd_diplom/backend/tasks.py
new file mode 100644
index 000000000..99964667d
--- /dev/null
+++ b/reference/netology_pd_diplom/backend/tasks.py
@@ -0,0 +1,71 @@
+from celery import shared_task
+from django.conf import settings
+from django.core.mail import EmailMultiAlternatives
+from backend.models import ConfirmEmailToken, User, Order
+
+
+@shared_task
+def send_password_reset_email(reset_password_token_key: str, user_email: str):
+ """
+ Асинхронная задача для отправки email с токеном сброса пароля.
+
+ Args:
+ reset_password_token_key (str): Токен для сброса пароля.
+ user_email (str): Email пользователя.
+ """
+ msg = EmailMultiAlternatives(
+ # Заголовок
+ f"Восстановление пароля",
+ # Тело сообщения
+ reset_password_token_key,
+ # Отправитель
+ settings.EMAIL_HOST_USER,
+ # Получатель
+ [user_email],
+ )
+ msg.send()
+
+
+@shared_task
+def send_new_user_confirmation_email(token: str, email: str):
+ """
+ Асинхронная задача для отправки email с подтверждением регистрации.
+
+ Args:
+ token (str): Токен для подтверждения email.
+ email (str): Email пользователя.
+ """
+ msg = EmailMultiAlternatives(
+ # Заголовок
+ "Подтверждение регистрации ",
+ # Тело сообщения
+ f"Ваш токен для подтверждения email: {token}",
+ # Отправитель
+ settings.EMAIL_HOST_USER,
+ # Получатель
+ [email],
+ )
+ msg.send()
+
+
+@shared_task
+def send_order_status_update_email(user_email: str, order_id: int, state_display: str):
+ """
+ Асинхронная задача для отправки уведомления о смене статуса заказа.
+
+ Args:
+ user_email (str): Email пользователя.
+ order_id (int): ID заказа.
+ state_display (str): Человеко-читаемый статус заказа.
+ """
+ msg = EmailMultiAlternatives(
+ # Заголовок
+ f"Обновление статуса заказа №{order_id}",
+ # Сообщение с текущим статусом
+ f"Статус вашего заказа изменен на: {state_display}",
+ # Отправитель
+ settings.EMAIL_HOST_USER,
+ # Получатель
+ [user_email],
+ )
+ msg.send()
diff --git a/reference/netology_pd_diplom/backend/tests.py b/reference/netology_pd_diplom/backend/tests.py
deleted file mode 100644
index 7ce503c2d..000000000
--- a/reference/netology_pd_diplom/backend/tests.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from django.test import TestCase
-
-# Create your tests here.
diff --git a/reference/netology_pd_diplom/backend/urls.py b/reference/netology_pd_diplom/backend/urls.py
index 51ab7e918..c94333148 100644
--- a/reference/netology_pd_diplom/backend/urls.py
+++ b/reference/netology_pd_diplom/backend/urls.py
@@ -15,8 +15,10 @@
path('user/details', AccountDetails.as_view(), name='user-details'),
path('user/contact', ContactView.as_view(), name='user-contact'),
path('user/login', LoginAccount.as_view(), name='user-login'),
- path('user/password_reset', reset_password_request_token, name='password-reset'),
- path('user/password_reset/confirm', reset_password_confirm, name='password-reset-confirm'),
+ path('user/password_reset', reset_password_request_token, name='password-reset'), # Этот путь определяет URL, по которому пользователи могут запрашивать сброс пароля,
+ # на указанный пользователем маил высылается письмо с токеном сброса пароля
+ path('user/password_reset/confirm', reset_password_confirm, name='password-reset-confirm'), # Этот путь определяет URL, по которому пользователи могут подтверждать сброс пароля.
+ # путем отправки токена сброса пароля
path('categories', CategoryView.as_view(), name='categories'),
path('shops', ShopView.as_view(), name='shops'),
path('products', ProductInfoView.as_view(), name='shops'),
diff --git a/reference/netology_pd_diplom/backend/views.py b/reference/netology_pd_diplom/backend/views.py
index c9618c83b..7dd774751 100644
--- a/reference/netology_pd_diplom/backend/views.py
+++ b/reference/netology_pd_diplom/backend/views.py
@@ -1,4 +1,5 @@
from distutils.util import strtobool
+from typing import Any
from rest_framework.request import Request
from django.contrib.auth import authenticate
from django.contrib.auth.password_validation import validate_password
@@ -12,8 +13,9 @@
from rest_framework.generics import ListAPIView
from rest_framework.response import Response
from rest_framework.views import APIView
-from ujson import loads as load_json
+from ujson import loads as load_json # более быстрая альтернатива стандартной библиотеки json
from yaml import load as load_yaml, Loader
+from rest_framework import status # статусы ошибок
from backend.models import Shop, Category, Product, ProductInfo, Parameter, ProductParameter, Order, OrderItem, \
Contact, ConfirmEmailToken
@@ -22,50 +24,98 @@
from backend.signals import new_user_registered, new_order
+# class RegisterAccount(APIView):
+# """
+# Для регистрации покупателей
+# """
+
+# # Регистрация методом POST
+# def post(self, request: Request, *args: Any, **kwargs: Any) -> JsonResponse: # NEW Добавлена аннотация типов
+# """
+# Process a POST request and create a new user.
+
+# Args:
+# - request (Request): The Django request object.
+
+# Returns:
+# - JsonResponse: The response indicating the status of the operation and any errors.
+# """
+# # проверяем, находятся ли все ключи в request.data
+# if {'first_name', 'last_name', 'email', 'password', 'company', 'position'}.issubset(request.data):
+
+# # проверяем пароль на сложность
+# # sad = 'asd'
+# try:
+# validate_password(request.data['password']) # Проверка сложности пароля
+# except ValidationError as password_error: # Проверка ошибок пароля NEW
+# error_array = []
+# for item in password_error:
+# error_array.append(str(item)) # добавление строки (NEW) ошибок пароля в массив error_array
+# return JsonResponse({'Status': False, 'Errors': {'password': error_array}}, status=400) # Отправка клиенту информации об ошибке ввода пароля
+# else:
+# # проверяем данные пользователя на валидность
+# user_serializer = UserSerializer(data=request.data)
+# if user_serializer.is_valid():
+# user = user_serializer.save() # сохраняем данные пользователя в БД (если e-mail уникальный,
+# # что проверяется в models.py в классе UserManager в момент записи в БД
+# # Так как пароли должны храниться в безопасном виде.
+# # Метод set_password() автоматически хеширует пароль из запроса
+# # с использованием алгоритма, установленного в настройках Django (по умолчанию это PBKDF2).
+# user.set_password(request.data['password'])
+# user.save() # сохраняем пользователя и хешированный пароль
+# return JsonResponse({'Status': True})
+# else:
+# return JsonResponse({'Status': False, 'Errors': user_serializer.errors}, status=400)
+
+# return JsonResponse({'Status': False, 'Errors': 'Не указаны все необходимые аргументы'}, status=400)
+
+
+### NEW Async Celery ### NEW Async Celery ### NEW Async Celery ### NEW Async Celery ###
+
+from rest_framework.status import HTTP_400_BAD_REQUEST, HTTP_201_CREATED
+from backend.tasks import send_new_user_confirmation_email # Импортируем задачу для отправки письма
+
class RegisterAccount(APIView):
"""
- Для регистрации покупателей
+ Для регистрации пользователей
"""
-
- # Регистрация методом POST
-
- def post(self, request, *args, **kwargs):
+
+ def post(self, request: Request, *args: Any, **kwargs: Any) -> Response:
+ """
+ Обрабатываем POST-запросы и создаем новых пользователей.
+
+ :param request: Объект запроса от клиента.
+ :return: JSON-ответ с результатом операции и возможными ошибками.
"""
- Process a POST request and create a new user.
+ required_fields = ['first_name', 'last_name', 'email', 'password', 'company', 'position']
+ missing_fields = [field for field in required_fields if field not in request.data]
+
+ if missing_fields:
+ return Response({"Status": False, "Errors": {"missing_fields": missing_fields}},
+ status=HTTP_400_BAD_REQUEST)
+
+ try:
+ validate_password(request.data['password'])
+ except ValidationError as password_errors:
+ return Response({"Status": False, "Errors": {"password": list(password_errors)}},
+ status=HTTP_400_BAD_REQUEST)
+
+ serializer = UserSerializer(data=request.data)
+ if serializer.is_valid():
+ user = serializer.save()
+ user.set_password(request.data['password'])
+ user.save()
+
+ # Создаем токен подтверждения почтового адреса и отправляем письмо
+ confirm_token = ConfirmEmailToken.objects.create(user=user).key
+ send_new_user_confirmation_email.delay(confirm_token, user.email)
+
+ return Response({"Status": True}, status=HTTP_201_CREATED)
+ else:
+ return Response({"Status": False, "Errors": serializer.errors}, status=HTTP_400_BAD_REQUEST)
- Args:
- request (Request): The Django request object.
+### NEW Async Celery ### NEW Async Celery ### NEW Async Celery ### NEW Async Celery ###
- Returns:
- JsonResponse: The response indicating the status of the operation and any errors.
- """
- # проверяем обязательные аргументы
- if {'first_name', 'last_name', 'email', 'password', 'company', 'position'}.issubset(request.data):
-
- # проверяем пароль на сложность
- sad = 'asd'
- try:
- validate_password(request.data['password'])
- except Exception as password_error:
- error_array = []
- # noinspection PyTypeChecker
- for item in password_error:
- error_array.append(item)
- return JsonResponse({'Status': False, 'Errors': {'password': error_array}})
- else:
- # проверяем данные для уникальности имени пользователя
-
- user_serializer = UserSerializer(data=request.data)
- if user_serializer.is_valid():
- # сохраняем пользователя
- user = user_serializer.save()
- user.set_password(request.data['password'])
- user.save()
- return JsonResponse({'Status': True})
- else:
- return JsonResponse({'Status': False, 'Errors': user_serializer.errors})
-
- return JsonResponse({'Status': False, 'Errors': 'Не указаны все необходимые аргументы'})
class ConfirmAccount(APIView):
@@ -76,33 +126,33 @@ class ConfirmAccount(APIView):
# Регистрация методом POST
def post(self, request, *args, **kwargs):
"""
- Подтверждает почтовый адрес пользователя.
+ Подтверждает почтовый адрес пользователя.
- Args:
- - request (Request): The Django request object.
-
- Returns:
- - JsonResponse: The response indicating the status of the operation and any errors.
- """
- # проверяем обязательные аргументы
- if {'email', 'token'}.issubset(request.data):
+ Args:
+ - request (Request): The Django request object.
+ Returns:
+ - JsonResponse: The response indicating the status of the operation and any errors.
+ """
+ # проверяем, находятся ли все ключи в request.data
+ if {'email', 'token'}.issubset(request.data):
+ # Устанавливаем соответствие между парой email-token переданной POST парой email-token, хранящейся в БД
token = ConfirmEmailToken.objects.filter(user__email=request.data['email'],
key=request.data['token']).first()
if token:
- token.user.is_active = True
- token.user.save()
- token.delete()
+ token.user.is_active = True # если токен есть, активируем нового пользователя
+ token.user.save() # и сохраняем изменеия в БД
+ token.delete() # токен удаляем в целях повышения защиты
return JsonResponse({'Status': True})
else:
- return JsonResponse({'Status': False, 'Errors': 'Неправильно указан токен или email'})
+ return JsonResponse({'Status': False, 'Errors': 'Неправильно указан токен или email'}, status=400)
- return JsonResponse({'Status': False, 'Errors': 'Не указаны все необходимые аргументы'})
+ return JsonResponse({'Status': False, 'Errors': 'Не указаны все необходимые аргументы'}, status=400)
class AccountDetails(APIView):
"""
- A class for managing user account details.
+ Класс для управления данными учетной записи пользователя.
Methods:
- get: Retrieve the details of the authenticated user.
@@ -115,53 +165,50 @@ class AccountDetails(APIView):
# получить данные
def get(self, request: Request, *args, **kwargs):
"""
- Retrieve the details of the authenticated user.
+ Retrieve the details of the authenticated user.
- Args:
- - request (Request): The Django request object.
+ Args:
+ - request (Request): The Django request object.
- Returns:
- - Response: The response containing the details of the authenticated user.
+ Returns:
+ - Response: The response containing the details of the authenticated user.
"""
- if not request.user.is_authenticated:
+ if not request.user.is_authenticated: # если пользователь не аутентифицирован
return JsonResponse({'Status': False, 'Error': 'Log in required'}, status=403)
- serializer = UserSerializer(request.user)
+ serializer = UserSerializer(request.user) # сериализуем пользователя
return Response(serializer.data)
# Редактирование методом POST
def post(self, request, *args, **kwargs):
"""
- Update the account details of the authenticated user.
+ Update the account details of the authenticated user.
- Args:
- - request (Request): The Django request object.
+ Args:
+ - request (Request): The Django request object.
- Returns:
- - JsonResponse: The response indicating the status of the operation and any errors.
- """
- if not request.user.is_authenticated:
+ Returns:
+ - JsonResponse: The response indicating the status of the operation and any errors.
+ """
+ if not request.user.is_authenticated: # если пользователь не аутентифицирован
return JsonResponse({'Status': False, 'Error': 'Log in required'}, status=403)
# проверяем обязательные аргументы
-
if 'password' in request.data:
- errors = {}
+ # errors = {}
# проверяем пароль на сложность
try:
validate_password(request.data['password'])
except Exception as password_error:
- error_array = []
- # noinspection PyTypeChecker
- for item in password_error:
- error_array.append(item)
+ error_array = [] # создаем список для хранения ошибок
+ for item in password_error: # добавление строки ошибок пароля в массив error_array
+ error_array.append(item) # добавление строки ошибок пароля в массив error_array
return JsonResponse({'Status': False, 'Errors': {'password': error_array}})
else:
- request.user.set_password(request.data['password'])
-
- # проверяем остальные данные
- user_serializer = UserSerializer(request.user, data=request.data, partial=True)
- if user_serializer.is_valid():
- user_serializer.save()
+ request.user.set_password(request.data['password']) # записываем новый пароль в БД
+ user_serializer = UserSerializer(request.user, data=request.data, partial=True) # сериализуем пользователя (data - обновляемые данные
+ # partial=True - не все поля необходимо передавать, и если некоторые поля отсутствуют, это не вызовет ошибку валидации)
+ if user_serializer.is_valid(): # если данные валидны
+ user_serializer.save() # сохраняем изменения в БД
return JsonResponse({'Status': True})
else:
return JsonResponse({'Status': False, 'Errors': user_serializer.errors})
@@ -175,80 +222,97 @@ class LoginAccount(APIView):
# Авторизация методом POST
def post(self, request, *args, **kwargs):
"""
- Authenticate a user.
+ Создание токена аутентификации для активированных пользователей.
- Args:
- request (Request): The Django request object.
+ Args:
+ request (Request): The Django request object.
- Returns:
- JsonResponse: The response indicating the status of the operation and any errors.
- """
- if {'email', 'password'}.issubset(request.data):
- user = authenticate(request, username=request.data['email'], password=request.data['password'])
+ Returns:
+ JsonResponse: The response indicating the status of the operation and any errors.
+ """
+ if {'email', 'password'}.issubset(request.data): # проверяем, находятся ли все ключи в request.data
+ user = authenticate(request, username=request.data['email'], password=request.data['password']) # сверяем учетные данные пользователя из запроса с теми, что хранятся в БД
+ # и возвращает объект пользователя, если аутентификация по имени и паролю прошла успешно
- if user is not None:
- if user.is_active:
- token, _ = Token.objects.get_or_create(user=user)
+ if user is not None: # если пользователь найден
+ if user.is_active: # если пользователь активирован (заодно в админке выбрать этот пользователь buyer или shop)
+ token, _ = Token.objects.get_or_create(user=user) # создаем токен (этот токен будет использоваться для аутентификации пользователя в будущих запросах)
- return JsonResponse({'Status': True, 'Token': token.key})
+ return JsonResponse({'Status': True, 'Token': token.key}, status=200) # возвращаем токен клиенту
- return JsonResponse({'Status': False, 'Errors': 'Не удалось авторизовать'})
+ return JsonResponse({'Status': False, 'Errors': 'Не удалось авторизовать'}, status=400) # если пользователь не найден
- return JsonResponse({'Status': False, 'Errors': 'Не указаны все необходимые аргументы'})
+ return JsonResponse({'Status': False, 'Errors': 'Не указаны все необходимые аргументы'}, status=400) # если не указаны все необходимые аргументы
-class CategoryView(ListAPIView):
+class CategoryView(ListAPIView):
"""
- Класс для просмотра категорий
+ Класс для просмотра списка категорий товаров
"""
- queryset = Category.objects.all()
- serializer_class = CategorySerializer
+ queryset = Category.objects.all() # выбираем все категории товаров
+ serializer_class = CategorySerializer # сериализуем их
class ShopView(ListAPIView):
"""
Класс для просмотра списка магазинов
"""
- queryset = Shop.objects.filter(state=True)
+ queryset = Shop.objects.filter(state=True) # фильтруем магазины по статусу готовности к получению заказов, т.е. магазин открыт(закрыт)
serializer_class = ShopSerializer
class ProductInfoView(APIView):
"""
- A class for searching products.
+ Класс для поиска продуктов.
- Methods:
- - get: Retrieve the product information based on the specified filters.
+ Methods:
+ - get: Retrieve the product information based on the specified filters.
- Attributes:
- - None
- """
+ Attributes:
+ - None
+ """
def get(self, request: Request, *args, **kwargs):
"""
- Retrieve the product information based on the specified filters.
+ Клас для поиска продуктов по фильтрам.
- Args:
- - request (Request): The Django request object.
+ Args:
+ - request (Request): The Django request object.
- Returns:
- - Response: The response containing the product information.
- """
- query = Q(shop__state=True)
- shop_id = request.query_params.get('shop_id')
- category_id = request.query_params.get('category_id')
+ Returns:
+ - Response: The response containing the product information.
+ """
+ # Объект Q используется для создания сложных запросов с помощью логических операторов
+ query = Q(shop__state=True) # фильтруем магазины по статусу готовности к получению заказов
+ shop_id = request.query_params.get('shop_id') # получаем id магазина из параметров запроса
+ category_id = request.query_params.get('category_id') # получаем id категории из параметров запроса
- if shop_id:
- query = query & Q(shop_id=shop_id)
+ if shop_id: # если id магазина указан
+ query = query & Q(shop_id=shop_id) # фильтруем продукты по id магазина
- if category_id:
- query = query & Q(product__category_id=category_id)
+ if category_id: # если id категории указан
+ query = query & Q(product__category_id=category_id) # фильтруем продукты по id категории
- # фильтруем и отбрасываем дуликаты
+ # фильтруем объекты ProductInfo по сформированному запросу query и отбрасываем дубликаты
queryset = ProductInfo.objects.filter(
- query).select_related(
- 'shop', 'product__category').prefetch_related(
- 'product_parameters__parameter').distinct()
+ query).select_related( # информация о связанных магазинах (shop) и категориях продуктов (product__category)
+ # будет загружена джоином с основным объектом ProductInfo (т.е. в одном запросе к БД что эффективно)
+ 'shop', 'product__category').prefetch_related( # В отличие от select_related, метод выполняет отдельные запросы для получения связанных объектов,
+ # а затем соединяет их в Python с объектом ProductInfo.
+ # Здесь он извлекает параметры каждого продукта и их значения и соединяет с информацией о продукте.
+ 'product_parameters__parameter').distinct() # удаления дублирующихся записей из результата выборки
+
+ # ПОМЕТКА! prefetch_related - выполняет отдельные запросы для основной модели и для связанных объектов. Затем результаты объединяются в Python.
+ # ПОМЕТКА! select_related - использует SQL JOIN для выполнения запроса и получения связанных объектов одновременно с основным объектом.
+
+ # ЗАМЕТКА
+ # Жадная загрузка (чтобы уменьшить количество обращений к базе данных)
+ # всех связей из поля 'shop' (это id магазинов модели Shop) и из поля 'product' -
+ # это id продуктов соответствующей категории (поле 'category' модели Product,
+ # содержит id категорий, связанной модели Category).
+
+ # поле 'product_parameters' содержит id, связывающие значения параметров продуктов,
+ # с ProductInfo, а поле 'parameter' содержит id названий этих параметров из модели Parametr.
serializer = ProductInfoSerializer(queryset, many=True)
@@ -272,58 +336,72 @@ class BasketView(APIView):
# получить корзину
def get(self, request, *args, **kwargs):
"""
- Retrieve the items in the user's basket.
+ Retrieve the items in the user's basket.
- Args:
- - request (Request): The Django request object.
+ Args:
+ - request (Request): The Django request object.
- Returns:
- - Response: The response containing the items in the user's basket.
- """
- if not request.user.is_authenticated:
+ Returns:
+ - Response: The response containing the items in the user's basket.
+ """
+ if not request.user.is_authenticated: # проверяется, аутентифицирован ли пользователь
return JsonResponse({'Status': False, 'Error': 'Log in required'}, status=403)
- basket = Order.objects.filter(
- user_id=request.user.id, state='basket').prefetch_related(
- 'ordered_items__product_info__product__category',
- 'ordered_items__product_info__product_parameters__parameter').annotate(
+
+ # Извлекаем заказы из корзины и рассчитываем итоговую стоимость заказов в корзине
+ basket = Order.objects.filter( # Из модели Order извлекаются заказы пользователя с заданным user_id, у которых состояние равно "basket" (корзина)
+ user_id=request.user.id, state='basket').prefetch_related( # заранее извлекаем связанные данные,
+ # что минимизирует количество запросов к базе данных и улучшает производительность
+ 'ordered_items__product_info__product__category', # у id покупателя заказ, количество заказанных товаров их характеристики, названия и категории.
+ 'ordered_items__product_info__product_parameters__parameter').annotate( # параметры товаров и их значения.
total_sum=Sum(F('ordered_items__quantity') * F('ordered_items__product_info__price'))).distinct()
-
- serializer = OrderSerializer(basket, many=True)
+ # далее рассчитываем итоговую стоимость заказа total_sum - умножаем количество заказанных товаров на их цену и суммируем общую цену
+ serializer = OrderSerializer(basket, many=True) # сериализуем все заказы в корзине в формат JSON
return Response(serializer.data)
# редактировать корзину
def post(self, request, *args, **kwargs):
"""
- Add an items to the user's basket.
+ Добавить товары в корзину пользователя (покупателя).
- Args:
- - request (Request): The Django request object.
+ Args:
+ - request (Request): The Django request object.
- Returns:
- - JsonResponse: The response indicating the status of the operation and any errors.
- """
- if not request.user.is_authenticated:
+ Returns:
+ - JsonResponse: The response indicating the status of the operation and any errors.
+ """
+ if not request.user.is_authenticated: # проверяется, аутентифицирован ли пользователь
return JsonResponse({'Status': False, 'Error': 'Log in required'}, status=403)
- items_sting = request.data.get('items')
- if items_sting:
+ items_sting = request.data.get('items') # из байт-строки в формате JSON получаем данные о товарах из запроса
+ if items_sting: # если данные о товарах указаны в запросе
try:
- items_dict = load_json(items_sting)
+ #
+ items_dict = load_json(items_sting) # преобразуем данные о товарах из строки JSON в словарь python
except ValueError:
return JsonResponse({'Status': False, 'Errors': 'Неверный формат запроса'})
- else:
- basket, _ = Order.objects.get_or_create(user_id=request.user.id, state='basket')
- objects_created = 0
- for order_item in items_dict:
- order_item.update({'order': basket.id})
- serializer = OrderItemSerializer(data=order_item)
- if serializer.is_valid():
+ else: # если JSON-строка при преобразовании в словарь python была валидна
+ # добавляем товары в корзину: в моделе Order создаем новый заказ для пользователя с id
+ # и присваеваем ему статус "корзина"
+ basket, _ = Order.objects.get_or_create(user_id=request.user.id, state='basket') # создаем корзину, как экземпляр модели Order
+
+ # ЗАМЕТКА
+ # Если заказ с указанными параметрами существует, basket будет ссылаться на этот существующий объект.
+ # Если заказа нет, будет создан новый Order, и basket будет указывать на этот новый объект.
+ # _: Параметр, который принимает значение True, если объект был создан, и False, если объект был найден.
+ # Этот параметр не используется в дальнейшем коде, так как он обозначен знаком подчеркивания (зачастую
+ # это означает, что значение не нужно).
+
+ objects_created = 0 # счетчик добавленных в корзину товаров
+ for order_item in items_dict: # для каждого товара из словаря items_dict ...
+ order_item.update({'order': basket.id}) # ... добавляем id заказа (в словарь order_item добавляем/обновляем ключ order со значением basket.id)
+ serializer = OrderItemSerializer(data=order_item) # создаем экземпляр сериализатора OrderItemSerializer с данными из order_item
+ if serializer.is_valid(): # если данные валидны
try:
- serializer.save()
+ serializer.save() # сохраняем данные в модели OrderItem
except IntegrityError as error:
return JsonResponse({'Status': False, 'Errors': str(error)})
else:
- objects_created += 1
+ objects_created += 1 # увеличиваем счетчик
else:
@@ -335,60 +413,76 @@ def post(self, request, *args, **kwargs):
# удалить товары из корзины
def delete(self, request, *args, **kwargs):
"""
- Remove items from the user's basket.
+ Удалить товары из корзины пользователя (покупателя).
- Args:
- - request (Request): The Django request object.
+ Args:
+ - request (Request): The Django request object.
- Returns:
- - JsonResponse: The response indicating the status of the operation and any errors.
- """
- if not request.user.is_authenticated:
+ Returns:
+ - JsonResponse: The response indicating the status of the operation and any errors.
+ """
+ if not request.user.is_authenticated: # если пользователь не аутентифицирован выкидываем 403
return JsonResponse({'Status': False, 'Error': 'Log in required'}, status=403)
- items_sting = request.data.get('items')
- if items_sting:
- items_list = items_sting.split(',')
- basket, _ = Order.objects.get_or_create(user_id=request.user.id, state='basket')
- query = Q()
- objects_deleted = False
- for order_item_id in items_list:
- if order_item_id.isdigit():
- query = query | Q(order_id=basket.id, id=order_item_id)
- objects_deleted = True
-
- if objects_deleted:
- deleted_count = OrderItem.objects.filter(query).delete()[0]
+ items_sting = request.data.get('items') # из байт-строки в формате JSON получаем данные о товарах из запроса
+ if items_sting: # если данные о товарах указаны в запросе
+ items_list = items_sting.split(',') # сплитуем строку по разделителю "," получая на выходе список id товаров для удаления
+ # Получаем корзину пользователя
+ basket, _ = Order.objects.get_or_create(user_id=request.user.id, state='basket')
+ query = Q() # создаем пустое условие фильтрации.
+ objects_deleted = False # Флаг для отслеживания, были ли удалены товары.
+ for order_item_id in items_list: # для каждого id товара в списке
+ if order_item_id.isdigit(): # Проверяем, является ли идентификатор числом.
+ # Добавляем условие для удаления в запрос Q ...
+ query = query | Q(order_id=basket.id, id=order_item_id) # которое используется для фильтрации модели
+ # OrderItem по идентификатору заказа и идентификатору товара ниже.
+ # СПРАВКА
+ # Если в переменной items_list находятся значения [3, 5], и id корзины basket.id = 1,
+ # запрос query будет последовательно строиться так:
+ # После первого элемента: Q(order_id=1, id=3)
+ # После второго элемента: Q(order_id=1, id=3) | Q(order_id=1, id=5)
+ # Таким образом, в результате вы получите один объект Q,
+ # который соответствует всем указанным идентификаторам в корзине.
+
+ objects_deleted = True # Устанавливаем флаг, что у нас есть объекты для удаления.
+
+ if objects_deleted: # Если есть объекты для удаления, выполняем удаление.
+ deleted_count = OrderItem.objects.filter(query).delete()[0] # Удаляем объекты, соответствующие условию в query из OrderItem.
+ # СПРАВКА
+ # Метод delete() возвращает кортеж (количество удаленных записей, словарь, где ключами являются модели, а значениями — количество удаленных записей для каждой модели)
+ # мы используем [0], чтобы извлечь только первый элемент (количество удаленных объектов) из возвращаемого кортежа
+ # для формирования ответа пользователю
+
return JsonResponse({'Status': True, 'Удалено объектов': deleted_count})
return JsonResponse({'Status': False, 'Errors': 'Не указаны все необходимые аргументы'})
# добавить позиции в корзину
def put(self, request, *args, **kwargs):
"""
- Update the items in the user's basket.
+ Обновление товаров в корзине пользователя (покупателя).
- Args:
- - request (Request): The Django request object.
+ Args:
+ - request (Request): The Django request object.
- Returns:
- - JsonResponse: The response indicating the status of the operation and any errors.
- """
- if not request.user.is_authenticated:
+ Returns:
+ - JsonResponse: The response indicating the status of the operation and any errors.
+ """
+ if not request.user.is_authenticated: # если пользователь не аутентифицирован выкидываем 403
return JsonResponse({'Status': False, 'Error': 'Log in required'}, status=403)
- items_sting = request.data.get('items')
- if items_sting:
+ items_sting = request.data.get('items') # из байт-строки в формате JSON получаем данные о товарах из запроса
+ if items_sting: # если данные о товарах указаны в запросе
try:
- items_dict = load_json(items_sting)
+ items_dict = load_json(items_sting) # преобразуем в словарь
except ValueError:
return JsonResponse({'Status': False, 'Errors': 'Неверный формат запроса'})
else:
- basket, _ = Order.objects.get_or_create(user_id=request.user.id, state='basket')
- objects_updated = 0
- for order_item in items_dict:
- if type(order_item['id']) == int and type(order_item['quantity']) == int:
+ basket, _ = Order.objects.get_or_create(user_id=request.user.id, state='basket') # Получаем заказ пользователя
+ objects_updated = 0 # счетчик обновленных объектов
+ for order_item in items_dict: # для каждого товара в запросе
+ if type(order_item['id']) == int and type(order_item['quantity']) == int: # проверяем, что идентификатор и количество являются целыми числами
objects_updated += OrderItem.objects.filter(order_id=basket.id, id=order_item['id']).update(
- quantity=order_item['quantity'])
+ quantity=order_item['quantity']) # обновляем количество товара
return JsonResponse({'Status': True, 'Обновлено объектов': objects_updated})
return JsonResponse({'Status': False, 'Errors': 'Не указаны все необходимые аргументы'})
@@ -407,40 +501,50 @@ class PartnerUpdate(APIView):
def post(self, request, *args, **kwargs):
"""
- Update the partner price list information.
-
- Args:
- - request (Request): The Django request object.
-
- Returns:
- - JsonResponse: The response indicating the status of the operation and any errors.
- """
- if not request.user.is_authenticated:
- return JsonResponse({'Status': False, 'Error': 'Log in required'}, status=403)
+ Обновите информацию о прайс-листе партнёров.
- if request.user.type != 'shop':
- return JsonResponse({'Status': False, 'Error': 'Только для магазинов'}, status=403)
+ Args:
+ - request (Request): The Django request object.
- url = request.data.get('url')
- if url:
- validate_url = URLValidator()
+ Returns:
+ - JsonResponse: The response indicating the status of the operation and any errors.
+ """
+ if not request.user.is_authenticated: # если пользователь не аутентифицирован
+ return JsonResponse({'Status': False, 'Error': 'Log in required'}, status=403) # возвращаем ошибку 403 и сообщение о необходимости аутентификации
+
+ if request.user.type != 'shop': # если пользователь не магазин
+ return JsonResponse({'Status': False, 'Error': 'Только для магазинов'}, status=403) # возвращаем ошибку 403 и сообщение о необходимости быть магазином (назначается в админ панеле)
+ # Загружаем товары из .yml который размещен по адресу с url
+ url = request.data.get('url') # получаем url из запроса пользователя
+ if url: # если url указан
+ validate_url = URLValidator() # создаем валидатор для url
try:
- validate_url(url)
+ validate_url(url) # проверяем валидность url
except ValidationError as e:
- return JsonResponse({'Status': False, 'Error': str(e)})
+ return JsonResponse({'Status': False, 'Error': str(e)}, status=400) # возвращаем ошибку 400 и сообщение об ошибке валидации url
else:
- stream = get(url).content
-
- data = load_yaml(stream, Loader=Loader)
-
- shop, _ = Shop.objects.get_or_create(name=data['shop'], user_id=request.user.id)
- for category in data['categories']:
- category_object, _ = Category.objects.get_or_create(id=category['id'], name=category['name'])
- category_object.shops.add(shop.id)
- category_object.save()
- ProductInfo.objects.filter(shop_id=shop.id).delete()
- for item in data['goods']:
- product, _ = Product.objects.get_or_create(name=item['name'], category_id=item['category'])
+ stream = get(url).content # получаем контент из yaml-файла размещенного по некоторому url
+
+ data = load_yaml(stream, Loader=Loader) # декодируем контент и преобразуем в словарь
+
+ shop, _ = Shop.objects.get_or_create(name=data['shop'], user_id=request.user.id) # получаем или создаем магазин с названием из data['shop'] и авторизированным пользователем request.user.id
+
+ # ЗАМЕТКА:
+ # request:
+ # Это объект HttpRequest, который передается в представление в Django. Он содержит всю информацию о текущем запросе, включая данные о пользователе,
+ # пришедшие из HTTP-заголовков, параметры URL, данные и тд
+ # request.user:
+ # Это атрибут объекта request, который возвращает объект пользователя (класса User) для текущего запроса (если пользователь аутенифицирован is_authenticated = True).
+ # request.user.id:
+ # Это доступ к атрибуту id объекта пользователя. Он возвращает уникальный идентификатор пользователя в базе данных.
+
+ for category in data['categories']: # проходим по категориям в data['categories']
+ category_object, _ = Category.objects.get_or_create(id=category['id'], name=category['name']) # получаем или создаем категорию с id и name из category
+ category_object.shops.add(shop.id) # добавляем магазин в категорию
+ category_object.save() # сохраняем изменения в категории
+ ProductInfo.objects.filter(shop_id=shop.id).delete() # удаляем из прайса все загруженные в магазин shop.id товары
+ for item in data['goods']: # проходим по товарам в data['goods']
+ product, _ = Product.objects.get_or_create(name=item['name'], category_id=item['category']) # получаем или создаем продукт с названием item['name'] и категорией item['category']
product_info = ProductInfo.objects.create(product_id=product.id,
external_id=item['id'],
@@ -448,69 +552,70 @@ def post(self, request, *args, **kwargs):
price=item['price'],
price_rrc=item['price_rrc'],
quantity=item['quantity'],
- shop_id=shop.id)
- for name, value in item['parameters'].items():
- parameter_object, _ = Parameter.objects.get_or_create(name=name)
+ shop_id=shop.id) # создаем информацию о продукте
+ for name, value in item['parameters'].items(): # проходим по ключам и значениям в item['parameters']
+ parameter_object, _ = Parameter.objects.get_or_create(name=name) # получаем или создаем параметр товара с названием name
ProductParameter.objects.create(product_info_id=product_info.id,
parameter_id=parameter_object.id,
- value=value)
+ value=value) # создаем значение параметра товара
- return JsonResponse({'Status': True})
+ return JsonResponse({'Status': True}, status=200) # возвращаем сообщение об успешном обновлении прайса
- return JsonResponse({'Status': False, 'Errors': 'Не указаны все необходимые аргументы'})
+ return JsonResponse({'Status': False, 'Errors': 'Не указаны все необходимые аргументы'}, status=400) # возвращаем ошибку 400 и сообщение о необходимости указать все необходимые аргументы
class PartnerState(APIView):
"""
- A class for managing partner state.
+ Клас открытия/закрытия магазина.
- Methods:
- - get: Retrieve the state of the partner.
+ Methods:
+ - get: Retrieve the state of the partner.
- Attributes:
- - None
- """
+ Attributes:
+ - None
+ """
# получить текущий статус
def get(self, request, *args, **kwargs):
"""
- Retrieve the state of the partner.
+ Получить статус магазина.
- Args:
- - request (Request): The Django request object.
+ Args:
+ - request (Request): The Django request object.
- Returns:
- - Response: The response containing the state of the partner.
- """
+ Returns:
+ - Response: The response containing the state of the partner.
+ """
if not request.user.is_authenticated:
return JsonResponse({'Status': False, 'Error': 'Log in required'}, status=403)
if request.user.type != 'shop':
return JsonResponse({'Status': False, 'Error': 'Только для магазинов'}, status=403)
- shop = request.user.shop
- serializer = ShopSerializer(shop)
- return Response(serializer.data)
+ shop = request.user.shop # получаем магазин
+ serializer = ShopSerializer(shop) # сериализуем данные
+ return Response(serializer.data) # возвращаем статус магазина
# изменить текущий статус
def post(self, request, *args, **kwargs):
"""
- Update the state of a partner.
+ Обновить статус магазина.
- Args:
- - request (Request): The Django request object.
+ Args:
+ - request (Request): The Django request object.
- Returns:
- - JsonResponse: The response indicating the status of the operation and any errors.
- """
- if not request.user.is_authenticated:
+ Returns:
+ - JsonResponse: The response indicating the status of the operation and any errors.
+ """
+ if not request.user.is_authenticated: # если пользователь не аутентифицирован
return JsonResponse({'Status': False, 'Error': 'Log in required'}, status=403)
- if request.user.type != 'shop':
+ if request.user.type != 'shop': # Если пользователь не магазин
return JsonResponse({'Status': False, 'Error': 'Только для магазинов'}, status=403)
- state = request.data.get('state')
+ state = request.data.get('state') # получаем статус
if state:
try:
- Shop.objects.filter(user_id=request.user.id).update(state=strtobool(state))
+ # Обновляем статус магазина отфильтрованного по ID пользователя
+ Shop.objects.filter(user_id=request.user.id).update(state=strtobool(state)) # преобразует строку в логическое значение True или False
return JsonResponse({'Status': True})
except ValueError as error:
return JsonResponse({'Status': False, 'Errors': str(error)})
@@ -520,7 +625,7 @@ def post(self, request, *args, **kwargs):
class PartnerOrders(APIView):
"""
- Класс для получения заказов поставщиками
+ Класс для получения заказов поставщиками и изменения статуса готовности товара к выдачи
Methods:
- get: Retrieve the orders associated with the authenticated partner.
@@ -530,33 +635,124 @@ class PartnerOrders(APIView):
def get(self, request, *args, **kwargs):
"""
- Retrieve the orders associated with the authenticated partner.
+ Извлечь заказы, поступившие магазину от покупателя.
- Args:
- - request (Request): The Django request object.
+ Args:
+ - request (Request): The Django request object.
- Returns:
- - Response: The response containing the orders associated with the partner.
- """
- if not request.user.is_authenticated:
+ Returns:
+ - Response: The response containing the orders associated with the partner.
+ """
+ if not request.user.is_authenticated: # если пользователь не аутентифицирован
return JsonResponse({'Status': False, 'Error': 'Log in required'}, status=403)
- if request.user.type != 'shop':
+ if request.user.type != 'shop': # если пользователь не магазин
return JsonResponse({'Status': False, 'Error': 'Только для магазинов'}, status=403)
- order = Order.objects.filter(
+ order = Order.objects.filter( # Фильтруем заказы по ID пользователя (владельца магазина),
+ # исключая заказы со статусом "в корзине"
ordered_items__product_info__shop__user_id=request.user.id).exclude(state='basket').prefetch_related(
- 'ordered_items__product_info__product__category',
+ # Загружаем информацию о заказе, включая количество заказанных товаров и их цену и добавляем контакты для доставки
+ 'ordered_items__product_info__product__category',
'ordered_items__product_info__product_parameters__parameter').select_related('contact').annotate(
+ # Вычисляем общую сумму заказа
total_sum=Sum(F('ordered_items__quantity') * F('ordered_items__product_info__price'))).distinct()
+
+ # ПОМЕТКА! prefetch_related - выполняет отдельные запросы для основной модели и для связанных объектов. Затем результаты объединяются в Python.
+ # ПОМЕТКА! select_related - использует SQL JOIN для выполнения запроса и получения связанных объектов одновременно с основным объектом.
serializer = OrderSerializer(order, many=True)
return Response(serializer.data)
+ ######################## NEW NEW NEW ########################
+
+ def post(self, request, *args, **kwargs):
+ """
+ Изменение статуса заказа пользователем(магазином).
+
+ Args:
+ request (Request): The Django request object.
+
+ Returns:
+ JsonResponse: The response containing the status of the operation and any errors.
+
+ Raises:
+ HTTPError: If the request is invalid or the user is not authenticated or is not a shop.
+ """
+ if not request.user.is_authenticated:
+ return JsonResponse(
+ {'Status': False, 'Error': 'Log in required'},
+ status=status.HTTP_403_FORBIDDEN
+ )
+
+ if request.user.type != 'shop':
+ return JsonResponse(
+ {'Status': False, 'Error': 'Только для магазинов'},
+ status=status.HTTP_403_FORBIDDEN
+ )
+
+ if {'id', 'state'}.issubset(request.data):
+ order_id = request.data['id']
+ new_state = request.data['state']
+
+ # Проверка, что ID заказа является числом
+ if not str(order_id).isdigit():
+ return JsonResponse(
+ {'Status': False, 'Error': 'ID должен быть числом'},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # Поиск заказа, связанного с магазином пользователя
+ order = Order.objects.filter(
+ id=order_id, # по id заказа
+ ordered_items__product_info__shop__user_id=request.user.id # по пользователю(магазину)
+ ).first() # получаем первый найденный заказ соответствующий условиям (жадность)
+
+ if not order:
+ return JsonResponse(
+ {'Status': False, 'Error': 'Заказ с указанным ID не найден'},
+ status=status.HTTP_404_NOT_FOUND
+ )
+
+ # Проверка текущего статуса
+ if order.state == new_state:
+ return JsonResponse(
+ {'Status': False, 'Error': 'Статус заказа уже установлен'},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ # Проверка допустимости нового статуса
+ valid_states = dict(Order.STATE_CHOICES).keys()
+ if new_state not in valid_states:
+ return JsonResponse(
+ {'Status': False, 'Error': f'Некорректный статус заказа. Укажите значения из {dict(Order.STATE_CHOICES)}'},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ try:
+ # Обновление статуса заказа
+ order.state = new_state
+ order.save()
+ new_order.send(sender=self.__class__, user_id=request.user.id, order=order)
+ return JsonResponse({'Status': True, 'Message': f'Статус заказа успешно изменен на {dict(Order.STATE_CHOICES)[order.state]}'}, status=status.HTTP_200_OK)
+ except Exception as e:
+ return JsonResponse(
+ {'Status': False, 'Error': str(e)},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+ return JsonResponse(
+ {'Status': False, 'Error': 'Не указаны id и state'},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+
+ ######################### NEW NEW NEW ########################
+
+
class ContactView(APIView):
"""
- A class for managing contact information.
+ Класс для управления контактной информацией.
Methods:
- get: Retrieve the contact information of the authenticated user.
@@ -571,42 +767,42 @@ class ContactView(APIView):
# получить мои контакты
def get(self, request, *args, **kwargs):
"""
- Retrieve the contact information of the authenticated user.
+ Retrieve the contact information of the authenticated user.
- Args:
- - request (Request): The Django request object.
+ Args:
+ - request (Request): The Django request object.
- Returns:
- - Response: The response containing the contact information.
- """
- if not request.user.is_authenticated:
+ Returns:
+ - Response: The response containing the contact information.
+ """
+ if not request.user.is_authenticated: # если пользователь не аутентифицирован
return JsonResponse({'Status': False, 'Error': 'Log in required'}, status=403)
contact = Contact.objects.filter(
- user_id=request.user.id)
- serializer = ContactSerializer(contact, many=True)
+ user_id=request.user.id) # выбираем все контакты, связанные с аутентифицированным пользователем
+ serializer = ContactSerializer(contact, many=True) # сериализуем контакты (возвращает QuerySet с несколькими записями)
return Response(serializer.data)
# добавить новый контакт
def post(self, request, *args, **kwargs):
"""
- Create a new contact for the authenticated user.
+ Create a new contact for the authenticated user.
- Args:
- - request (Request): The Django request object.
+ Args:
+ - request (Request): The Django request object.
- Returns:
- - JsonResponse: The response indicating the status of the operation and any errors.
- """
- if not request.user.is_authenticated:
+ Returns:
+ - JsonResponse: The response indicating the status of the operation and any errors.
+ """
+ if not request.user.is_authenticated: # если пользователь не аутентифицирован
return JsonResponse({'Status': False, 'Error': 'Log in required'}, status=403)
- if {'city', 'street', 'phone'}.issubset(request.data):
- request.data._mutable = True
- request.data.update({'user': request.user.id})
+ if {'city', 'street', 'phone'}.issubset(request.data): # если есть все необходимые аргументы
+ request.data._mutable = True # изменяем мутабельность чтобы ...
+ request.data.update({'user': request.user.id}) # добавить в request.data новый ключ user, который содержит идентификатор аутентифицированного пользователя
serializer = ContactSerializer(data=request.data)
- if serializer.is_valid():
- serializer.save()
+ if serializer.is_valid(): # если данные валидны
+ serializer.save() # сохраняем контактные данные в базу данных
return JsonResponse({'Status': True})
else:
return JsonResponse({'Status': False, 'Errors': serializer.errors})
@@ -616,54 +812,55 @@ def post(self, request, *args, **kwargs):
# удалить контакт
def delete(self, request, *args, **kwargs):
"""
- Delete the contact of the authenticated user.
+ Delete the contact of the authenticated user.
- Args:
- - request (Request): The Django request object.
+ Args:
+ - request (Request): The Django request object.
- Returns:
- - JsonResponse: The response indicating the status of the operation and any errors.
- """
- if not request.user.is_authenticated:
+ Returns:
+ - JsonResponse: The response indicating the status of the operation and any errors.
+ """
+ if not request.user.is_authenticated: # если пользователь не аутентифицирован
return JsonResponse({'Status': False, 'Error': 'Log in required'}, status=403)
- items_sting = request.data.get('items')
- if items_sting:
- items_list = items_sting.split(',')
- query = Q()
- objects_deleted = False
- for contact_id in items_list:
- if contact_id.isdigit():
- query = query | Q(user_id=request.user.id, id=contact_id)
- objects_deleted = True
-
- if objects_deleted:
- deleted_count = Contact.objects.filter(query).delete()[0]
+ items_sting = request.data.get('items') # получаем список id контактов
+ if items_sting: # если список id контактов указан
+ items_list = items_sting.split(',') # сплитуем строку по разделителю "," получая на выходе список id контактов для удаления
+ query = Q() # создаем пустое условие фильтрации
+ objects_deleted = False # флаг удаленных объектов
+ for contact_id in items_list: # перебираем список id контактов
+ if contact_id.isdigit(): # если id контакта является числом
+ # Добавляем условие для удаления в запрос Q ...
+ query = query | Q(user_id=request.user.id, id=contact_id) # которое используется для фильтрации модели Contact
+ # по идентификатору пользователя и идентификатору контакта ниже.
+ objects_deleted = True # устанавливаем флаг удаленных объектов
+ if objects_deleted: # если True
+ deleted_count = Contact.objects.filter(query).delete()[0] # удаляем объекты по запросу query
return JsonResponse({'Status': True, 'Удалено объектов': deleted_count})
return JsonResponse({'Status': False, 'Errors': 'Не указаны все необходимые аргументы'})
# редактировать контакт
def put(self, request, *args, **kwargs):
- if not request.user.is_authenticated:
- """
- Update the contact information of the authenticated user.
+ """
+ Update the contact information of the authenticated user.
- Args:
- - request (Request): The Django request object.
+ Args:
+ - request (Request): The Django request object.
- Returns:
- - JsonResponse: The response indicating the status of the operation and any errors.
- """
+ Returns:
+ - JsonResponse: The response indicating the status of the operation and any errors.
+ """
+ if not request.user.is_authenticated:
return JsonResponse({'Status': False, 'Error': 'Log in required'}, status=403)
- if 'id' in request.data:
- if request.data['id'].isdigit():
- contact = Contact.objects.filter(id=request.data['id'], user_id=request.user.id).first()
- print(contact)
- if contact:
- serializer = ContactSerializer(contact, data=request.data, partial=True)
- if serializer.is_valid():
- serializer.save()
+ if 'id' in request.data: # если id контакта указан
+ if request.data['id'].isdigit(): # если id контакта является числом
+ contact = Contact.objects.filter(id=request.data['id'], user_id=request.user.id).first() # фильтруем контакты по заданному id
+ if contact: # если контакт найден
+ serializer = ContactSerializer(contact, data=request.data, partial=True) # обновляем контакт (data - данные для обновления,
+ # partial=True - не все поля необходимо передавать, и если некоторые поля отсутствуют, это не вызовет ошибку валидации)
+ if serializer.is_valid(): # если объект сериализаторы содержит валидные данные
+ serializer.save() # записываем данные в БД
return JsonResponse({'Status': True})
else:
return JsonResponse({'Status': False, 'Errors': serializer.errors})
@@ -673,7 +870,9 @@ def put(self, request, *args, **kwargs):
class OrderView(APIView):
"""
- Класс для получения и размешения заказов пользователями
+ Класс для получения и размешения заказов пользователями (покупателями)
+ и отправки уведомлений пользователям о статусе заказа.
+
Methods:
- get: Retrieve the details of a specific order.
- post: Create a new order.
@@ -687,52 +886,95 @@ class OrderView(APIView):
# получить мои заказы
def get(self, request, *args, **kwargs):
"""
- Retrieve the details of user orders.
+ Получить данные о заказах пользователя(покупателя).
- Args:
- - request (Request): The Django request object.
+ Args:
+ - request (Request): The Django request object.
- Returns:
- - Response: The response containing the details of the order.
- """
- if not request.user.is_authenticated:
+ Returns:
+ - Response: The response containing the details of the order.
+ """
+ if not request.user.is_authenticated: #
return JsonResponse({'Status': False, 'Error': 'Log in required'}, status=403)
+ # фильтруем заказы по id пользователя(покупателя)
+ # исключая из результата фильтрации заказы со статусом "в корзине"...
order = Order.objects.filter(
- user_id=request.user.id).exclude(state='basket').prefetch_related(
- 'ordered_items__product_info__product__category',
- 'ordered_items__product_info__product_parameters__parameter').select_related('contact').annotate(
- total_sum=Sum(F('ordered_items__quantity') * F('ordered_items__product_info__price'))).distinct()
+ user_id=request.user.id).exclude(state='basket').prefetch_related( # добавляем связанную предварительную выборку:
+ 'ordered_items__product_info__product__category', # заказанные_товары__информация_о_продукте__категория_продукта,
+ # заказанные_товары__информация_о_продукте__параметры_продукта_параметр
+ 'ordered_items__product_info__product_parameters__parameter').select_related('contact').annotate( # выбираем связанные контакты и добавляем аннотацию
+ total_sum=Sum(F('ordered_items__quantity') * F('ordered_items__product_info__price'))).distinct() # рассчитываем общую стоимость заказа,
+ # перемножая количество и цену товара и удаляем дубликаты из результата выборки
+ # ПОМЕТКА! prefetch_related - выполняет отдельные запросы для основной модели и для связанных объектов. Затем результаты объединяются в Python.
+ # ПОМЕТКА! select_related - использует SQL JOIN для выполнения запроса и получения связанных объектов одновременно с основным объектом.
serializer = OrderSerializer(order, many=True)
return Response(serializer.data)
- # разместить заказ из корзины
+ ######################### NEW NEW NEW ########################
+
+ # разместить новый заказ из корзины
def post(self, request, *args, **kwargs):
"""
- Put an order and send a notification.
-
- Args:
- - request (Request): The Django request object.
-
- Returns:
- - JsonResponse: The response indicating the status of the operation and any errors.
- """
- if not request.user.is_authenticated:
- return JsonResponse({'Status': False, 'Error': 'Log in required'}, status=403)
-
- if {'id', 'contact'}.issubset(request.data):
+ Разместить новый заказ из корзины и отправить уведомление магазину.
+ Args:
+ - request (Request): The Django request object.
+ Returns:
+ - JsonResponse: The response indicating the status of the operation and any errors.
+ """
+
+ if not request.user.is_authenticated: # проверяем токен аутентификации в запросе
+ return JsonResponse(
+ {'Status': False, 'Error': 'Log in required'},
+ status=status.HTTP_403_FORBIDDEN
+ )
+
+ if {'id', 'contact'}.issubset(request.data): # проверка наличия в запросе id заказа и контактных данных покупателя
if request.data['id'].isdigit():
try:
- is_updated = Order.objects.filter(
- user_id=request.user.id, id=request.data['id']).update(
- contact_id=request.data['contact'],
- state='new')
+ # Получаем объект заказа
+ order = Order.objects.get(
+ user_id=request.user.id, # по id покупателя
+ id=request.data['id'] # и по id заказа переданному в запросе
+ )
+
+ # ЗАМЕТКА!
+ # если вы уверены в единственности объекта (например, для `id` или уникальных ключей, как здесь),
+ # использовать `.get()` не только быстрее, но и легче интерпретировать
+ # https://docs.google.com/document/d/1_zO0NaMzGqY6ohgqYxi875pghBdFho9RvKxB5fhbjSc/edit?usp=sharing
+
+ # Обновляем поля объекта заказа
+ order.contact_id = request.data['contact']
+ order.state = 'new' # устанавливаем статус заказа с "в корзине" на "новый"
+ order.save() # сохраняем изменения в БД
+
+ # Отправляем сигнал с order (уведомление магазину от заказчика)
+ new_order.send(
+ sender=self.__class__, # отправляем от текущего класса
+ user_id=request.user.id, # по id покупателя
+ order=order # объект заказа
+ )
+ return JsonResponse({'Status': True}, status=status.HTTP_200_OK)
+
+ except Order.DoesNotExist:
+ return JsonResponse(
+ {'Status': False, 'Errors': 'Заказ не найден'},
+ status=status.HTTP_404_NOT_FOUND
+ )
except IntegrityError as error:
print(error)
- return JsonResponse({'Status': False, 'Errors': 'Неправильно указаны аргументы'})
- else:
- if is_updated:
- new_order.send(sender=self.__class__, user_id=request.user.id)
- return JsonResponse({'Status': True})
-
- return JsonResponse({'Status': False, 'Errors': 'Не указаны все необходимые аргументы'})
+ return JsonResponse(
+ {'Status': False, 'Errors': 'Неправильно указаны аргументы'},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+ except Exception as e:
+ return JsonResponse(
+ {'Status': False, 'Errors': str(e)},
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+ return JsonResponse(
+ {'Status': False, 'Errors': 'Не указаны все необходимые аргументы'},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+ ######################### NEW NEW NEW ########################
\ No newline at end of file
diff --git a/reference/netology_pd_diplom/docker-compose.yml b/reference/netology_pd_diplom/docker-compose.yml
new file mode 100644
index 000000000..462040662
--- /dev/null
+++ b/reference/netology_pd_diplom/docker-compose.yml
@@ -0,0 +1,87 @@
+services:
+
+ app:
+ build:
+ context: .
+ environment:
+ DEBUG: ${DEBUG}
+ SECRET_KEY: ${SECRET_KEY}
+ ALLOWED_HOSTS: ${ALLOWED_HOSTS}
+ DB_ENGINE: ${DB_ENGINE}
+ DB_NAME: ${DB_NAME}
+ EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD}
+ entrypoint: python manage.py runserver 0.0.0.0:8000
+ volumes:
+ - ./:/app
+ ports:
+ - "8000:8000" # Добавляем проброс порта
+ depends_on:
+ celery:
+ condition: service_started
+ broker:
+ condition: service_healthy
+ # db:
+ # condition: service_healthy
+ networks:
+ - backend-network
+
+ broker:
+ image: redis
+ ports:
+ - "6379:6379"
+ healthcheck:
+ test: ["CMD", "redis-cli", "ping"]
+ interval: 5s
+ timeout: 5s
+ retries: 3
+ networks:
+ - backend-network
+
+ celery:
+ build:
+ context: .
+ entrypoint: python -m celery -A netology_pd_diplom worker
+ volumes:
+ - ./:/app
+ depends_on:
+ # db:
+ # condition: service_healthy
+ broker:
+ condition: service_healthy
+ networks:
+ - backend-network
+
+networks:
+ backend-network:
+ driver: bridge
+
+
+ # broker:
+ # image: rabbitmq:4-management
+ # ports:
+ # - "5672:5672"
+ # - "15672:15672"
+ # environment:
+ # RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER}
+ # RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS}
+ # healthcheck:
+ # test: ["CMD", "rabbitmqctl", "status"]
+ # interval: 5s
+ # timeout: 5s
+ # retries: 3
+ # networks:
+ # - backend-network
+
+ # db:
+ # image: postgres:15.3
+ # ports:
+ # - "5432:5432"
+ # environment:
+ # POSTGRES_USER: ${POSTGRES_USER}
+ # POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
+ # POSTGRES_DB: ${POSTGRES_DB}
+
+ # healthcheck:
+ # test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
+ # networks:
+ # - backend-network
diff --git a/reference/netology_pd_diplom/netology_pd_diplom/__init__.py b/reference/netology_pd_diplom/netology_pd_diplom/__init__.py
index e69de29bb..478a0d658 100644
--- a/reference/netology_pd_diplom/netology_pd_diplom/__init__.py
+++ b/reference/netology_pd_diplom/netology_pd_diplom/__init__.py
@@ -0,0 +1,4 @@
+# Этот импорт гарантирует, что приложение Celery будет загружено при запуске Django
+from .celery import app as celery_app
+
+__all__ = ['celery_app']
diff --git a/reference/netology_pd_diplom/netology_pd_diplom/asgi.py b/reference/netology_pd_diplom/netology_pd_diplom/asgi.py
new file mode 100644
index 000000000..b3f700070
--- /dev/null
+++ b/reference/netology_pd_diplom/netology_pd_diplom/asgi.py
@@ -0,0 +1,16 @@
+"""
+ASGI config for django_celery_example project.
+
+It exposes the ASGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
+"""
+
+import os
+
+from django.core.asgi import get_asgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'netology_pd_diplom.settings')
+
+application = get_asgi_application()
\ No newline at end of file
diff --git a/reference/netology_pd_diplom/netology_pd_diplom/celery.py b/reference/netology_pd_diplom/netology_pd_diplom/celery.py
new file mode 100644
index 000000000..dc44fa033
--- /dev/null
+++ b/reference/netology_pd_diplom/netology_pd_diplom/celery.py
@@ -0,0 +1,27 @@
+# по материалам
+# https://realpython.com/asynchronous-tasks-with-django-and-celery/
+# https://stackabuse.com/asynchronous-tasks-in-django-with-redis-and-celery/
+
+import os
+from celery import Celery
+from celery.result import AsyncResult
+
+# Устанавливаем модуль настроек Django по умолчанию
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'netology_pd_diplom.settings')
+
+# Создаем экземпляр Celery
+app = Celery('netology_pd_diplom')
+
+# Загружаем настройки из Django
+app.config_from_object('django.conf:settings', namespace='CELERY')
+
+# Автоматическое обнаружение задач (tasks.py в каждом приложении)
+app.autodiscover_tasks()
+
+def get_result(task_id: str) -> AsyncResult:
+ """
+ Функция для получения результата асинхронной задачи
+ :param task_id: Идентификатор задачи
+ :return: Объект AsyncResult
+ """
+ return AsyncResult(task_id, app=app)
\ No newline at end of file
diff --git a/reference/netology_pd_diplom/netology_pd_diplom/settings.py b/reference/netology_pd_diplom/netology_pd_diplom/settings.py
index 6660e9906..f13d34c04 100644
--- a/reference/netology_pd_diplom/netology_pd_diplom/settings.py
+++ b/reference/netology_pd_diplom/netology_pd_diplom/settings.py
@@ -3,14 +3,32 @@
Generated by 'django-admin startproject' using Django 5.0.
-For more information on this file, see
-https://docs.djangoproject.com/en/2.2/topics/settings/
+Более подробную информацию об этом файле см. на странице
+https://docs.djangoproject.com/en/5.1/topics/settings/
-For the full list of settings and their values, see
-https://docs.djangoproject.com/en/2.2/ref/settings/
+Полный список настроек и их значений см. на странице
+https://docs.djangoproject.com/en/5.1/ref/settings/
"""
import os
+from dotenv import load_dotenv # pip install python-dotenv
+
+load_dotenv('.env')
+
+# Обновление сертификата SSL (для почтового клиента)
+import certifi
+
+# На macOS: Запустите скрипт установки сертификатов Python:
+# bash
+# /Applications/Python\ 3.10/Install\ Certificates.command
+# (Замените 3.10 на вашу версию Python, если отличается.)
+
+# Установите пакет certifi:
+# pip install certifi
+
+# В settings.py добавьте:
+# Путь к доверенным обновленным сертификатам
+os.environ['SSL_CERT_FILE'] = certifi.where() # Указываем Python использовать сертификаты из certifi
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -19,12 +37,14 @@
# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
-SECRET_KEY = '=hs6$#5om031nujz4staql9mbuste=!dc^6)4opsjq!vvjxzj@'
+SECRET_KEY = os.getenv('SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production!
-DEBUG = True
+DEBUG = os.getenv('DEBUG')
-ALLOWED_HOSTS = ['*']
+ALLOWED_HOSTS = [
+ os.getenv('ALLOWED_HOSTS'),
+]
# Application definition
@@ -38,7 +58,7 @@
'rest_framework',
'rest_framework.authtoken',
'django_rest_passwordreset',
- 'backend',
+ 'backend.apps.BackendConfig',
]
MIDDLEWARE = [
@@ -76,15 +96,19 @@
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
DATABASES = {
-
'default': {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
+ 'ENGINE': os.getenv('DB_ENGINE'),
+ 'NAME': os.path.join(f"{BASE_DIR}", f"{os.getenv('DB_NAME')}")
}
-
-
}
+# DATABASES = {
+# 'default': {
+# 'ENGINE': 'django.db.backends.sqlite3',
+# 'NAME': BASE_DIR / 'db.sqlite3',
+# }
+# }
+
# Password validation
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
@@ -106,7 +130,9 @@
# Internationalization
# https://docs.djangoproject.com/en/2.2/topics/i18n/
-LANGUAGE_CODE = 'en-us'
+LANGUAGE_CODE = 'ru' # Язык по умолчанию
+
+USE_I18N = True # Включение интернационализации
TIME_ZONE = 'UTC'
@@ -125,14 +151,43 @@
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
# EMAIL_USE_TLS = True
+# EMAIL_HOST = 'smtp.mail.ru'
+# EMAIL_HOST_USER = 'netology.diplom@mail.ru'
+# EMAIL_HOST_PASSWORD = 'CLdm7yW4U9nivz9mbexu'
+# EMAIL_PORT = '465'
+# EMAIL_USE_SSL = True
+# SERVER_EMAIL = EMAIL_HOST_USER
+
+# # Настройки для mail.ru, справка https://help.mail.ru/mail/security/protection/external/
+EMAIL_HOST = 'smtp.mail.ru' # SMTP-сервер
+EMAIL_HOST_USER = 'festchuk@mail.ru'
+EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD') # Пароль приложения
+EMAIL_PORT = '465' # Для SSL (или 587 для TLS)
+EMAIL_USE_SSL = True # Для порта 465 (если используете 587 (более безопасно) → EMAIL_USE_TLS = True)
+SERVER_EMAIL = EMAIL_HOST_USER # Для отправки системных писем
+
+
+# # Проверка доступа к SMTP
+# # python manage.py shell
+
+# from django.core.mail import send_mail
+# from django.conf import settings
+
+# # Проверка параметров
+# print("EMAIL_HOST:", settings.EMAIL_HOST)
+# print("EMAIL_PORT:", settings.EMAIL_PORT)
+# print("EMAIL_HOST_USER:", settings.EMAIL_HOST_USER)
+
+# # Тест отправки
+# send_mail(
+# 'Тест аутентификации',
+# 'Проверка доступа к SMTP.',
+# settings.EMAIL_HOST_USER,
+# ['dilmah949dilma@gmail.com'], # Список рассылки тестового письма
+# fail_silently=False,
+# )
-EMAIL_HOST = 'smtp.mail.ru'
-EMAIL_HOST_USER = 'netology.diplom@mail.ru'
-EMAIL_HOST_PASSWORD = 'CLdm7yW4U9nivz9mbexu'
-EMAIL_PORT = '465'
-EMAIL_USE_SSL = True
-SERVER_EMAIL = EMAIL_HOST_USER
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
@@ -141,14 +196,25 @@
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
'rest_framework.renderers.BrowsableAPIRenderer',
-
- ),
+ ), # Классы рендереров
'DEFAULT_AUTHENTICATION_CLASSES': (
-
'rest_framework.authentication.TokenAuthentication',
- ),
-
+ ), # Аутентификация по токену
}
-DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
+DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
+
+
+# Celery
+# Необходимо использовать имя контейнера Redis в качестве адреса для подключения, а не localhost.
+# Например, если Redis запущен в контейнере с именем broker, то адрес должен выглядеть так:
+CELERY_BROKER_URL = 'redis://localhost:6379' # Бекенд задач (Если запускаем через docker-compose 'redis://broker:6379'
+ # если локально то 'redis://localhost:6379')
+CELERY_RESULT_BACKEND = 'redis://localhost:6379' # Бекенд результатов
+CELERY_ACCEPT_CONTENT = ['application/json'] # Допустимый формат
+CELERY_RESULT_SERIALIZER = 'json' # Сериализатор результатов
+CELERY_TASK_SERIALIZER = 'json' # Сериализатор задач
+# В логе указано предупреждение о том, что настройка broker_connection_retry устареет в Celery 6.0.
+# Чтобы отключить это, в конфиг добавляется:
+CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True
diff --git a/reference/netology_pd_diplom/pytest.ini b/reference/netology_pd_diplom/pytest.ini
new file mode 100644
index 000000000..3be1f2837
--- /dev/null
+++ b/reference/netology_pd_diplom/pytest.ini
@@ -0,0 +1,4 @@
+[pytest]
+DJANGO_SETTINGS_MODULE = netology_pd_diplom.settings
+python_files = tests.py test_*.py *_tests.py
+addopts = --reuse-db
\ No newline at end of file
diff --git a/reference/netology_pd_diplom/requirements.txt b/reference/netology_pd_diplom/requirements.txt
index 9f6ea3cb7..201aaae32 100644
--- a/reference/netology_pd_diplom/requirements.txt
+++ b/reference/netology_pd_diplom/requirements.txt
@@ -1,7 +1,85 @@
-django~=5.0
-djangorestframework~=3.14.0
-celery~=5.3.0
-requests~=2.31.0
-ujson~=5.9.0
-pyyaml~=6.0.0
-django-rest-passwordreset>=1.3.0
+amqp==5.3.1
+asgiref==3.8.1
+async-timeout==5.0.1
+billiard==4.2.1
+celery==5.3.6
+certifi==2025.1.31
+charset-normalizer==3.4.1
+click==8.1.8
+click-didyoumean==0.3.1
+click-plugins==1.1.1
+click-repl==0.3.0
+Django==5.1.6
+django-extensions==3.2.3
+django-rest-passwordreset==1.5.0
+djangorestframework==3.14.0
+exceptiongroup==1.2.2
+idna==3.10
+iniconfig==2.0.0
+kombu==5.4.2
+model-bakery==1.20.3
+packaging==24.2
+pluggy==1.5.0
+prompt_toolkit==3.0.50
+pytest==8.3.4
+pytest-django==4.10.0
+pytest-mock==3.14.0
+python-dateutil==2.9.0.post0
+python-dotenv==1.0.1
+pytz==2025.1
+PyYAML==6.0.2
+redis==5.2.1
+requests==2.32.3
+requests-mock==1.12.1
+six==1.17.0
+sqlparse==0.5.3
+tomli==2.2.1
+typing_extensions==4.12.2
+tzdata==2025.1
+ujson==5.9.0
+urllib3==2.3.0
+vine==5.1.0
+wcwidth==0.2.13
+amqp==5.3.1
+asgiref==3.8.1
+async-timeout==5.0.1
+billiard==4.2.1
+celery==5.3.6
+certifi==2025.1.31
+charset-normalizer==3.4.1
+click==8.1.8
+click-didyoumean==0.3.1
+click-plugins==1.1.1
+click-repl==0.3.0
+coverage==7.8.0
+Django==5.1.6
+django-extensions==3.2.3
+django-rest-passwordreset==1.5.0
+djangorestframework==3.14.0
+exceptiongroup==1.2.2
+idna==3.10
+iniconfig==2.0.0
+kombu==5.4.2
+model-bakery==1.20.3
+packaging==24.2
+pluggy==1.5.0
+prompt_toolkit==3.0.50
+pytest==8.3.4
+pytest-django==4.10.0
+pytest-mock==3.14.0
+python-dateutil==2.9.0.post0
+python-dotenv==1.0.1
+pytz==2025.1
+PyYAML==6.0.2
+redis==5.2.1
+requests==2.32.3
+requests-mock==1.12.1
+six==1.17.0
+sqlparse==0.5.3
+tomli==2.2.1
+typing_extensions==4.12.2
+tzdata==2025.1
+ujson==5.9.0
+urllib3==2.3.0
+vine==5.1.0
+wcwidth==0.2.13
diff --git a/reference/netology_pd_diplom/tests/README.md b/reference/netology_pd_diplom/tests/README.md
new file mode 100644
index 000000000..49e80ff92
--- /dev/null
+++ b/reference/netology_pd_diplom/tests/README.md
@@ -0,0 +1,72 @@
+# Тестирование
+#### *Тесты сфокусированны на проверке API, а модели проверяются косвенно (т.е. в текущих тестах проверяется интеграция — например, при добавлении товаров в корзину создаётся OrderItem, но нет юнит-тестов конкретно для модели OrderItem).*
+---
+#### Чтобы запустить подготовленные тесты, выполните следующие шаги:
+---
+
+1. Установите необходимые зависимости
+```bash
+pip install pytest pytest-django model_bakery requests-mock pytest-mock
+```
+2. Настройте окружение
+Создайте файл `pytest.ini` в корне проекта:
+```ini
+[pytest]
+DJANGO_SETTINGS_MODULE = your_project.settings
+python_files = tests.py test_*.py *_tests.py
+addopts = --reuse-db
+```
+3. Разместите тесты
+Создайте структуру директорий:
+```
+your_app/
+├── tests/
+│ ├── __init__.py
+│ └── test_api.py # файл с тестами
+```
+4. Запуск тестов
+Основные команды:
+```bash
+# Все тесты
+pytest your_app/tests/ -v
+
+# Конкретный тестовый класс
+pytest your_app/tests/test_api.py::TestRegisterAccount -v
+
+# Конкретный тестовый метод
+pytest your_app/tests/test_api.py::TestRegisterAccount::test_register_success -v
+
+# Запись лога
+pytest your_app/tests/test_api.py::TestRegisterAccount::test_register_success -v --log-cli-level=INFO
+```
+5. Пример вывода при успешном запуске
+```bash
+============================= test session starts ==============================
+collected 9 items
+
+your_app/tests/test_api.py::TestRegisterAccount::test_register_missing_fields PASSED
+your_app/tests/test_api.py::TestRegisterAccount::test_register_invalid_password PASSED
+your_app/tests/test_api.py::TestRegisterAccount::test_register_success PASSED
+...
+============================== 9 passed in 2.15s ===============================
+```
+6. Возможные проблемы и решения
+Проблема: База данных не создается
+Решение: Добавьте флаг при первом запуске:
+
+```bash
+pytest --create-db
+````
+Проблема: Ошибки с миграциями
+Решение: Выполните миграции перед тестами:
+```bash
+python manage.py migrate
+```
+7. Дополнительные настройки
+Для детализации вывода используйте:
+```bash
+pytest -v # Подробный вывод
+pytest --cov # Покрытие кода
+pytest -x # Остановка при первой ошибке
+pytest --ff # Запуск сначала проваленных тестов
+```
\ No newline at end of file
diff --git a/reference/netology_pd_diplom/tests/__init__.py b/reference/netology_pd_diplom/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/reference/netology_pd_diplom/tests/log.txt b/reference/netology_pd_diplom/tests/log.txt
new file mode 100644
index 000000000..fdf5640e4
--- /dev/null
+++ b/reference/netology_pd_diplom/tests/log.txt
@@ -0,0 +1,16 @@
+============================= test session starts ==============================
+platform darwin -- Python 3.10.7, pytest-8.3.4, pluggy-1.5.0 -- /Users/apple/Documents/Full-stack-Python-developer/my_diplom/venv/bin/python3
+cachedir: .pytest_cache
+django: version: 5.1.6, settings: netology_pd_diplom.settings (from ini)
+rootdir: /Users/apple/Documents/Full-stack-Python-developer/my_diplom/python-final-diplom/reference/netology_pd_diplom
+configfile: pytest.ini
+plugins: django-4.10.0, mock-3.14.0, requests-mock-1.12.1
+collecting ... collected 5 items
+
+tests/test_api.py::TestPartnerUpdate::test_partner_update_permission PASSED [ 20%]
+tests/test_api.py::TestPartnerUpdate::test_unauthenticated_user PASSED [ 40%]
+tests/test_api.py::TestPartnerUpdate::test_partner_update_invalid_url PASSED [ 60%]
+tests/test_api.py::TestPartnerUpdate::test_partner_update_missing_url PASSED [ 80%]
+tests/test_api.py::TestPartnerUpdate::test_successful_update PASSED [100%]
+
+============================== 5 passed in 1.37s ===============================
diff --git a/reference/netology_pd_diplom/tests/test_api.py b/reference/netology_pd_diplom/tests/test_api.py
new file mode 100644
index 000000000..91af2b832
--- /dev/null
+++ b/reference/netology_pd_diplom/tests/test_api.py
@@ -0,0 +1,345 @@
+# Тесты сфокусированны на проверке API, а модели проверяются
+# косвенно (т.е. в текущих тестах проверяется интеграция — например,
+# при добавлении товаров в корзину создаётся OrderItem,
+# но нет юнит-тестов конкретно для модели OrderItem).
+
+import json # для работы с JSON
+import pytest # для написания тестов
+from django.urls import reverse # для работы с пространством имен
+# APIClient - тестовый клиент DRF, который позволяет имитировать
+# HTTP-запросы к разработанному API в тестах
+from rest_framework.test import APIClient
+from django.contrib.auth import get_user_model # для получения модели пользователя
+from model_bakery import baker # для создания тестовых данных
+from unittest.mock import patch # для работы с моками
+from backend.models import Shop, Category, Product, ProductInfo, Order, Contact, ConfirmEmailToken
+
+User = get_user_model() # получаем модель пользователя
+
+# Фикстура для клиента API
+@pytest.fixture
+def client():
+ """
+ Фикстура для клиента API.
+ """
+ return APIClient() # клиент для тестирования API
+
+# Общая фикстура для аутентифицированного пользователя
+@pytest.fixture
+def authenticated_user(client):
+ """
+ Фикстура для аутентифицированного пользователя.
+ """
+ user = baker.make(User) # создаем тестового пользователя
+ client.force_authenticate(user=user) # Принудительная аутентификация пользователя
+ return user
+
+# Тесты для RegisterAccount
+@pytest.mark.django_db
+class TestRegisterAccount:
+ """
+ Класс для тестирования регистрации пользователя.
+ """
+ def test_register_missing_fields(self, client):
+ """
+ Проверяем, что регистрация пользователя не проходит, если не указаны все обязательные поля.
+ """
+ url = reverse('backend:user-register') # получаем url эндпоинта RegisterAccount из пространства имен
+ response = client.post(url, {}) # отправляем POST-запрос
+ assert response.status_code == 400 # проверка статуса
+ assert 'Errors' in response.json() # проверяем, что в ответе есть ключ 'Errors'
+
+ def test_register_invalid_password(self, client):
+ """
+ Проверяем, что регистрация пользователя не проходит, если пароль невалиден.
+ """
+ data = {
+ 'first_name': 'Test',
+ 'last_name': 'User',
+ 'email': 'test@test.com',
+ 'password': '123',
+ 'company': 'TestCo',
+ 'position': 'Manager'
+ } # создаем словарь с невалидным паролем
+ url = reverse('backend:user-register') # получаем url эндпоинта RegisterAccount из пространства имен
+ response = client.post(url, data) # отправляем POST-запрос
+ assert response.status_code == 400 # проверка статуса
+ assert 'password' in response.json()['Errors'] # проверяем, что в ответе есть ключ 'password' внутри ключа 'Errors'
+
+ def test_register_success(self, client):
+ """
+ Проверяем, что регистрация пользователя проходит успешно.
+ """
+ data = {
+ 'first_name': 'Test',
+ 'last_name': 'User',
+ 'email': 'test@test.com',
+ 'password': 'TestPass123!',
+ 'company': 'TestCo',
+ 'position': 'Manager'
+ } # создаем словарь с валидным паролем
+ url = reverse('backend:user-register')
+ response = client.post(url, data)
+ assert response.status_code == 200
+ assert response.json()['Status'] is True # проверяем, что в ответе есть ключ 'Status' со значением True
+
+# Тесты для ConfirmAccount
+@pytest.mark.django_db # декоратор для работы с базой данных
+class TestConfirmAccount:
+ """
+ Класс для тестирования подтверждения учетной записи.
+ """
+ def test_confirm_invalid_token(self, client):
+ """
+ Проверяем, что подтверждение учетной записи не проходит, если указан невалидный токен.
+ """
+ user = baker.make(User, email='test@test.com', is_active=False) # создаем тестового пользователя
+ baker.make(ConfirmEmailToken, user=user) # создаем токен
+ url = reverse('backend:user-register-confirm') # получаем url эндпоинта ConfirmAccount из пространства имен
+ data = {'email': 'test@test.com', 'token': 'wrong_token'} # создаем словарь с невалидным токеном
+ response = client.post(url, data)
+ assert response.status_code == 400
+ assert 'Errors' in response.json() # проверяем, что в ответе есть ключ 'Errors'
+
+ def test_confirm_success(self, client):
+ """
+ Проверяем, что подтверждение учетной записи проходит успешно.
+ """
+ user = baker.make(User, email='test@test.com', is_active=False) # создаем тестового пользователя
+ token = baker.make(ConfirmEmailToken, user=user) # создаем токен
+ url = reverse('backend:user-register-confirm') # получаем url эндпоинта ConfirmAccount из пространства имен
+ data = {'email': 'test@test.com', 'token': token.key} # создаем словарь с валидным токеном
+ response = client.post(url, data)
+ user.refresh_from_db() # обновляем данные пользователя из базы данных
+ assert response.status_code == 200
+ assert user.is_active is True # проверяем, что пользователь активирован (поле is_active установлено в True)
+
+# Тесты для LoginAccount
+@pytest.mark.django_db
+class TestLoginAccount:
+ """
+ Класс для тестирования входа в систему.
+ """
+ def test_login_invalid_credentials(self, client):
+ """
+ Проверяем, что вход в систему не проходит, если указаны невалидные учетные данные.
+ """
+ user = baker.make(User, email='test@test.com', password='TestPass123!', is_active=True) # создаем тестового пользователя (для авторизации он должен быть активирован)
+ user.set_password(user.password) # устанавливаем пароль
+ user.save() # сохраняем его в базе данных
+ url = reverse('backend:user-login') # получаем url эндпоинта LoginAccount из пространства имен
+ data = {'email': 'test@test.com', 'password': 'WrongPass'} # создаем словарь с невалидным паролем
+ response = client.post(url, data)
+ assert response.status_code == 400
+ assert 'Errors' in response.json() # проверяем, что в ответе есть ключ 'Errors'
+
+ def test_login_success(self, client):
+ """
+ Проверяем, что вход в систему проходит успешно.
+ """
+ user = baker.make(User, email='test@test.com', password='TestPass123!', is_active=True)
+ user.set_password(user.password)
+ user.save()
+ url = reverse('backend:user-login')
+ data = {'email': 'test@test.com', 'password': 'TestPass123!'} # создаем словарь с валидным паролем
+ response = client.post(url, data)
+ assert response.status_code == 200
+ assert 'Token' in response.json() # проверяем, что в ответе есть ключ 'Token'
+
+# Тесты для PartnerUpdate
+@pytest.mark.django_db
+class TestPartnerUpdate:
+ """
+ Класс для тестирования обновления партнера.
+ """
+ def test_partner_update_permission(self, client):
+ """
+ Проверяем, что обновление партнера доступно только для магазинов.
+ """
+ user = baker.make(User, type='buyer') # назначаем пользователю тип 'buyer' - покупатель
+ client.force_authenticate(user=user) # Принудительная аутентификация пользователя
+ url = reverse('backend:partner-update') # получаем url эндпоинта PartnerUpdate
+ response = client.post(url, {}) # отправляем POST-запрос
+ assert response.status_code == 403
+ assert response.json()['Error'] == 'Только для магазинов' # проверяем, что в ответе есть ключ 'Error' со значением 'Только для магазинов'
+
+ def test_unauthenticated_user(self, client):
+ """
+ Проверяем, что обновление партнера доступно только для
+ зарегистрированных пользователей.
+ """
+ url = reverse('backend:partner-update')
+ response = client.post(url, {})
+ assert response.status_code == 403
+ assert response.json()['Error'] == 'Log in required' # проверяем, что в ответе есть ключ 'Error' со значением 'Требуется вход'
+
+ def test_partner_update_invalid_url(self, client):
+ """
+ Проверяем, что обновление партнера доступно только для
+ зарегистрированных пользователей с правильными учетными данными.
+ """
+ user = baker.make(User, type='shop') # назначаем пользователю тип 'shop' - магазин
+ client.force_authenticate(user=user) # Принудительная аутентификация пользователя
+ url = reverse('backend:partner-update') # получаем url эндпоинта PartnerUpdate
+ data = {'url': 'invalid-url'} # создаем словарь с невалидным url
+ response = client.post(url, data) # отправляем POST-запрос
+ assert response.status_code == 400
+ assert 'Error' in response.json() # проверяем, что в ответе есть ключ 'Error'
+
+ def test_partner_update_missing_url(self, client):
+ """
+ Проверяем, что обновление партнера доступно только для
+ зарегистрированных пользователей с полными учетными данными.
+ """
+ user = baker.make(User, type='shop')
+ client.force_authenticate(user=user) # Принудительная аутентификация пользователя
+ url = reverse('backend:partner-update')
+ response = client.post(url, {})
+ assert response.status_code == 400
+ assert response.json()['Errors'] == 'Не указаны все необходимые аргументы'
+
+ @patch('backend.views.get') # подменяем метод `requests.get` с помощью Mock для изоляции теста от реальных HTTP-запросов
+ @patch('backend.views.load_yaml') # подменяем функцию `load_yaml`, которая парсит YAML-файлы
+ def test_successful_update(self, mock_load_yaml, mock_get, client):
+ """
+ Проверяем, что обновление партнера проходит успешно.
+ mock_load_yaml - мокирование функции `load_yaml`, которая парсит YAML-файлы
+ mock_get - мокирование метода `requests.get`, чтобы вернуть заранее заданные данные
+ """
+ user = baker.make(User, type='shop') # назначаем пользователю тип 'shop' - магазин
+ client.force_authenticate(user=user) # принудительная аутентификация пользователя
+ mock_load_yaml.return_value = {
+ 'shop': 'Test Shop',
+ 'categories': [{'id': 1, 'name': 'Category 1'}],
+ 'goods': [{
+ 'id': 1,
+ 'name': 'Test Product',
+ 'model': 'Model X',
+ 'category': 1,
+ 'price': 100,
+ 'price_rrc': 150,
+ 'quantity': 10,
+ 'parameters': {'color': 'red'}
+ }]
+ } # заранее задаем данные
+ mock_get.return_value.content = b'mock-content' # мокирование данных, которые возвращаются как байтовая строка
+ url = reverse('backend:partner-update') # получаем url эндпоинта PartnerUpdate
+ data = {'url': 'http://example.com/test.yml'} # мокированный URL прайс-листа
+ response = client.post(url, data, format='json') # отправляем POST-запрос
+ assert response.status_code == 200
+ assert response.json()['Status'] is True # проверяем, что в ответе есть ключ 'Status' со значением True
+
+# Тесты для BasketView
+@pytest.mark.django_db
+class TestBasketView:
+ """
+ Класс для тестирования BasketView (корзина).
+ """
+ @pytest.fixture
+ def setup_data(self):
+ """
+ Фикстура для создания необходимых данных для тестирования.
+ """
+ user = baker.make(User) # создаем тестового пользователя
+ category = baker.make(Category) # создаем тестовую категорию
+ shop = baker.make(Shop) # создаем тестовый магазин
+ product = baker.make(Product, category=category) # создаем тестовый товар
+ product_info = baker.make(ProductInfo, product=product, shop=shop) # создаем тестовую информацию о продукте
+ return user, product_info # возвращаем созданные данные
+
+ def test_basket_access_unauthorized(self, client):
+ """
+ Проверяем доступ к корзине для неавторизованных пользователей.
+ """
+ url = reverse('backend:basket')
+ response = client.get(url)
+ assert response.status_code == 403
+ assert response.json()['Error'] == 'Log in required'
+
+ def test_basket_add_items(self, client, setup_data):
+ """
+ Проверяем добавление товаров в корзину для авторизованных пользователей.
+ """
+ user, product_info = setup_data # получаем данные из фикстуры
+ client.force_authenticate(user=user) # аутентификация пользователя
+ payload = {"items": json.dumps([{"product_info": product_info.id, "quantity": 2}])} # подготавливаем данные для запроса в формате JSON
+ response = client.post(reverse('backend:basket'), data=payload, format='json') # отправляем POST-запрос
+ assert response.status_code == 200
+ assert response.json() == {'Status': True, 'Создано объектов': 1} # проверяем, что в ответе есть ключ 'Status' со значением True и ключ 'Создано объектов' со значением 1
+
+# Тесты для OrderView
+@pytest.mark.django_db
+class TestOrderView:
+ """
+ Класс для тестирования OrderView (заказы).
+ """
+ @pytest.fixture
+ def setup_order(self):
+ """
+ Фикстура для создания необходимых данных для тестирования.
+ """
+ user = baker.make(User) # создаем тестового пользователя
+ contact = baker.make(Contact, user=user) # создаем тестовый контакт
+ order = baker.make(Order, user=user, state='basket') # создаем тестовый заказ
+ return user, contact, order
+
+ def test_order_create_success(self, client, setup_order):
+ """
+ Проверяем успешное создание заказа.
+ """
+ user, contact, order = setup_order # получаем данные из фикстуры
+ client.force_authenticate(user=user) # аутентификация пользователя
+ response = client.post(
+ reverse('backend:order'),
+ {'id': str(order.id), 'contact': contact.id},
+ format='json'
+ ) # отправляем POST-запрос с данными об заказе и контакте
+ assert response.status_code == 200
+ assert response.json() == {'Status': True} # проверяем, что в ответе есть ключ 'Status' со значением True
+
+ def test_order_create_not_found(self, client):
+ """
+ Проверяем, что заказ отсутствует в базе данных при неправильном ID для поиска.
+ """
+ user = baker.make(User) # создаем тестового пользователя
+ contact = baker.make(Contact, user=user) # создаем тестовый контакт
+ client.force_authenticate(user=user) # аутентификация пользователя
+ response = client.post(
+ reverse('backend:order'),
+ {'id': '999', 'contact': contact.id},
+ format='json'
+ ) # отправляем POST-запрос с неправильным ID заказа
+ assert response.status_code == 404
+ assert response.json() == {'Status': False, 'Errors': 'Заказ не найден'} # проверяем, что в ответе есть ключ 'Status' со значением False и ключ 'Errors' со значением 'Заказ не найден'
+
+ def test_order_create_unauthenticated(self, client):
+ """
+ Проверяем, что заказ не создается, если пользователь не аутентифицирован.
+ """
+ response = client.post(reverse('backend:order'), {})
+ assert response.status_code == 403
+ assert response.json() == {'Status': False, 'Error': 'Log in required'} # проверяем, что в ответе есть ключ 'Status' со значением False и ключ 'Error' со значением 'Log in required'
+
+ def test_order_create_invalid_data(self, client):
+ """
+ Проверяем, что заказ не создается с невалидными данными.
+ """
+ user = baker.make(User) # создаем тестового пользователя
+ client.force_authenticate(user=user) # аутентификация пользователя
+ response = client.post(
+ reverse('backend:order'),
+ {'id': 'invalid_id', 'contact': 1},
+ format='json'
+ ) # отправляем POST-запрос с невалидными данными
+ assert response.status_code == 400
+ assert 'Errors' in response.json() # проверяем, что в ответе есть ключ 'Errors'
+
+ def test_missing_required_fields(self, client):
+ """
+ Проверяем, что заказ не создается, если не указаны все необходимые аргументы.
+ """
+ user = baker.make(User) # создаем тестового пользователя
+ client.force_authenticate(user=user) # аутентификация пользователя
+ response = client.post(reverse('backend:order'), {}, format='json') # отправляем POST-запрос без необходимых аргументов
+ assert response.status_code == 400
+ assert response.json() == {'Status': False, 'Errors': 'Не указаны все необходимые аргументы'}
diff --git a/requirements.txt b/requirements.txt
index 9f6ea3cb7..201aaae32 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,85 @@
-django~=5.0
-djangorestframework~=3.14.0
-celery~=5.3.0
-requests~=2.31.0
-ujson~=5.9.0
-pyyaml~=6.0.0
-django-rest-passwordreset>=1.3.0
+amqp==5.3.1
+asgiref==3.8.1
+async-timeout==5.0.1
+billiard==4.2.1
+celery==5.3.6
+certifi==2025.1.31
+charset-normalizer==3.4.1
+click==8.1.8
+click-didyoumean==0.3.1
+click-plugins==1.1.1
+click-repl==0.3.0
+Django==5.1.6
+django-extensions==3.2.3
+django-rest-passwordreset==1.5.0
+djangorestframework==3.14.0
+exceptiongroup==1.2.2
+idna==3.10
+iniconfig==2.0.0
+kombu==5.4.2
+model-bakery==1.20.3
+packaging==24.2
+pluggy==1.5.0
+prompt_toolkit==3.0.50
+pytest==8.3.4
+pytest-django==4.10.0
+pytest-mock==3.14.0
+python-dateutil==2.9.0.post0
+python-dotenv==1.0.1
+pytz==2025.1
+PyYAML==6.0.2
+redis==5.2.1
+requests==2.32.3
+requests-mock==1.12.1
+six==1.17.0
+sqlparse==0.5.3
+tomli==2.2.1
+typing_extensions==4.12.2
+tzdata==2025.1
+ujson==5.9.0
+urllib3==2.3.0
+vine==5.1.0
+wcwidth==0.2.13
+amqp==5.3.1
+asgiref==3.8.1
+async-timeout==5.0.1
+billiard==4.2.1
+celery==5.3.6
+certifi==2025.1.31
+charset-normalizer==3.4.1
+click==8.1.8
+click-didyoumean==0.3.1
+click-plugins==1.1.1
+click-repl==0.3.0
+coverage==7.8.0
+Django==5.1.6
+django-extensions==3.2.3
+django-rest-passwordreset==1.5.0
+djangorestframework==3.14.0
+exceptiongroup==1.2.2
+idna==3.10
+iniconfig==2.0.0
+kombu==5.4.2
+model-bakery==1.20.3
+packaging==24.2
+pluggy==1.5.0
+prompt_toolkit==3.0.50
+pytest==8.3.4
+pytest-django==4.10.0
+pytest-mock==3.14.0
+python-dateutil==2.9.0.post0
+python-dotenv==1.0.1
+pytz==2025.1
+PyYAML==6.0.2
+redis==5.2.1
+requests==2.32.3
+requests-mock==1.12.1
+six==1.17.0
+sqlparse==0.5.3
+tomli==2.2.1
+typing_extensions==4.12.2
+tzdata==2025.1
+ujson==5.9.0
+urllib3==2.3.0
+vine==5.1.0
+wcwidth==0.2.13