├── .gitignore ├── README.md ├── data └── shop1.yaml ├── reference ├── api.md ├── netology_pd_diplom │ ├── .gitignore │ ├── README.md │ ├── backend │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── migrations │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── serializers.py │ │ ├── signals.py │ │ ├── tests.py │ │ ├── urls.py │ │ └── views.py │ ├── manage.py │ ├── netology_pd_diplom │ │ ├── __init__.py │ │ ├── settings.py │ │ ├── urls.py │ │ └── wsgi.py │ └── requirements.txt ├── screens.md ├── service.md ├── step-1.md ├── step-2.md ├── step-3.md ├── step-4.md ├── step-5.md ├── step-6-adv.md └── step-7-adv.md └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | ### Python ### 2 | .idea/ 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | *.sqlite3 9 | env/ 10 | 11 | ### Project specific ### 12 | settings_local.py 13 | 14 | # Byte-compiled / optimized / DLL files 15 | __pycache__/ 16 | *.py[cod] 17 | *$py.class 18 | .idea/ 19 | 20 | # C extensions 21 | *.so 22 | 23 | # Distribution / packaging 24 | .Python 25 | build/ 26 | develop-eggs/ 27 | dist/ 28 | downloads/ 29 | eggs/ 30 | .eggs/ 31 | lib/ 32 | lib64/ 33 | parts/ 34 | sdist/ 35 | var/ 36 | wheels/ 37 | *.egg-info/ 38 | .installed.cfg 39 | *.egg 40 | MANIFEST 41 | 42 | # PyInstaller 43 | # Usually these files are written by a python script from a template 44 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 45 | *.manifest 46 | *.spec 47 | 48 | # Installer logs 49 | pip-log.txt 50 | pip-delete-this-directory.txt 51 | 52 | # Unit test / coverage reports 53 | htmlcov/ 54 | .tox/ 55 | .coverage 56 | .coverage.* 57 | .cache 58 | nosetests.xml 59 | coverage.xml 60 | *.cover 61 | .hypothesis/ 62 | .pytest_cache/ 63 | 64 | # Translations 65 | *.mo 66 | *.pot 67 | 68 | # Django stuff: 69 | 70 | 71 | 72 | 73 | */migrations/* 74 | .log 75 | db.sqlite3 76 | 77 | 78 | # Scrapy stuff: 79 | .scrapy 80 | 81 | # Sphinx documentation 82 | docs/_build/ 83 | 84 | # PyBuilder 85 | target/ 86 | 87 | # Jupyter Notebook 88 | .ipynb_checkpoints 89 | 90 | # pyenv 91 | .python-version 92 | 93 | # celery beat schedule file 94 | celerybeat-schedule 95 | 96 | # SageMath parsed files 97 | *.sage.py 98 | 99 | # Environments 100 | .env 101 | .venv 102 | env/ 103 | venv/ 104 | ENV/ 105 | env.bak/ 106 | venv.bak/ 107 | 108 | # Spyder project settings 109 | .spyderproject 110 | .spyproject 111 | 112 | # Rope project settings 113 | .ropeproject 114 | 115 | # mkdocs documentation 116 | /site 117 | 118 | # mypy 119 | .mypy_cache/ 120 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Дипломный проект профессии «Python-разработчик: расширенный курс» 2 | 3 | ## Backend-приложение для автоматизации закупок 4 | 5 | ### Цель дипломного проекта 6 | 7 | Создадите и настроите проект по автоматизации закупок в розничной сети, проработаете модели данных, импорт товаров, API views. 8 | 9 | Вам нужно: 10 | 11 | * разработать backend для сервиса заказа товаров, 12 | * усовершенствовать навыки работы с Django ORM через проработку моделей товаров и связанных сущностей, 13 | * реализовать импорт и экспорт товаров, 14 | * внедрить систему управления заказами, 15 | * оптимизировать методы с использованием асинхронности, 16 | * освоить работу со сторонними библиотеками и фреймворками, 17 | * подготовить документацию к проекту, 18 | * использовать AI инструменты для решения задач. 19 | 20 | ----- 21 | 22 | ## Чеклист готовности к работе над проектом 23 | 24 | 1. Изучить материалы лекции подготовки к дипломной работе. 25 | 2. Подготовить компьютер или виртуальную машину с ОС Linux или MacOS (не рекомендуем использовать Windows). 26 | 3. Установить IDE с поддержкой Python: Pycharm, VS Code или др. 27 | 4. Установить версию Python 3.10 или более позднюю. 28 | 5. Установить AI-плагин из списка: 29 | - [Machinet](https://www.machinet.net/), 30 | - [Codeium](https://codeium.com/), 31 | - [Tabnine](https://www.tabnine.com/), 32 | - [Amazon Сodewhisperer](https://aws.amazon.com/ru/codewhisperer/), 33 | - [Mutable.AI](https://mutable.ai/pricing/), 34 | - [Google Duet-AI](https://cloud.google.com/duet-ai/docs/developers/use-in-ide). 35 | 36 | ----- 37 | 38 | ## Инструкция к работе над проектом 39 | 40 | ### Общее описание приложения 41 | 42 | Приложение предназначено для автоматизации закупок в розничной сети через REST API. 43 | 44 | **Внимание! Все взаимодействие с приложением ведется через API запросы. 45 | Реализация фронтенд-приложения возможна только по желанию обучающегося** 46 | 47 | **Пользователи сервиса:** 48 | 49 | 1. Клиент (покупатель): 50 | 51 | - делает ежедневные закупки по каталогу, в котором представлены товары от нескольких поставщиков, 52 | - в одном заказе можно указать товары от разных поставщиков, 53 | - пользователь может авторизироваться, регистрироваться и восстанавливать пароль через API. 54 | 55 | 2. Поставщик: 56 | 57 | - через API информирует сервис об обновлении прайса, 58 | - может включать и отключать приём заказов, 59 | - может получать список оформленных заказов (с товарами из его прайса). 60 | 61 | ### Задача 62 | 63 | Необходимо разработать backend-часть сервиса заказа товаров для розничных сетей на Django Rest Framework. 64 | 65 | **Базовая часть:** 66 | * разработка сервиса под готовую спецификацию (API), 67 | * возможность добавления настраиваемых полей (характеристик) товаров, 68 | * импорт товаров, 69 | * отправка накладной на email администратора (для исполнения заказа), 70 | * отправка заказа на email клиента (подтверждение приёма заказа). 71 | 72 | **Продвинутая часть (необязательная к выполнению, не влияет на получение зачёта):** 73 | * экспорт товаров, 74 | * админка заказов (проставление статуса заказа и уведомление клиента), 75 | * выделение медленных методов в отдельные асинхронные функции (email, импорт, экспорт). 76 | 77 | _Обратите внимание!_ 78 | 79 | В репозитории приведён готовый пример с базовой частью проекта. Вы можете работать с проектом, выбрав один из двух вариантов: 80 | - разработать свою версию, исходя из текстового описания базовой части проекта, 81 | - взять за основу пример из репозитория, изучить его и выполнить продвинутую часть задания. 82 | 83 | Вы можете интерпретировать текстовое описание проекта по-своему. Работа над дипломом - это в первую очередь творческий процесс. Главное - отсутствие плагиата (не сдавать работы других студентов). 84 | 85 | ### Исходные данные для проекта 86 | 87 | 1. Общее описание сервиса 88 | 1. [Спецификация (API) - 1 шт.](./reference/screens.md) 89 | 1. [Файлы yaml для импорта товаров - 1 шт.](./data/shop1.yaml) 90 | 1. [Базовый пример API Сервиса для магазина](./reference//netology_pd_diplom/) 91 | 92 | ## Этапы разработки 93 | 94 | Разработку backend рекомендуется разделить на следующие этапы. 95 | 96 | **Базовая часть:** 97 | 1. [Создание и настройка проекта.](./reference/step-1.md) 98 | 2. [Проработка моделей данных.](./reference/step-2.md) 99 | 3. [Реализация импорта товаров.](./reference/step-3.md) 100 | 4. [Реализация API views.](./reference/step-4.md) 101 | 5. [Полностью готовый backend.](./reference/step-5.md) 102 | 103 | **Продвинутая часть** (выполняется по желанию, если базовая часть полностью готова): 104 | 105 | 6. [Реализация forms и views админки склада.](./reference/step-6-adv.md) 106 | 7. [Вынос медленных методов в задачи Celery.](./reference/step-7-adv.md) 107 | 8. Создание docker-compose файла для приложения. 108 | 109 | 110 | Разработку следует вести с использованием git (github/gitlab/bitbucket) с регулярными коммитами в репозиторий, доступный вашему дипломному руководителю. Старайтесь делать коммиты как можно чаще. 111 | 112 | ### Этап 1. Создание и настройка проекта 113 | 114 | **Критерии достижения** 115 | 116 | 1. Вы имеете актуальный код данного репозитория на рабочем компьютере. 117 | 2. У вас создан Django-проект, и он запускается без ошибок. 118 | 119 | Для получения подробностей по данному этапу 120 | [перейдите по ссылке](./reference/step-1.md). 121 | 122 | ### Этап 2. Проработка моделей данных 123 | 124 | **Критерии достижения** 125 | 126 | 1. Созданы модели и их дополнительные методы. 127 | 128 | Для получения подробностей по данному этапу 129 | [перейдите по ссылке](./reference/step-2.md). 130 | 131 | ### Этап 3. Реализация импорта товаров 132 | 133 | **Критерии достижения** 134 | 135 | 1. Созданы функции загрузки товаров из приложенных файлов в модели Django. 136 | 2. Загружены товары из всех файлов для импорта. 137 | 138 | Для получения подробностей по данному этапу 139 | [перейдите по ссылке](./reference/step-3.md). 140 | 141 | ### Этап 4. Реализация APIViews 142 | 143 | **Критерии достижения** 144 | 145 | 1. Реализованы API Views для основных [страниц](./reference/screens.md) сервиса (без админки): 146 | - Авторизация 147 | - Регистрация 148 | - Получение списка товаров 149 | - Получение спецификации по отдельному товару в базе данных 150 | - Работа с корзиной (добавление, удаление товаров) 151 | - Добавление/удаление адреса доставки 152 | - Подтверждение заказа 153 | - Отправка email c подтверждением 154 | - Получение списка заказов 155 | - Получение деталей заказа 156 | - Редактирование статуса заказа 157 | 158 | Для получения подробностей по данному этапу 159 | [перейдите по ссылке](./reference/step-4.md). 160 | 161 | ### Этап 5. Полностью готовый backend 162 | 163 | **Критерии достижения** 164 | 165 | 1. Полностью работающие API Endpoint'ы 166 | 2. Корректно отрабатывает следующий сценарий: 167 | - пользователь может авторизироваться, 168 | - есть возможность отправки данных для регистрации и получения email с подтверждением регистрации, 169 | - пользователь может добавлять в корзину товары от разных магазинов, 170 | - пользователь может подтверждать заказ с вводом адреса доставки, 171 | - пользователь получает email с подтверждением после ввода адреса доставки, 172 | - пользователь может переходить на страницу «‎Заказы» и открывать созданный заказ. 173 | 174 | Для получения подробностей по данному этапу 175 | [перейдите по ссылке](./reference/step-5.md). 176 | 177 | ## Полезные материалы 178 | 179 | 1. [Информация о сервисе](./reference/service.md) 180 | 2. [Спецификация API](./reference/api.md) 181 | 3. [Описание страниц сервиса](./reference/screens.md) 182 | 183 | --- 184 | 185 | ## Продвинутая часть (выполняется по желанию, не влияет на получение зачёта) 186 | 187 | Обязательное условие: базовая часть проекта полностью готова. 188 | 189 | ### Этап 6. Реализация API views админки склада 190 | 191 | **Критерии достижения** 192 | 193 | 1. Реализованы API views для [страниц админки](./reference/screens.md) сервиса. 194 | 195 | Для получения подробностей по данному этапу 196 | [перейдите по ссылке](reference/step-6-adv.md). 197 | 198 | ### Этап 7. Вынос медленных методов в задачи Celery 199 | 200 | **Критерии достижения** 201 | 202 | 1. Создано Celery-приложение c методами: 203 | - send_email, 204 | - do_import. 205 | 2. Создан view для запуска Celery-задачи do_import из админки. 206 | 207 | Для получения подробностей по данному этапу 208 | [перейдите по ссылке](reference/step-7-adv.md). 209 | 210 | ### Этап 8. Создание docker-compose файла для приложения 211 | 1. Создать docker-compose файл для сборки приложения. 212 | 2. Предоставить инструкцию для сборки docker-образа. 213 | 214 | _Важно: не нарушайте дедлайн сдачи, возникающие вопросы задавайте в чате с дипломным руководителем._ 215 | 216 | ----- 217 | 218 | ## Правила приёма дипломной работы 219 | 220 | 1. Проект разместить в GitHub. Ссылка на дипломную работу должна оставаться неизменной, чтобы дипломный руководитель мог видеть ваш прогресс. 221 | 2. Сдавать финальный вариант дипломной работы в личном кабинете Нетологии. 222 | 223 | ----- 224 | 225 | ## Критерии оценки 226 | 227 | Зачёт по дипломной работе можно получить, если работа соответствует критериям: 228 | 229 | * работоспособный проект в репозитории с документацией по запуску, 230 | * выполненная базовая часть проекта, 231 | * наличие собственных комментариев к коду, 232 | * использование сторонних библиотек и фреймворков. 233 | -------------------------------------------------------------------------------- /data/shop1.yaml: -------------------------------------------------------------------------------- 1 | shop: Связной 2 | categories: 3 | - id: 224 4 | name: Смартфоны 5 | - id: 15 6 | name: Аксессуары 7 | - id: 1 8 | name: Flash-накопители 9 | - id: 5 10 | name: Телевизоры 11 | 12 | goods: 13 | - id: 4216292 14 | category: 224 15 | model: apple/iphone/xs-max 16 | name: Смартфон Apple iPhone XS Max 512GB (золотистый) 17 | price: 110000 18 | price_rrc: 116990 19 | quantity: 14 20 | parameters: 21 | "Диагональ (дюйм)": 6.5 22 | "Разрешение (пикс)": 2688x1242 23 | "Встроенная память (Гб)": 512 24 | "Цвет": золотистый 25 | - id: 4216313 26 | category: 224 27 | model: apple/iphone/xr 28 | name: Смартфон Apple iPhone XR 256GB (красный) 29 | price: 65000 30 | price_rrc: 69990 31 | quantity: 9 32 | parameters: 33 | "Диагональ (дюйм)": 6.1 34 | "Разрешение (пикс)": 1792x828 35 | "Встроенная память (Гб)": 256 36 | "Цвет": красный 37 | - id: 4216226 38 | category: 224 39 | model: apple/iphone/xr 40 | name: Смартфон Apple iPhone XR 256GB (черный) 41 | price: 65000 42 | price_rrc: 69990 43 | quantity: 5 44 | parameters: 45 | "Диагональ (дюйм)": 6.1 46 | "Разрешение (пикс)": 1792x828 47 | "Встроенная память (Гб)": 256 48 | "Цвет": черный 49 | - id: 4672670 50 | category: 224 51 | model: apple/iphone/xr 52 | name: Смартфон Apple iPhone XR 128GB (синий) 53 | price: 60000 54 | price_rrc: 64990 55 | quantity: 7 56 | parameters: 57 | "Диагональ (дюйм)": 6.1 58 | "Разрешение (пикс)": 1792x828 59 | "Встроенная память (Гб)": 256 60 | "Цвет": синий 61 | - id: 1234567 62 | category: 15 63 | model: samsung/galaxy-s20 64 | name: Smartphone Samsung Galaxy S20 128GB (black) 65 | price: 80000 66 | price_rrc: 84990 67 | quantity: 5 68 | parameters: 69 | "Screen Size (inches)": 6.2 70 | "Resolution (pixels)": 3200x1440 71 | "Internal Memory (GB)": 128 72 | "Color": black 73 | - id: 1234568 74 | category: 15 75 | model: samsung/galaxy-note20 76 | name: Smartphone Samsung Galaxy Note20 256GB (mystic bronze) 77 | price: 95000 78 | price_rrc: 99990 79 | quantity: 3 80 | parameters: 81 | "Screen Size (inches)": 6.7 82 | "Resolution (pixels)": 2400x1080 83 | "Internal Memory (GB)": 256 84 | "Color": mystic bronze 85 | - id: 1235464569 86 | category: 1 87 | model: sandisk/ultra-flair-64gb 88 | name: USB Flash Drive SanDisk Ultra Flair 64GB (silver) 89 | price: 1500 90 | price_rrc: 1990 91 | quantity: 10 92 | parameters: 93 | "Capacity (GB)": 64 94 | "Color": silver 95 | - id: 1235434570 96 | category: 1 97 | model: kingston/datatraveler-32gb 98 | name: USB Flash Drive Kingston DataTraveler 32GB (red) 99 | price: 1000 100 | price_rrc: 1290 101 | quantity: 8 102 | parameters: 103 | "Capacity (GB)": 32 104 | "Color": red 105 | - id: 123544571 106 | category: 224 107 | model: xiaomi/mi-10t-pro 108 | name: Smartphone Xiaomi Mi 10T Pro 256GB (cosmic black) 109 | price: 70000 110 | price_rrc: 74990 111 | quantity: 6 112 | parameters: 113 | "Screen Size (inches)": 6.67 114 | "Resolution (pixels)": 2400x1080 115 | "Internal Memory (GB)": 256 116 | "Color": cosmic black 117 | - id: 1234572 118 | category: 5 119 | model: samsung/qled-q90r 120 | name: Samsung QLED Q90R 65" 4K UHD Smart TV 121 | price: 2500 122 | price_rrc: 2999 123 | quantity: 4 124 | parameters: 125 | "Screen Size (inches)": 65 126 | "Resolution (pixels)": 3840x2160 127 | "Smart TV": true 128 | - id: 123476573 129 | category: 5 130 | model: lg/oled-cx 131 | name: LG OLED CX 55" 4K UHD Smart TV 132 | price: 1800 133 | price_rrc: 1999 134 | quantity: 7 135 | parameters: 136 | "Screen Size (inches)": 55 137 | "Resolution (pixels)": 3840x2160 138 | "Smart TV": true 139 | - id: 1234453574 140 | category: 5 141 | model: sony/bravia-x900h 142 | name: Sony Bravia X900H 75" 4K UHD Smart TV 143 | price: 3000 144 | price_rrc: 3499 145 | quantity: 3 146 | parameters: 147 | "Screen Size (inches)": 75 148 | "Resolution (pixels)": 3840x2160 149 | "Smart TV": true 150 | - id: 123234575 151 | category: 5 152 | model: tcl/6-series 153 | name: TCL 6-Series 55" 4K UHD Smart TV 154 | price: 700 155 | price_rrc: 799 156 | quantity: 10 157 | parameters: 158 | "Screen Size (inches)": 55 159 | "Resolution (pixels)": 3840x2160 160 | "Smart TV": true 161 | - id: 123424576 162 | category: 5 163 | model: vizio/p-series-quantum 164 | name: Vizio P-Series Quantum 65" 4K UHD Smart TV 165 | price: 1200 166 | price_rrc: 1299 167 | quantity: 5 168 | parameters: 169 | "Screen Size (inches)": 65 170 | "Resolution (pixels)": 3840x2160 171 | "Smart TV": true 172 | -------------------------------------------------------------------------------- /reference/api.md: -------------------------------------------------------------------------------- 1 | ## API 2 | Пример документации API на основе базового примера дипломного проекта 3 | 4 | Документация сгенерирована в Postman 5 | 6 | Крайне рекомендуется установить Postman или аналоги (Insomnia) для изучения возможностей данного класса ПО 7 | 8 | [Документация по запросам в PostMan](https://documenter.getpostman.com/view/5037826/SVfJUrSc) 9 | -------------------------------------------------------------------------------- /reference/netology_pd_diplom/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | .idea/ 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | 57 | 58 | 59 | 60 | */migrations/* 61 | .log 62 | db.sqlite3 63 | 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # Environments 87 | .env 88 | .venv 89 | env/ 90 | venv/ 91 | ENV/ 92 | env.bak/ 93 | venv.bak/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | -------------------------------------------------------------------------------- /reference/netology_pd_diplom/README.md: -------------------------------------------------------------------------------- 1 | # Пример API-сервиса для магазина 2 | 3 | [Документация по запросам в PostMan](https://documenter.getpostman.com/view/5037826/SVfJUrSc) 4 | 5 | 6 | 7 | 8 | ## **Получить исходный код** 9 | 10 | git config --global user.name "YOUR_USERNAME" 11 | 12 | git config --global user.email "your_email_address@example.com" 13 | 14 | mkdir ~/my_diplom 15 | 16 | cd my_diplom 17 | 18 | git clone git@github.com:A-Iskakov/netology_pd_diplom.git 19 | 20 | cd netology_pd_diplom 21 | 22 | sudo pip3 install --upgrade pip 23 | 24 | sudo pip3 install -r requirements.txt 25 | 26 | python3 manage.py makemigrations 27 | 28 | python3 manage.py migrate 29 | 30 | python3 manage.py createsuperuser 31 | 32 | 33 | ## **Проверить работу модулей** 34 | 35 | 36 | python3 manage.py runserver 0.0.0.0:8000 37 | 38 | 39 | ## **Установить СУБД (опционально)** 40 | 41 | sudo nano /etc/apt/sources.list.d/pgdg.list 42 | 43 | -----> 44 | deb http://apt.postgresql.org/pub/repos/apt/ bionic-pgdg main 45 | <<---- 46 | 47 | 48 | wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - 49 | 50 | sudo apt-get update 51 | 52 | sudo apt-get install postgresql-11 postgresql-server-dev-11 53 | 54 | sudo -u postgres psql postgres 55 | 56 | create user diplom_user with password 'password'; 57 | 58 | alter role diplom_user set client_encoding to 'utf8'; 59 | 60 | alter role diplom_user set default_transaction_isolation to 'read committed'; 61 | 62 | alter role diplom_user set timezone to 'Europe/Moscow'; 63 | 64 | create database diplom_db owner mploy; 65 | alter user mploy createdb; 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /reference/netology_pd_diplom/backend/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netology-code/python-final-diplom/dd11860cf2f16b890fe5031ec565482658cb7a96/reference/netology_pd_diplom/backend/__init__.py -------------------------------------------------------------------------------- /reference/netology_pd_diplom/backend/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth.admin import UserAdmin 3 | 4 | from backend.models import User, Shop, Category, Product, ProductInfo, Parameter, ProductParameter, Order, OrderItem, \ 5 | Contact, ConfirmEmailToken 6 | 7 | 8 | @admin.register(User) 9 | class CustomUserAdmin(UserAdmin): 10 | """ 11 | Панель управления пользователями 12 | """ 13 | model = User 14 | 15 | fieldsets = ( 16 | (None, {'fields': ('email', 'password', 'type')}), 17 | ('Personal info', {'fields': ('first_name', 'last_name', 'company', 'position')}), 18 | ('Permissions', { 19 | 'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions'), 20 | }), 21 | ('Important dates', {'fields': ('last_login', 'date_joined')}), 22 | ) 23 | list_display = ('email', 'first_name', 'last_name', 'is_staff') 24 | 25 | 26 | @admin.register(Shop) 27 | class ShopAdmin(admin.ModelAdmin): 28 | pass 29 | 30 | 31 | @admin.register(Category) 32 | class CategoryAdmin(admin.ModelAdmin): 33 | pass 34 | 35 | 36 | @admin.register(Product) 37 | class ProductAdmin(admin.ModelAdmin): 38 | pass 39 | 40 | 41 | @admin.register(ProductInfo) 42 | class ProductInfoAdmin(admin.ModelAdmin): 43 | pass 44 | 45 | 46 | @admin.register(Parameter) 47 | class ParameterAdmin(admin.ModelAdmin): 48 | pass 49 | 50 | 51 | @admin.register(ProductParameter) 52 | class ProductParameterAdmin(admin.ModelAdmin): 53 | pass 54 | 55 | 56 | @admin.register(Order) 57 | class OrderAdmin(admin.ModelAdmin): 58 | pass 59 | 60 | 61 | @admin.register(OrderItem) 62 | class OrderItemAdmin(admin.ModelAdmin): 63 | pass 64 | 65 | 66 | @admin.register(Contact) 67 | class ContactAdmin(admin.ModelAdmin): 68 | pass 69 | 70 | 71 | @admin.register(ConfirmEmailToken) 72 | class ConfirmEmailTokenAdmin(admin.ModelAdmin): 73 | list_display = ('user', 'key', 'created_at',) 74 | -------------------------------------------------------------------------------- /reference/netology_pd_diplom/backend/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class BackendConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'backend' 7 | 8 | def ready(self): 9 | """ 10 | импортируем сигналы 11 | """ 12 | -------------------------------------------------------------------------------- /reference/netology_pd_diplom/backend/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netology-code/python-final-diplom/dd11860cf2f16b890fe5031ec565482658cb7a96/reference/netology_pd_diplom/backend/migrations/__init__.py -------------------------------------------------------------------------------- /reference/netology_pd_diplom/backend/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.base_user import BaseUserManager 2 | from django.contrib.auth.models import AbstractUser 3 | from django.contrib.auth.validators import UnicodeUsernameValidator 4 | from django.db import models 5 | from django.utils.translation import gettext_lazy as _ 6 | from django_rest_passwordreset.tokens import get_token_generator 7 | 8 | STATE_CHOICES = ( 9 | ('basket', 'Статус корзины'), 10 | ('new', 'Новый'), 11 | ('confirmed', 'Подтвержден'), 12 | ('assembled', 'Собран'), 13 | ('sent', 'Отправлен'), 14 | ('delivered', 'Доставлен'), 15 | ('canceled', 'Отменен'), 16 | ) 17 | 18 | USER_TYPE_CHOICES = ( 19 | ('shop', 'Магазин'), 20 | ('buyer', 'Покупатель'), 21 | 22 | ) 23 | 24 | 25 | # Create your models here. 26 | 27 | 28 | class UserManager(BaseUserManager): 29 | """ 30 | Миксин для управления пользователями 31 | """ 32 | use_in_migrations = True 33 | 34 | def _create_user(self, email, password, **extra_fields): 35 | """ 36 | Create and save a user with the given username, email, and password. 37 | """ 38 | if not email: 39 | raise ValueError('The given email must be set') 40 | email = self.normalize_email(email) 41 | user = self.model(email=email, **extra_fields) 42 | user.set_password(password) 43 | user.save(using=self._db) 44 | return user 45 | 46 | def create_user(self, email, password=None, **extra_fields): 47 | extra_fields.setdefault('is_staff', False) 48 | extra_fields.setdefault('is_superuser', False) 49 | return self._create_user(email, password, **extra_fields) 50 | 51 | def create_superuser(self, email, password, **extra_fields): 52 | extra_fields.setdefault('is_staff', True) 53 | extra_fields.setdefault('is_superuser', True) 54 | extra_fields.setdefault('is_active', True) 55 | 56 | if extra_fields.get('is_staff') is not True: 57 | raise ValueError('Superuser must have is_staff=True.') 58 | if extra_fields.get('is_superuser') is not True: 59 | raise ValueError('Superuser must have is_superuser=True.') 60 | 61 | return self._create_user(email, password, **extra_fields) 62 | 63 | 64 | class User(AbstractUser): 65 | """ 66 | Стандартная модель пользователей 67 | """ 68 | REQUIRED_FIELDS = [] 69 | objects = UserManager() 70 | USERNAME_FIELD = 'email' 71 | email = models.EmailField(_('email address'), unique=True) 72 | company = models.CharField(verbose_name='Компания', max_length=40, blank=True) 73 | position = models.CharField(verbose_name='Должность', max_length=40, blank=True) 74 | username_validator = UnicodeUsernameValidator() 75 | username = models.CharField( 76 | _('username'), 77 | max_length=150, 78 | help_text=_('Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.'), 79 | validators=[username_validator], 80 | error_messages={ 81 | 'unique': _("A user with that username already exists."), 82 | }, 83 | ) 84 | is_active = models.BooleanField( 85 | _('active'), 86 | default=False, 87 | help_text=_( 88 | 'Designates whether this user should be treated as active. ' 89 | 'Unselect this instead of deleting accounts.' 90 | ), 91 | ) 92 | type = models.CharField(verbose_name='Тип пользователя', choices=USER_TYPE_CHOICES, max_length=5, default='buyer') 93 | 94 | def __str__(self): 95 | return f'{self.first_name} {self.last_name}' 96 | 97 | class Meta: 98 | verbose_name = 'Пользователь' 99 | verbose_name_plural = "Список пользователей" 100 | ordering = ('email',) 101 | 102 | 103 | class Shop(models.Model): 104 | objects = models.manager.Manager() 105 | name = models.CharField(max_length=50, verbose_name='Название') 106 | url = models.URLField(verbose_name='Ссылка', null=True, blank=True) 107 | user = models.OneToOneField(User, verbose_name='Пользователь', 108 | blank=True, null=True, 109 | on_delete=models.CASCADE) 110 | state = models.BooleanField(verbose_name='статус получения заказов', default=True) 111 | 112 | # filename 113 | 114 | class Meta: 115 | verbose_name = 'Магазин' 116 | verbose_name_plural = "Список магазинов" 117 | ordering = ('-name',) 118 | 119 | def __str__(self): 120 | return self.name 121 | 122 | 123 | class Category(models.Model): 124 | objects = models.manager.Manager() 125 | name = models.CharField(max_length=40, verbose_name='Название') 126 | shops = models.ManyToManyField(Shop, verbose_name='Магазины', related_name='categories', blank=True) 127 | 128 | class Meta: 129 | verbose_name = 'Категория' 130 | verbose_name_plural = "Список категорий" 131 | ordering = ('-name',) 132 | 133 | def __str__(self): 134 | return self.name 135 | 136 | 137 | class Product(models.Model): 138 | objects = models.manager.Manager() 139 | name = models.CharField(max_length=80, verbose_name='Название') 140 | category = models.ForeignKey(Category, verbose_name='Категория', related_name='products', blank=True, 141 | on_delete=models.CASCADE) 142 | 143 | class Meta: 144 | verbose_name = 'Продукт' 145 | verbose_name_plural = "Список продуктов" 146 | ordering = ('-name',) 147 | 148 | def __str__(self): 149 | return self.name 150 | 151 | 152 | class ProductInfo(models.Model): 153 | objects = models.manager.Manager() 154 | model = models.CharField(max_length=80, verbose_name='Модель', blank=True) 155 | external_id = models.PositiveIntegerField(verbose_name='Внешний ИД') 156 | product = models.ForeignKey(Product, verbose_name='Продукт', related_name='product_infos', blank=True, 157 | on_delete=models.CASCADE) 158 | shop = models.ForeignKey(Shop, verbose_name='Магазин', related_name='product_infos', blank=True, 159 | on_delete=models.CASCADE) 160 | quantity = models.PositiveIntegerField(verbose_name='Количество') 161 | price = models.PositiveIntegerField(verbose_name='Цена') 162 | price_rrc = models.PositiveIntegerField(verbose_name='Рекомендуемая розничная цена') 163 | 164 | class Meta: 165 | verbose_name = 'Информация о продукте' 166 | verbose_name_plural = "Информационный список о продуктах" 167 | constraints = [ 168 | models.UniqueConstraint(fields=['product', 'shop', 'external_id'], name='unique_product_info'), 169 | ] 170 | 171 | 172 | class Parameter(models.Model): 173 | objects = models.manager.Manager() 174 | name = models.CharField(max_length=40, verbose_name='Название') 175 | 176 | class Meta: 177 | verbose_name = 'Имя параметра' 178 | verbose_name_plural = "Список имен параметров" 179 | ordering = ('-name',) 180 | 181 | def __str__(self): 182 | return self.name 183 | 184 | 185 | class ProductParameter(models.Model): 186 | objects = models.manager.Manager() 187 | product_info = models.ForeignKey(ProductInfo, verbose_name='Информация о продукте', 188 | related_name='product_parameters', blank=True, 189 | on_delete=models.CASCADE) 190 | parameter = models.ForeignKey(Parameter, verbose_name='Параметр', related_name='product_parameters', blank=True, 191 | on_delete=models.CASCADE) 192 | value = models.CharField(verbose_name='Значение', max_length=100) 193 | 194 | class Meta: 195 | verbose_name = 'Параметр' 196 | verbose_name_plural = "Список параметров" 197 | constraints = [ 198 | models.UniqueConstraint(fields=['product_info', 'parameter'], name='unique_product_parameter'), 199 | ] 200 | 201 | 202 | class Contact(models.Model): 203 | objects = models.manager.Manager() 204 | user = models.ForeignKey(User, verbose_name='Пользователь', 205 | related_name='contacts', blank=True, 206 | on_delete=models.CASCADE) 207 | 208 | city = models.CharField(max_length=50, verbose_name='Город') 209 | street = models.CharField(max_length=100, verbose_name='Улица') 210 | house = models.CharField(max_length=15, verbose_name='Дом', blank=True) 211 | structure = models.CharField(max_length=15, verbose_name='Корпус', blank=True) 212 | building = models.CharField(max_length=15, verbose_name='Строение', blank=True) 213 | apartment = models.CharField(max_length=15, verbose_name='Квартира', blank=True) 214 | phone = models.CharField(max_length=20, verbose_name='Телефон') 215 | 216 | class Meta: 217 | verbose_name = 'Контакты пользователя' 218 | verbose_name_plural = "Список контактов пользователя" 219 | 220 | def __str__(self): 221 | return f'{self.city} {self.street} {self.house}' 222 | 223 | 224 | class Order(models.Model): 225 | objects = models.manager.Manager() 226 | user = models.ForeignKey(User, verbose_name='Пользователь', 227 | related_name='orders', blank=True, 228 | on_delete=models.CASCADE) 229 | dt = models.DateTimeField(auto_now_add=True) 230 | state = models.CharField(verbose_name='Статус', choices=STATE_CHOICES, max_length=15) 231 | contact = models.ForeignKey(Contact, verbose_name='Контакт', 232 | blank=True, null=True, 233 | on_delete=models.CASCADE) 234 | 235 | class Meta: 236 | verbose_name = 'Заказ' 237 | verbose_name_plural = "Список заказ" 238 | ordering = ('-dt',) 239 | 240 | def __str__(self): 241 | return str(self.dt) 242 | 243 | # @property 244 | # def sum(self): 245 | # return self.ordered_items.aggregate(total=Sum("quantity"))["total"] 246 | 247 | 248 | class OrderItem(models.Model): 249 | objects = models.manager.Manager() 250 | order = models.ForeignKey(Order, verbose_name='Заказ', related_name='ordered_items', blank=True, 251 | on_delete=models.CASCADE) 252 | 253 | product_info = models.ForeignKey(ProductInfo, verbose_name='Информация о продукте', related_name='ordered_items', 254 | blank=True, 255 | on_delete=models.CASCADE) 256 | quantity = models.PositiveIntegerField(verbose_name='Количество') 257 | 258 | class Meta: 259 | verbose_name = 'Заказанная позиция' 260 | verbose_name_plural = "Список заказанных позиций" 261 | constraints = [ 262 | models.UniqueConstraint(fields=['order_id', 'product_info'], name='unique_order_item'), 263 | ] 264 | 265 | 266 | class ConfirmEmailToken(models.Model): 267 | objects = models.manager.Manager() 268 | class Meta: 269 | verbose_name = 'Токен подтверждения Email' 270 | verbose_name_plural = 'Токены подтверждения Email' 271 | 272 | @staticmethod 273 | def generate_key(): 274 | """ generates a pseudo random code using os.urandom and binascii.hexlify """ 275 | return get_token_generator().generate_token() 276 | 277 | user = models.ForeignKey( 278 | User, 279 | related_name='confirm_email_tokens', 280 | on_delete=models.CASCADE, 281 | verbose_name=_("The User which is associated to this password reset token") 282 | ) 283 | 284 | created_at = models.DateTimeField( 285 | auto_now_add=True, 286 | verbose_name=_("When was this token generated") 287 | ) 288 | 289 | # Key field, though it is not the primary key of the model 290 | key = models.CharField( 291 | _("Key"), 292 | max_length=64, 293 | db_index=True, 294 | unique=True 295 | ) 296 | 297 | def save(self, *args, **kwargs): 298 | if not self.key: 299 | self.key = self.generate_key() 300 | return super(ConfirmEmailToken, self).save(*args, **kwargs) 301 | 302 | def __str__(self): 303 | return "Password reset token for user {user}".format(user=self.user) 304 | -------------------------------------------------------------------------------- /reference/netology_pd_diplom/backend/serializers.py: -------------------------------------------------------------------------------- 1 | # Верстальщик 2 | from rest_framework import serializers 3 | 4 | from backend.models import User, Category, Shop, ProductInfo, Product, ProductParameter, OrderItem, Order, Contact 5 | 6 | 7 | class ContactSerializer(serializers.ModelSerializer): 8 | class Meta: 9 | model = Contact 10 | fields = ('id', 'city', 'street', 'house', 'structure', 'building', 'apartment', 'user', 'phone') 11 | read_only_fields = ('id',) 12 | extra_kwargs = { 13 | 'user': {'write_only': True} 14 | } 15 | 16 | 17 | class UserSerializer(serializers.ModelSerializer): 18 | contacts = ContactSerializer(read_only=True, many=True) 19 | 20 | class Meta: 21 | model = User 22 | fields = ('id', 'first_name', 'last_name', 'email', 'company', 'position', 'contacts') 23 | read_only_fields = ('id',) 24 | 25 | 26 | class CategorySerializer(serializers.ModelSerializer): 27 | class Meta: 28 | model = Category 29 | fields = ('id', 'name',) 30 | read_only_fields = ('id',) 31 | 32 | 33 | class ShopSerializer(serializers.ModelSerializer): 34 | class Meta: 35 | model = Shop 36 | fields = ('id', 'name', 'state',) 37 | read_only_fields = ('id',) 38 | 39 | 40 | class ProductSerializer(serializers.ModelSerializer): 41 | category = serializers.StringRelatedField() 42 | 43 | class Meta: 44 | model = Product 45 | fields = ('name', 'category',) 46 | 47 | 48 | class ProductParameterSerializer(serializers.ModelSerializer): 49 | parameter = serializers.StringRelatedField() 50 | 51 | class Meta: 52 | model = ProductParameter 53 | fields = ('parameter', 'value',) 54 | 55 | 56 | class ProductInfoSerializer(serializers.ModelSerializer): 57 | product = ProductSerializer(read_only=True) 58 | product_parameters = ProductParameterSerializer(read_only=True, many=True) 59 | 60 | class Meta: 61 | model = ProductInfo 62 | fields = ('id', 'model', 'product', 'shop', 'quantity', 'price', 'price_rrc', 'product_parameters',) 63 | read_only_fields = ('id',) 64 | 65 | 66 | class OrderItemSerializer(serializers.ModelSerializer): 67 | class Meta: 68 | model = OrderItem 69 | fields = ('id', 'product_info', 'quantity', 'order',) 70 | read_only_fields = ('id',) 71 | extra_kwargs = { 72 | 'order': {'write_only': True} 73 | } 74 | 75 | 76 | class OrderItemCreateSerializer(OrderItemSerializer): 77 | product_info = ProductInfoSerializer(read_only=True) 78 | 79 | 80 | class OrderSerializer(serializers.ModelSerializer): 81 | ordered_items = OrderItemCreateSerializer(read_only=True, many=True) 82 | 83 | total_sum = serializers.IntegerField() 84 | contact = ContactSerializer(read_only=True) 85 | 86 | class Meta: 87 | model = Order 88 | fields = ('id', 'ordered_items', 'state', 'dt', 'total_sum', 'contact',) 89 | read_only_fields = ('id',) 90 | -------------------------------------------------------------------------------- /reference/netology_pd_diplom/backend/signals.py: -------------------------------------------------------------------------------- 1 | from typing import Type 2 | 3 | from django.conf import settings 4 | from django.core.mail import EmailMultiAlternatives 5 | from django.db.models.signals import post_save 6 | from django.dispatch import receiver, Signal 7 | from django_rest_passwordreset.signals import reset_password_token_created 8 | 9 | from backend.models import ConfirmEmailToken, User 10 | 11 | new_user_registered = Signal() 12 | 13 | new_order = Signal() 14 | 15 | 16 | @receiver(reset_password_token_created) 17 | def password_reset_token_created(sender, instance, reset_password_token, **kwargs): 18 | """ 19 | Отправляем письмо с токеном для сброса пароля 20 | When a token is created, an e-mail needs to be sent to the user 21 | :param sender: View Class that sent the signal 22 | :param instance: View Instance that sent the signal 23 | :param reset_password_token: Token Model Object 24 | :param kwargs: 25 | :return: 26 | """ 27 | # send an e-mail to the user 28 | 29 | msg = EmailMultiAlternatives( 30 | # title: 31 | f"Password Reset Token for {reset_password_token.user}", 32 | # message: 33 | reset_password_token.key, 34 | # from: 35 | settings.EMAIL_HOST_USER, 36 | # to: 37 | [reset_password_token.user.email] 38 | ) 39 | msg.send() 40 | 41 | 42 | @receiver(post_save, sender=User) 43 | def new_user_registered_signal(sender: Type[User], instance: User, created: bool, **kwargs): 44 | """ 45 | отправляем письмо с подтрердждением почты 46 | """ 47 | if created and not instance.is_active: 48 | # send an e-mail to the user 49 | token, _ = ConfirmEmailToken.objects.get_or_create(user_id=instance.pk) 50 | 51 | msg = EmailMultiAlternatives( 52 | # title: 53 | f"Password Reset Token for {instance.email}", 54 | # message: 55 | token.key, 56 | # from: 57 | settings.EMAIL_HOST_USER, 58 | # to: 59 | [instance.email] 60 | ) 61 | msg.send() 62 | 63 | 64 | @receiver(new_order) 65 | def new_order_signal(user_id, **kwargs): 66 | """ 67 | отправяем письмо при изменении статуса заказа 68 | """ 69 | # send an e-mail to the user 70 | user = User.objects.get(id=user_id) 71 | 72 | msg = EmailMultiAlternatives( 73 | # title: 74 | f"Обновление статуса заказа", 75 | # message: 76 | 'Заказ сформирован', 77 | # from: 78 | settings.EMAIL_HOST_USER, 79 | # to: 80 | [user.email] 81 | ) 82 | msg.send() 83 | -------------------------------------------------------------------------------- /reference/netology_pd_diplom/backend/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /reference/netology_pd_diplom/backend/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from django_rest_passwordreset.views import reset_password_request_token, reset_password_confirm 3 | 4 | from backend.views import PartnerUpdate, RegisterAccount, LoginAccount, CategoryView, ShopView, ProductInfoView, \ 5 | BasketView, \ 6 | AccountDetails, ContactView, OrderView, PartnerState, PartnerOrders, ConfirmAccount 7 | 8 | app_name = 'backend' 9 | urlpatterns = [ 10 | path('partner/update', PartnerUpdate.as_view(), name='partner-update'), 11 | path('partner/state', PartnerState.as_view(), name='partner-state'), 12 | path('partner/orders', PartnerOrders.as_view(), name='partner-orders'), 13 | path('user/register', RegisterAccount.as_view(), name='user-register'), 14 | path('user/register/confirm', ConfirmAccount.as_view(), name='user-register-confirm'), 15 | path('user/details', AccountDetails.as_view(), name='user-details'), 16 | path('user/contact', ContactView.as_view(), name='user-contact'), 17 | path('user/login', LoginAccount.as_view(), name='user-login'), 18 | path('user/password_reset', reset_password_request_token, name='password-reset'), 19 | path('user/password_reset/confirm', reset_password_confirm, name='password-reset-confirm'), 20 | path('categories', CategoryView.as_view(), name='categories'), 21 | path('shops', ShopView.as_view(), name='shops'), 22 | path('products', ProductInfoView.as_view(), name='shops'), 23 | path('basket', BasketView.as_view(), name='basket'), 24 | path('order', OrderView.as_view(), name='order'), 25 | 26 | ] 27 | -------------------------------------------------------------------------------- /reference/netology_pd_diplom/backend/views.py: -------------------------------------------------------------------------------- 1 | from distutils.util import strtobool 2 | from rest_framework.request import Request 3 | from django.contrib.auth import authenticate 4 | from django.contrib.auth.password_validation import validate_password 5 | from django.core.exceptions import ValidationError 6 | from django.core.validators import URLValidator 7 | from django.db import IntegrityError 8 | from django.db.models import Q, Sum, F 9 | from django.http import JsonResponse 10 | from requests import get 11 | from rest_framework.authtoken.models import Token 12 | from rest_framework.generics import ListAPIView 13 | from rest_framework.response import Response 14 | from rest_framework.views import APIView 15 | from ujson import loads as load_json 16 | from yaml import load as load_yaml, Loader 17 | 18 | from backend.models import Shop, Category, Product, ProductInfo, Parameter, ProductParameter, Order, OrderItem, \ 19 | Contact, ConfirmEmailToken 20 | from backend.serializers import UserSerializer, CategorySerializer, ShopSerializer, ProductInfoSerializer, \ 21 | OrderItemSerializer, OrderSerializer, ContactSerializer 22 | from backend.signals import new_user_registered, new_order 23 | 24 | 25 | class RegisterAccount(APIView): 26 | """ 27 | Для регистрации покупателей 28 | """ 29 | 30 | # Регистрация методом POST 31 | 32 | def post(self, request, *args, **kwargs): 33 | """ 34 | Process a POST request and create a new user. 35 | 36 | Args: 37 | request (Request): The Django request object. 38 | 39 | Returns: 40 | JsonResponse: The response indicating the status of the operation and any errors. 41 | """ 42 | # проверяем обязательные аргументы 43 | if {'first_name', 'last_name', 'email', 'password', 'company', 'position'}.issubset(request.data): 44 | 45 | # проверяем пароль на сложность 46 | sad = 'asd' 47 | try: 48 | validate_password(request.data['password']) 49 | except Exception as password_error: 50 | error_array = [] 51 | # noinspection PyTypeChecker 52 | for item in password_error: 53 | error_array.append(item) 54 | return JsonResponse({'Status': False, 'Errors': {'password': error_array}}) 55 | else: 56 | # проверяем данные для уникальности имени пользователя 57 | 58 | user_serializer = UserSerializer(data=request.data) 59 | if user_serializer.is_valid(): 60 | # сохраняем пользователя 61 | user = user_serializer.save() 62 | user.set_password(request.data['password']) 63 | user.save() 64 | return JsonResponse({'Status': True}) 65 | else: 66 | return JsonResponse({'Status': False, 'Errors': user_serializer.errors}) 67 | 68 | return JsonResponse({'Status': False, 'Errors': 'Не указаны все необходимые аргументы'}) 69 | 70 | 71 | class ConfirmAccount(APIView): 72 | """ 73 | Класс для подтверждения почтового адреса 74 | """ 75 | 76 | # Регистрация методом POST 77 | def post(self, request, *args, **kwargs): 78 | """ 79 | Подтверждает почтовый адрес пользователя. 80 | 81 | Args: 82 | - request (Request): The Django request object. 83 | 84 | Returns: 85 | - JsonResponse: The response indicating the status of the operation and any errors. 86 | """ 87 | # проверяем обязательные аргументы 88 | if {'email', 'token'}.issubset(request.data): 89 | 90 | token = ConfirmEmailToken.objects.filter(user__email=request.data['email'], 91 | key=request.data['token']).first() 92 | if token: 93 | token.user.is_active = True 94 | token.user.save() 95 | token.delete() 96 | return JsonResponse({'Status': True}) 97 | else: 98 | return JsonResponse({'Status': False, 'Errors': 'Неправильно указан токен или email'}) 99 | 100 | return JsonResponse({'Status': False, 'Errors': 'Не указаны все необходимые аргументы'}) 101 | 102 | 103 | class AccountDetails(APIView): 104 | """ 105 | A class for managing user account details. 106 | 107 | Methods: 108 | - get: Retrieve the details of the authenticated user. 109 | - post: Update the account details of the authenticated user. 110 | 111 | Attributes: 112 | - None 113 | """ 114 | 115 | # получить данные 116 | def get(self, request: Request, *args, **kwargs): 117 | """ 118 | Retrieve the details of the authenticated user. 119 | 120 | Args: 121 | - request (Request): The Django request object. 122 | 123 | Returns: 124 | - Response: The response containing the details of the authenticated user. 125 | """ 126 | if not request.user.is_authenticated: 127 | return JsonResponse({'Status': False, 'Error': 'Log in required'}, status=403) 128 | 129 | serializer = UserSerializer(request.user) 130 | return Response(serializer.data) 131 | 132 | # Редактирование методом POST 133 | def post(self, request, *args, **kwargs): 134 | """ 135 | Update the account details of the authenticated user. 136 | 137 | Args: 138 | - request (Request): The Django request object. 139 | 140 | Returns: 141 | - JsonResponse: The response indicating the status of the operation and any errors. 142 | """ 143 | if not request.user.is_authenticated: 144 | return JsonResponse({'Status': False, 'Error': 'Log in required'}, status=403) 145 | # проверяем обязательные аргументы 146 | 147 | if 'password' in request.data: 148 | errors = {} 149 | # проверяем пароль на сложность 150 | try: 151 | validate_password(request.data['password']) 152 | except Exception as password_error: 153 | error_array = [] 154 | # noinspection PyTypeChecker 155 | for item in password_error: 156 | error_array.append(item) 157 | return JsonResponse({'Status': False, 'Errors': {'password': error_array}}) 158 | else: 159 | request.user.set_password(request.data['password']) 160 | 161 | # проверяем остальные данные 162 | user_serializer = UserSerializer(request.user, data=request.data, partial=True) 163 | if user_serializer.is_valid(): 164 | user_serializer.save() 165 | return JsonResponse({'Status': True}) 166 | else: 167 | return JsonResponse({'Status': False, 'Errors': user_serializer.errors}) 168 | 169 | 170 | class LoginAccount(APIView): 171 | """ 172 | Класс для авторизации пользователей 173 | """ 174 | 175 | # Авторизация методом POST 176 | def post(self, request, *args, **kwargs): 177 | """ 178 | Authenticate a user. 179 | 180 | Args: 181 | request (Request): The Django request object. 182 | 183 | Returns: 184 | JsonResponse: The response indicating the status of the operation and any errors. 185 | """ 186 | if {'email', 'password'}.issubset(request.data): 187 | user = authenticate(request, username=request.data['email'], password=request.data['password']) 188 | 189 | if user is not None: 190 | if user.is_active: 191 | token, _ = Token.objects.get_or_create(user=user) 192 | 193 | return JsonResponse({'Status': True, 'Token': token.key}) 194 | 195 | return JsonResponse({'Status': False, 'Errors': 'Не удалось авторизовать'}) 196 | 197 | return JsonResponse({'Status': False, 'Errors': 'Не указаны все необходимые аргументы'}) 198 | 199 | 200 | class CategoryView(ListAPIView): 201 | """ 202 | Класс для просмотра категорий 203 | """ 204 | queryset = Category.objects.all() 205 | serializer_class = CategorySerializer 206 | 207 | 208 | class ShopView(ListAPIView): 209 | """ 210 | Класс для просмотра списка магазинов 211 | """ 212 | queryset = Shop.objects.filter(state=True) 213 | serializer_class = ShopSerializer 214 | 215 | 216 | class ProductInfoView(APIView): 217 | """ 218 | A class for searching products. 219 | 220 | Methods: 221 | - get: Retrieve the product information based on the specified filters. 222 | 223 | Attributes: 224 | - None 225 | """ 226 | 227 | def get(self, request: Request, *args, **kwargs): 228 | """ 229 | Retrieve the product information based on the specified filters. 230 | 231 | Args: 232 | - request (Request): The Django request object. 233 | 234 | Returns: 235 | - Response: The response containing the product information. 236 | """ 237 | query = Q(shop__state=True) 238 | shop_id = request.query_params.get('shop_id') 239 | category_id = request.query_params.get('category_id') 240 | 241 | if shop_id: 242 | query = query & Q(shop_id=shop_id) 243 | 244 | if category_id: 245 | query = query & Q(product__category_id=category_id) 246 | 247 | # фильтруем и отбрасываем дуликаты 248 | queryset = ProductInfo.objects.filter( 249 | query).select_related( 250 | 'shop', 'product__category').prefetch_related( 251 | 'product_parameters__parameter').distinct() 252 | 253 | serializer = ProductInfoSerializer(queryset, many=True) 254 | 255 | return Response(serializer.data) 256 | 257 | 258 | class BasketView(APIView): 259 | """ 260 | A class for managing the user's shopping basket. 261 | 262 | Methods: 263 | - get: Retrieve the items in the user's basket. 264 | - post: Add an item to the user's basket. 265 | - put: Update the quantity of an item in the user's basket. 266 | - delete: Remove an item from the user's basket. 267 | 268 | Attributes: 269 | - None 270 | """ 271 | 272 | # получить корзину 273 | def get(self, request, *args, **kwargs): 274 | """ 275 | Retrieve the items in the user's basket. 276 | 277 | Args: 278 | - request (Request): The Django request object. 279 | 280 | Returns: 281 | - Response: The response containing the items in the user's basket. 282 | """ 283 | if not request.user.is_authenticated: 284 | return JsonResponse({'Status': False, 'Error': 'Log in required'}, status=403) 285 | basket = Order.objects.filter( 286 | user_id=request.user.id, state='basket').prefetch_related( 287 | 'ordered_items__product_info__product__category', 288 | 'ordered_items__product_info__product_parameters__parameter').annotate( 289 | total_sum=Sum(F('ordered_items__quantity') * F('ordered_items__product_info__price'))).distinct() 290 | 291 | serializer = OrderSerializer(basket, many=True) 292 | return Response(serializer.data) 293 | 294 | # редактировать корзину 295 | def post(self, request, *args, **kwargs): 296 | """ 297 | Add an items to the user's basket. 298 | 299 | Args: 300 | - request (Request): The Django request object. 301 | 302 | Returns: 303 | - JsonResponse: The response indicating the status of the operation and any errors. 304 | """ 305 | if not request.user.is_authenticated: 306 | return JsonResponse({'Status': False, 'Error': 'Log in required'}, status=403) 307 | 308 | items_sting = request.data.get('items') 309 | if items_sting: 310 | try: 311 | items_dict = load_json(items_sting) 312 | except ValueError: 313 | return JsonResponse({'Status': False, 'Errors': 'Неверный формат запроса'}) 314 | else: 315 | basket, _ = Order.objects.get_or_create(user_id=request.user.id, state='basket') 316 | objects_created = 0 317 | for order_item in items_dict: 318 | order_item.update({'order': basket.id}) 319 | serializer = OrderItemSerializer(data=order_item) 320 | if serializer.is_valid(): 321 | try: 322 | serializer.save() 323 | except IntegrityError as error: 324 | return JsonResponse({'Status': False, 'Errors': str(error)}) 325 | else: 326 | objects_created += 1 327 | 328 | else: 329 | 330 | return JsonResponse({'Status': False, 'Errors': serializer.errors}) 331 | 332 | return JsonResponse({'Status': True, 'Создано объектов': objects_created}) 333 | return JsonResponse({'Status': False, 'Errors': 'Не указаны все необходимые аргументы'}) 334 | 335 | # удалить товары из корзины 336 | def delete(self, request, *args, **kwargs): 337 | """ 338 | Remove items from the user's basket. 339 | 340 | Args: 341 | - request (Request): The Django request object. 342 | 343 | Returns: 344 | - JsonResponse: The response indicating the status of the operation and any errors. 345 | """ 346 | if not request.user.is_authenticated: 347 | return JsonResponse({'Status': False, 'Error': 'Log in required'}, status=403) 348 | 349 | items_sting = request.data.get('items') 350 | if items_sting: 351 | items_list = items_sting.split(',') 352 | basket, _ = Order.objects.get_or_create(user_id=request.user.id, state='basket') 353 | query = Q() 354 | objects_deleted = False 355 | for order_item_id in items_list: 356 | if order_item_id.isdigit(): 357 | query = query | Q(order_id=basket.id, id=order_item_id) 358 | objects_deleted = True 359 | 360 | if objects_deleted: 361 | deleted_count = OrderItem.objects.filter(query).delete()[0] 362 | return JsonResponse({'Status': True, 'Удалено объектов': deleted_count}) 363 | return JsonResponse({'Status': False, 'Errors': 'Не указаны все необходимые аргументы'}) 364 | 365 | # добавить позиции в корзину 366 | def put(self, request, *args, **kwargs): 367 | """ 368 | Update the items in the user's basket. 369 | 370 | Args: 371 | - request (Request): The Django request object. 372 | 373 | Returns: 374 | - JsonResponse: The response indicating the status of the operation and any errors. 375 | """ 376 | if not request.user.is_authenticated: 377 | return JsonResponse({'Status': False, 'Error': 'Log in required'}, status=403) 378 | 379 | items_sting = request.data.get('items') 380 | if items_sting: 381 | try: 382 | items_dict = load_json(items_sting) 383 | except ValueError: 384 | return JsonResponse({'Status': False, 'Errors': 'Неверный формат запроса'}) 385 | else: 386 | basket, _ = Order.objects.get_or_create(user_id=request.user.id, state='basket') 387 | objects_updated = 0 388 | for order_item in items_dict: 389 | if type(order_item['id']) == int and type(order_item['quantity']) == int: 390 | objects_updated += OrderItem.objects.filter(order_id=basket.id, id=order_item['id']).update( 391 | quantity=order_item['quantity']) 392 | 393 | return JsonResponse({'Status': True, 'Обновлено объектов': objects_updated}) 394 | return JsonResponse({'Status': False, 'Errors': 'Не указаны все необходимые аргументы'}) 395 | 396 | 397 | class PartnerUpdate(APIView): 398 | """ 399 | A class for updating partner information. 400 | 401 | Methods: 402 | - post: Update the partner information. 403 | 404 | Attributes: 405 | - None 406 | """ 407 | 408 | def post(self, request, *args, **kwargs): 409 | """ 410 | Update the partner price list information. 411 | 412 | Args: 413 | - request (Request): The Django request object. 414 | 415 | Returns: 416 | - JsonResponse: The response indicating the status of the operation and any errors. 417 | """ 418 | if not request.user.is_authenticated: 419 | return JsonResponse({'Status': False, 'Error': 'Log in required'}, status=403) 420 | 421 | if request.user.type != 'shop': 422 | return JsonResponse({'Status': False, 'Error': 'Только для магазинов'}, status=403) 423 | 424 | url = request.data.get('url') 425 | if url: 426 | validate_url = URLValidator() 427 | try: 428 | validate_url(url) 429 | except ValidationError as e: 430 | return JsonResponse({'Status': False, 'Error': str(e)}) 431 | else: 432 | stream = get(url).content 433 | 434 | data = load_yaml(stream, Loader=Loader) 435 | 436 | shop, _ = Shop.objects.get_or_create(name=data['shop'], user_id=request.user.id) 437 | for category in data['categories']: 438 | category_object, _ = Category.objects.get_or_create(id=category['id'], name=category['name']) 439 | category_object.shops.add(shop.id) 440 | category_object.save() 441 | ProductInfo.objects.filter(shop_id=shop.id).delete() 442 | for item in data['goods']: 443 | product, _ = Product.objects.get_or_create(name=item['name'], category_id=item['category']) 444 | 445 | product_info = ProductInfo.objects.create(product_id=product.id, 446 | external_id=item['id'], 447 | model=item['model'], 448 | price=item['price'], 449 | price_rrc=item['price_rrc'], 450 | quantity=item['quantity'], 451 | shop_id=shop.id) 452 | for name, value in item['parameters'].items(): 453 | parameter_object, _ = Parameter.objects.get_or_create(name=name) 454 | ProductParameter.objects.create(product_info_id=product_info.id, 455 | parameter_id=parameter_object.id, 456 | value=value) 457 | 458 | return JsonResponse({'Status': True}) 459 | 460 | return JsonResponse({'Status': False, 'Errors': 'Не указаны все необходимые аргументы'}) 461 | 462 | 463 | class PartnerState(APIView): 464 | """ 465 | A class for managing partner state. 466 | 467 | Methods: 468 | - get: Retrieve the state of the partner. 469 | 470 | Attributes: 471 | - None 472 | """ 473 | # получить текущий статус 474 | def get(self, request, *args, **kwargs): 475 | """ 476 | Retrieve the state of the partner. 477 | 478 | Args: 479 | - request (Request): The Django request object. 480 | 481 | Returns: 482 | - Response: The response containing the state of the partner. 483 | """ 484 | if not request.user.is_authenticated: 485 | return JsonResponse({'Status': False, 'Error': 'Log in required'}, status=403) 486 | 487 | if request.user.type != 'shop': 488 | return JsonResponse({'Status': False, 'Error': 'Только для магазинов'}, status=403) 489 | 490 | shop = request.user.shop 491 | serializer = ShopSerializer(shop) 492 | return Response(serializer.data) 493 | 494 | # изменить текущий статус 495 | def post(self, request, *args, **kwargs): 496 | """ 497 | Update the state of a partner. 498 | 499 | Args: 500 | - request (Request): The Django request object. 501 | 502 | Returns: 503 | - JsonResponse: The response indicating the status of the operation and any errors. 504 | """ 505 | if not request.user.is_authenticated: 506 | return JsonResponse({'Status': False, 'Error': 'Log in required'}, status=403) 507 | 508 | if request.user.type != 'shop': 509 | return JsonResponse({'Status': False, 'Error': 'Только для магазинов'}, status=403) 510 | state = request.data.get('state') 511 | if state: 512 | try: 513 | Shop.objects.filter(user_id=request.user.id).update(state=strtobool(state)) 514 | return JsonResponse({'Status': True}) 515 | except ValueError as error: 516 | return JsonResponse({'Status': False, 'Errors': str(error)}) 517 | 518 | return JsonResponse({'Status': False, 'Errors': 'Не указаны все необходимые аргументы'}) 519 | 520 | 521 | class PartnerOrders(APIView): 522 | """ 523 | Класс для получения заказов поставщиками 524 | Methods: 525 | - get: Retrieve the orders associated with the authenticated partner. 526 | 527 | Attributes: 528 | - None 529 | """ 530 | 531 | def get(self, request, *args, **kwargs): 532 | """ 533 | Retrieve the orders associated with the authenticated partner. 534 | 535 | Args: 536 | - request (Request): The Django request object. 537 | 538 | Returns: 539 | - Response: The response containing the orders associated with the partner. 540 | """ 541 | if not request.user.is_authenticated: 542 | return JsonResponse({'Status': False, 'Error': 'Log in required'}, status=403) 543 | 544 | if request.user.type != 'shop': 545 | return JsonResponse({'Status': False, 'Error': 'Только для магазинов'}, status=403) 546 | 547 | order = Order.objects.filter( 548 | ordered_items__product_info__shop__user_id=request.user.id).exclude(state='basket').prefetch_related( 549 | 'ordered_items__product_info__product__category', 550 | 'ordered_items__product_info__product_parameters__parameter').select_related('contact').annotate( 551 | total_sum=Sum(F('ordered_items__quantity') * F('ordered_items__product_info__price'))).distinct() 552 | 553 | serializer = OrderSerializer(order, many=True) 554 | return Response(serializer.data) 555 | 556 | 557 | class ContactView(APIView): 558 | """ 559 | A class for managing contact information. 560 | 561 | Methods: 562 | - get: Retrieve the contact information of the authenticated user. 563 | - post: Create a new contact for the authenticated user. 564 | - put: Update the contact information of the authenticated user. 565 | - delete: Delete the contact of the authenticated user. 566 | 567 | Attributes: 568 | - None 569 | """ 570 | 571 | # получить мои контакты 572 | def get(self, request, *args, **kwargs): 573 | """ 574 | Retrieve the contact information of the authenticated user. 575 | 576 | Args: 577 | - request (Request): The Django request object. 578 | 579 | Returns: 580 | - Response: The response containing the contact information. 581 | """ 582 | if not request.user.is_authenticated: 583 | return JsonResponse({'Status': False, 'Error': 'Log in required'}, status=403) 584 | contact = Contact.objects.filter( 585 | user_id=request.user.id) 586 | serializer = ContactSerializer(contact, many=True) 587 | return Response(serializer.data) 588 | 589 | # добавить новый контакт 590 | def post(self, request, *args, **kwargs): 591 | """ 592 | Create a new contact for the authenticated user. 593 | 594 | Args: 595 | - request (Request): The Django request object. 596 | 597 | Returns: 598 | - JsonResponse: The response indicating the status of the operation and any errors. 599 | """ 600 | if not request.user.is_authenticated: 601 | return JsonResponse({'Status': False, 'Error': 'Log in required'}, status=403) 602 | 603 | if {'city', 'street', 'phone'}.issubset(request.data): 604 | request.data._mutable = True 605 | request.data.update({'user': request.user.id}) 606 | serializer = ContactSerializer(data=request.data) 607 | 608 | if serializer.is_valid(): 609 | serializer.save() 610 | return JsonResponse({'Status': True}) 611 | else: 612 | return JsonResponse({'Status': False, 'Errors': serializer.errors}) 613 | 614 | return JsonResponse({'Status': False, 'Errors': 'Не указаны все необходимые аргументы'}) 615 | 616 | # удалить контакт 617 | def delete(self, request, *args, **kwargs): 618 | """ 619 | Delete the contact of the authenticated user. 620 | 621 | Args: 622 | - request (Request): The Django request object. 623 | 624 | Returns: 625 | - JsonResponse: The response indicating the status of the operation and any errors. 626 | """ 627 | if not request.user.is_authenticated: 628 | return JsonResponse({'Status': False, 'Error': 'Log in required'}, status=403) 629 | 630 | items_sting = request.data.get('items') 631 | if items_sting: 632 | items_list = items_sting.split(',') 633 | query = Q() 634 | objects_deleted = False 635 | for contact_id in items_list: 636 | if contact_id.isdigit(): 637 | query = query | Q(user_id=request.user.id, id=contact_id) 638 | objects_deleted = True 639 | 640 | if objects_deleted: 641 | deleted_count = Contact.objects.filter(query).delete()[0] 642 | return JsonResponse({'Status': True, 'Удалено объектов': deleted_count}) 643 | return JsonResponse({'Status': False, 'Errors': 'Не указаны все необходимые аргументы'}) 644 | 645 | # редактировать контакт 646 | def put(self, request, *args, **kwargs): 647 | if not request.user.is_authenticated: 648 | """ 649 | Update the contact information of the authenticated user. 650 | 651 | Args: 652 | - request (Request): The Django request object. 653 | 654 | Returns: 655 | - JsonResponse: The response indicating the status of the operation and any errors. 656 | """ 657 | return JsonResponse({'Status': False, 'Error': 'Log in required'}, status=403) 658 | 659 | if 'id' in request.data: 660 | if request.data['id'].isdigit(): 661 | contact = Contact.objects.filter(id=request.data['id'], user_id=request.user.id).first() 662 | print(contact) 663 | if contact: 664 | serializer = ContactSerializer(contact, data=request.data, partial=True) 665 | if serializer.is_valid(): 666 | serializer.save() 667 | return JsonResponse({'Status': True}) 668 | else: 669 | return JsonResponse({'Status': False, 'Errors': serializer.errors}) 670 | 671 | return JsonResponse({'Status': False, 'Errors': 'Не указаны все необходимые аргументы'}) 672 | 673 | 674 | class OrderView(APIView): 675 | """ 676 | Класс для получения и размешения заказов пользователями 677 | Methods: 678 | - get: Retrieve the details of a specific order. 679 | - post: Create a new order. 680 | - put: Update the details of a specific order. 681 | - delete: Delete a specific order. 682 | 683 | Attributes: 684 | - None 685 | """ 686 | 687 | # получить мои заказы 688 | def get(self, request, *args, **kwargs): 689 | """ 690 | Retrieve the details of user orders. 691 | 692 | Args: 693 | - request (Request): The Django request object. 694 | 695 | Returns: 696 | - Response: The response containing the details of the order. 697 | """ 698 | if not request.user.is_authenticated: 699 | return JsonResponse({'Status': False, 'Error': 'Log in required'}, status=403) 700 | order = Order.objects.filter( 701 | user_id=request.user.id).exclude(state='basket').prefetch_related( 702 | 'ordered_items__product_info__product__category', 703 | 'ordered_items__product_info__product_parameters__parameter').select_related('contact').annotate( 704 | total_sum=Sum(F('ordered_items__quantity') * F('ordered_items__product_info__price'))).distinct() 705 | 706 | serializer = OrderSerializer(order, many=True) 707 | return Response(serializer.data) 708 | 709 | # разместить заказ из корзины 710 | def post(self, request, *args, **kwargs): 711 | """ 712 | Put an order and send a notification. 713 | 714 | Args: 715 | - request (Request): The Django request object. 716 | 717 | Returns: 718 | - JsonResponse: The response indicating the status of the operation and any errors. 719 | """ 720 | if not request.user.is_authenticated: 721 | return JsonResponse({'Status': False, 'Error': 'Log in required'}, status=403) 722 | 723 | if {'id', 'contact'}.issubset(request.data): 724 | if request.data['id'].isdigit(): 725 | try: 726 | is_updated = Order.objects.filter( 727 | user_id=request.user.id, id=request.data['id']).update( 728 | contact_id=request.data['contact'], 729 | state='new') 730 | except IntegrityError as error: 731 | print(error) 732 | return JsonResponse({'Status': False, 'Errors': 'Неправильно указаны аргументы'}) 733 | else: 734 | if is_updated: 735 | new_order.send(sender=self.__class__, user_id=request.user.id) 736 | return JsonResponse({'Status': True}) 737 | 738 | return JsonResponse({'Status': False, 'Errors': 'Не указаны все необходимые аргументы'}) 739 | -------------------------------------------------------------------------------- /reference/netology_pd_diplom/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'netology_pd_diplom.settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /reference/netology_pd_diplom/netology_pd_diplom/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netology-code/python-final-diplom/dd11860cf2f16b890fe5031ec565482658cb7a96/reference/netology_pd_diplom/netology_pd_diplom/__init__.py -------------------------------------------------------------------------------- /reference/netology_pd_diplom/netology_pd_diplom/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for netology_pd_diplom project. 3 | 4 | Generated by 'django-admin startproject' using Django 5.0. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.2/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | # Quick-start development settings - unsuitable for production 19 | # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ 20 | 21 | # SECURITY WARNING: keep the secret key used in production secret! 22 | SECRET_KEY = '=hs6$#5om031nujz4staql9mbuste=!dc^6)4opsjq!vvjxzj@' 23 | 24 | # SECURITY WARNING: don't run with debug turned on in production! 25 | DEBUG = True 26 | 27 | ALLOWED_HOSTS = ['*'] 28 | 29 | # Application definition 30 | 31 | INSTALLED_APPS = [ 32 | 'django.contrib.admin', 33 | 'django.contrib.auth', 34 | 'django.contrib.contenttypes', 35 | 'django.contrib.sessions', 36 | 'django.contrib.messages', 37 | 'django.contrib.staticfiles', 38 | 'rest_framework', 39 | 'rest_framework.authtoken', 40 | 'django_rest_passwordreset', 41 | 'backend', 42 | ] 43 | 44 | MIDDLEWARE = [ 45 | 'django.middleware.security.SecurityMiddleware', 46 | 'django.contrib.sessions.middleware.SessionMiddleware', 47 | 'django.middleware.common.CommonMiddleware', 48 | 'django.middleware.csrf.CsrfViewMiddleware', 49 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 50 | 'django.contrib.messages.middleware.MessageMiddleware', 51 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 52 | ] 53 | 54 | ROOT_URLCONF = 'netology_pd_diplom.urls' 55 | 56 | TEMPLATES = [ 57 | { 58 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 59 | 'DIRS': [os.path.join(BASE_DIR, 'templates')] 60 | , 61 | 'APP_DIRS': True, 62 | 'OPTIONS': { 63 | 'context_processors': [ 64 | 'django.template.context_processors.debug', 65 | 'django.template.context_processors.request', 66 | 'django.contrib.auth.context_processors.auth', 67 | 'django.contrib.messages.context_processors.messages', 68 | ], 69 | }, 70 | }, 71 | ] 72 | 73 | WSGI_APPLICATION = 'netology_pd_diplom.wsgi.application' 74 | 75 | # Database 76 | # https://docs.djangoproject.com/en/2.2/ref/settings/#databases 77 | 78 | DATABASES = { 79 | 80 | 'default': { 81 | 'ENGINE': 'django.db.backends.sqlite3', 82 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 83 | } 84 | 85 | 86 | } 87 | 88 | # Password validation 89 | # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators 90 | 91 | AUTH_PASSWORD_VALIDATORS = [ 92 | { 93 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 94 | }, 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 97 | }, 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 100 | }, 101 | { 102 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 103 | }, 104 | ] 105 | 106 | # Internationalization 107 | # https://docs.djangoproject.com/en/2.2/topics/i18n/ 108 | 109 | LANGUAGE_CODE = 'en-us' 110 | 111 | TIME_ZONE = 'UTC' 112 | 113 | USE_I18N = True 114 | 115 | USE_L10N = True 116 | 117 | USE_TZ = True 118 | 119 | # Static files (CSS, JavaScript, Images) 120 | # https://docs.djangoproject.com/en/2.2/howto/static-files/ 121 | 122 | STATIC_URL = '/static/' 123 | 124 | AUTH_USER_MODEL = 'backend.User' 125 | 126 | EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' 127 | # EMAIL_USE_TLS = True 128 | 129 | EMAIL_HOST = 'smtp.mail.ru' 130 | 131 | EMAIL_HOST_USER = 'netology.diplom@mail.ru' 132 | EMAIL_HOST_PASSWORD = 'CLdm7yW4U9nivz9mbexu' 133 | EMAIL_PORT = '465' 134 | EMAIL_USE_SSL = True 135 | SERVER_EMAIL = EMAIL_HOST_USER 136 | 137 | REST_FRAMEWORK = { 138 | 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 139 | 'PAGE_SIZE': 40, 140 | 141 | 'DEFAULT_RENDERER_CLASSES': ( 142 | 'rest_framework.renderers.JSONRenderer', 143 | 'rest_framework.renderers.BrowsableAPIRenderer', 144 | 145 | ), 146 | 147 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 148 | 149 | 'rest_framework.authentication.TokenAuthentication', 150 | ), 151 | 152 | } 153 | 154 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 155 | -------------------------------------------------------------------------------- /reference/netology_pd_diplom/netology_pd_diplom/urls.py: -------------------------------------------------------------------------------- 1 | """netology_pd_diplom URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.2/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path, include 18 | 19 | urlpatterns = [ 20 | path('admin/', admin.site.urls), 21 | path('api/v1/', include('backend.urls', namespace='backend')) 22 | ] 23 | -------------------------------------------------------------------------------- /reference/netology_pd_diplom/netology_pd_diplom/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for netology_pd_diplom project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'netology_pd_diplom.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /reference/netology_pd_diplom/requirements.txt: -------------------------------------------------------------------------------- 1 | django~=5.0 2 | djangorestframework~=3.14.0 3 | celery~=5.3.0 4 | requests~=2.31.0 5 | ujson~=5.9.0 6 | pyyaml~=6.0.0 7 | django-rest-passwordreset>=1.3.0 8 | -------------------------------------------------------------------------------- /reference/screens.md: -------------------------------------------------------------------------------- 1 | # Точки входа API сервиса 2 | 3 | [Пример API Сервиса для магазина](./netology_pd_diplom/) 4 | 5 | ### Вход 6 | Аргументы для отправки API запроса 7 | * поля 8 | - Email 9 | - Пароль 10 | 11 | 12 | ### Регистрация 13 | Аргументы для отправки API запроса 14 | * поля 15 | - Фамилия 16 | - Имя 17 | - Email 18 | - Пароль 19 | 20 | ### Запрос списка товаров с возможностью фильтрации и поиска 21 | 22 | ### JSON поля товара 23 | * наименование 24 | * описание 25 | * поставщик 26 | * характеристики 27 | * цена 28 | * количество 29 | 30 | ### Корзина с возможностью добавления и удаления товаров 31 | Список товаров с полями 32 | * Наименование товара 33 | * Магазин 34 | * Цена 35 | * Количество 36 | * Сумма 37 | 38 | 39 | 40 | ### API запрос добавления контакта 41 | * Аргументы для отправки 42 | - Фамилия 43 | - Имя 44 | - Отчество 45 | - Email 46 | - Телефон 47 | - Адрес 48 | + Город 49 | + Улица 50 | + Дом 51 | + Корпус 52 | + Строение 53 | + Квартира 54 | 55 | 56 | ### API запрос на подтверждение заказа 57 | * ID корзины 58 | * ID контакта 59 | 60 | 61 | ### Получение статуса и истории заказов 62 | 63 | 64 | * Номер 65 | * Дата 66 | * Сумма 67 | * Статус 68 | 69 | -------------------------------------------------------------------------------- /reference/service.md: -------------------------------------------------------------------------------- 1 | # Описание сервиса 2 | 3 | Сервис предоставляет возможность заказа товаров из нескольких магазинов. 4 | Каталог товаров, информация о ценах и наличии загружаются из файлов 5 | единого формата. 6 | 7 | Пользователь собирает заказ (корзину), добавляя товары разных магазинов, 8 | представленные в едином каталоге. Если один товар есть в наличии 9 | в нескольких магазинах, можно выбрать какой из них добавить. 10 | Так как цены в магазинах могут отличаться, это повлияет и на сумму 11 | заказа. 12 | 13 | После подтверждения заказа на странице подтверждения, заказ сохраняется 14 | в базе со статусом "Новый". 15 | 16 | *adv: В админке можно просмотреть все заказы и изменить их статус* 17 | 18 | При любом изменении статуса заказа, пользователю на email отправляется 19 | уведомление об изменении. 20 | 21 | ## Основные сущности 22 | 23 | 1. Магазин 24 | 2. Категория 25 | 3. Товар 26 | 4. Заказ 27 | 5. Контакты пользователя 28 | 29 | ## Магазин 30 | 31 | У магазина есть url или имя файла, из которого будут загружаться товары. 32 | 33 | ## Категория 34 | 35 | Категории связаны с магазинами через m2m. 36 | Вложенных категорий не предусмотрено. 37 | 38 | ## Товар 39 | 40 | У каждого товара есть несколько цен - по каждому магазину. 41 | 42 | ## Заказ 43 | 44 | Заказ включает в себя список товаров, с указанием магазинов и цен. 45 | 46 | ## Контакты пользователя 47 | 48 | Контакты могут быть двух типов: 49 | * телефон (1 запись) 50 | * адрес (до 5 записей) 51 | 52 | ## Порядок действий пользователя для заказа 53 | 54 | 1. Вход/регистрация 55 | 2. Выбор фильтров в каталоге товаров 56 | 1. Выбор магазинов (по необходимости) 57 | 2. Выбор категории (по необходимости) 58 | 3. Выбор товара 59 | 4. Выбор количества, цены/магазина 60 | 5. Экран "Корзина" 61 | 6. Экран "Подтверждение заказа" 62 | 7. Экран "Спасибо за заказ" 63 | 64 | После подтверждения заказа, нужно отправить email пользователю (покупателю) 65 | и администратору (для исполнения заказа) -------------------------------------------------------------------------------- /reference/step-1.md: -------------------------------------------------------------------------------- 1 | # Этап 1. Создание и настройка проекта. 2 | 3 | ## Критерии достижения 4 | 5 | 1. Вы имеете актуальный код данного репозитория на рабочем компьютере; 6 | 2. У вас создан django-проект и он запускается без ошибок. 7 | 8 | ## Необходимые инструменты 9 | 10 | 1. Система контроля версий Git; 11 | 2. Аккаунт на Github; 12 | 3. Установленный локально (на вашем компьютере) Python >= 3.10. 13 | 14 | ## Порядок выполнения 15 | 16 | 1. Создайте копию данного проекта, нажав кнопку Fork в 17 | правом верхнем углу этой страницы 18 | 2. Клонируйте репозиторий командой в вашем терминале 19 | ```git clone адрес_вашего_репозитория``` 20 | 3. Создайте виртуальное окружение для своего проекта 21 | ```virtualenv --python=python3.10 env``` 22 | 4. Установите все необходимые пакеты проекта командой 23 | ```pip install -r requirements.txt``` 24 | 5. Создайте проект django, в случае если создаете проект с нуля без использования примера 25 | ```django-admin startproject orders```, ```django-admin startapp backend``` 26 | -------------------------------------------------------------------------------- /reference/step-2.md: -------------------------------------------------------------------------------- 1 | # Этап 2. Проработка моделей данных 2 | 3 | ## Критерии достижения: 4 | 5 | 1. Созданы модели и их дополнительные методы. 6 | 7 | ## Порядок выполнения 8 | Данная структура может быть изменена под конретные цели вашего дипломного проекта. 9 | 10 | 1. Создать модели: 11 | 1. Shop 12 | - name 13 | - url 14 | 2. Category 15 | - shops (m2m) 16 | - name 17 | 3. Product 18 | - category 19 | - name 20 | 4. ProductInfo 21 | - product 22 | - shop 23 | - name 24 | - quantity 25 | - price 26 | - price_rrc 27 | 5. Parameter 28 | - name 29 | 6. ProductParameter 30 | - product_info 31 | - parameter 32 | - value 33 | 7. Order 34 | - user 35 | - dt 36 | - status 37 | 8. OrderItem 38 | - order 39 | - product 40 | - shop 41 | - quantity 42 | 9. Contact 43 | - type 44 | - user 45 | - value 46 | 47 | 2. Пример реального кода из Python: 48 | 49 | ```python 50 | from django.contrib.auth.base_user import BaseUserManager 51 | from django.contrib.auth.models import AbstractUser 52 | from django.contrib.auth.validators import UnicodeUsernameValidator 53 | from django.db import models 54 | from django.utils.translation import gettext_lazy as _ 55 | from django_rest_passwordreset.tokens import get_token_generator 56 | 57 | STATE_CHOICES = ( 58 | ('basket', 'Статус корзины'), 59 | ('new', 'Новый'), 60 | ('confirmed', 'Подтвержден'), 61 | ('assembled', 'Собран'), 62 | ('sent', 'Отправлен'), 63 | ('delivered', 'Доставлен'), 64 | ('canceled', 'Отменен'), 65 | ) 66 | 67 | USER_TYPE_CHOICES = ( 68 | ('shop', 'Магазин'), 69 | ('buyer', 'Покупатель'), 70 | 71 | ) 72 | 73 | 74 | # Create your models here. 75 | 76 | 77 | class UserManager(BaseUserManager): 78 | """ 79 | Миксин для управления пользователями 80 | """ 81 | use_in_migrations = True 82 | 83 | def _create_user(self, email, password, **extra_fields): 84 | """ 85 | Create and save a user with the given username, email, and password. 86 | """ 87 | if not email: 88 | raise ValueError('The given email must be set') 89 | email = self.normalize_email(email) 90 | user = self.model(email=email, **extra_fields) 91 | user.set_password(password) 92 | user.save(using=self._db) 93 | return user 94 | 95 | def create_user(self, email, password=None, **extra_fields): 96 | extra_fields.setdefault('is_staff', False) 97 | extra_fields.setdefault('is_superuser', False) 98 | return self._create_user(email, password, **extra_fields) 99 | 100 | def create_superuser(self, email, password, **extra_fields): 101 | extra_fields.setdefault('is_staff', True) 102 | extra_fields.setdefault('is_superuser', True) 103 | 104 | if extra_fields.get('is_staff') is not True: 105 | raise ValueError('Superuser must have is_staff=True.') 106 | if extra_fields.get('is_superuser') is not True: 107 | raise ValueError('Superuser must have is_superuser=True.') 108 | 109 | return self._create_user(email, password, **extra_fields) 110 | 111 | 112 | class User(AbstractUser): 113 | """ 114 | Стандартная модель пользователей 115 | """ 116 | REQUIRED_FIELDS = [] 117 | objects = UserManager() 118 | USERNAME_FIELD = 'email' 119 | email = models.EmailField(_('email address'), unique=True) 120 | company = models.CharField(verbose_name='Компания', max_length=40, blank=True) 121 | position = models.CharField(verbose_name='Должность', max_length=40, blank=True) 122 | username_validator = UnicodeUsernameValidator() 123 | username = models.CharField( 124 | _('username'), 125 | max_length=150, 126 | help_text=_('Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.'), 127 | validators=[username_validator], 128 | error_messages={ 129 | 'unique': _("A user with that username already exists."), 130 | }, 131 | ) 132 | is_active = models.BooleanField( 133 | _('active'), 134 | default=False, 135 | help_text=_( 136 | 'Designates whether this user should be treated as active. ' 137 | 'Unselect this instead of deleting accounts.' 138 | ), 139 | ) 140 | type = models.CharField(verbose_name='Тип пользователя', choices=USER_TYPE_CHOICES, max_length=5, default='buyer') 141 | 142 | def __str__(self): 143 | return f'{self.first_name} {self.last_name}' 144 | 145 | class Meta: 146 | verbose_name = 'Пользователь' 147 | verbose_name_plural = "Список пользователей" 148 | ordering = ('email',) 149 | 150 | 151 | class Shop(models.Model): 152 | name = models.CharField(max_length=50, verbose_name='Название') 153 | url = models.URLField(verbose_name='Ссылка', null=True, blank=True) 154 | user = models.OneToOneField(User, verbose_name='Пользователь', 155 | blank=True, null=True, 156 | on_delete=models.CASCADE) 157 | state = models.BooleanField(verbose_name='статус получения заказов', default=True) 158 | 159 | # filename 160 | 161 | class Meta: 162 | verbose_name = 'Магазин' 163 | verbose_name_plural = "Список магазинов" 164 | ordering = ('-name',) 165 | 166 | def __str__(self): 167 | return self.name 168 | 169 | 170 | class Category(models.Model): 171 | name = models.CharField(max_length=40, verbose_name='Название') 172 | shops = models.ManyToManyField(Shop, verbose_name='Магазины', related_name='categories', blank=True) 173 | 174 | class Meta: 175 | verbose_name = 'Категория' 176 | verbose_name_plural = "Список категорий" 177 | ordering = ('-name',) 178 | 179 | def __str__(self): 180 | return self.name 181 | 182 | 183 | class Product(models.Model): 184 | name = models.CharField(max_length=80, verbose_name='Название') 185 | category = models.ForeignKey(Category, verbose_name='Категория', related_name='products', blank=True, 186 | on_delete=models.CASCADE) 187 | 188 | class Meta: 189 | verbose_name = 'Продукт' 190 | verbose_name_plural = "Список продуктов" 191 | ordering = ('-name',) 192 | 193 | def __str__(self): 194 | return self.name 195 | 196 | 197 | class ProductInfo(models.Model): 198 | model = models.CharField(max_length=80, verbose_name='Модель', blank=True) 199 | external_id = models.PositiveIntegerField(verbose_name='Внешний ИД') 200 | product = models.ForeignKey(Product, verbose_name='Продукт', related_name='product_infos', blank=True, 201 | on_delete=models.CASCADE) 202 | shop = models.ForeignKey(Shop, verbose_name='Магазин', related_name='product_infos', blank=True, 203 | on_delete=models.CASCADE) 204 | quantity = models.PositiveIntegerField(verbose_name='Количество') 205 | price = models.PositiveIntegerField(verbose_name='Цена') 206 | price_rrc = models.PositiveIntegerField(verbose_name='Рекомендуемая розничная цена') 207 | 208 | class Meta: 209 | verbose_name = 'Информация о продукте' 210 | verbose_name_plural = "Информационный список о продуктах" 211 | constraints = [ 212 | models.UniqueConstraint(fields=['product', 'shop', 'external_id'], name='unique_product_info'), 213 | ] 214 | 215 | 216 | class Parameter(models.Model): 217 | name = models.CharField(max_length=40, verbose_name='Название') 218 | 219 | class Meta: 220 | verbose_name = 'Имя параметра' 221 | verbose_name_plural = "Список имен параметров" 222 | ordering = ('-name',) 223 | 224 | def __str__(self): 225 | return self.name 226 | 227 | 228 | class ProductParameter(models.Model): 229 | product_info = models.ForeignKey(ProductInfo, verbose_name='Информация о продукте', 230 | related_name='product_parameters', blank=True, 231 | on_delete=models.CASCADE) 232 | parameter = models.ForeignKey(Parameter, verbose_name='Параметр', related_name='product_parameters', blank=True, 233 | on_delete=models.CASCADE) 234 | value = models.CharField(verbose_name='Значение', max_length=100) 235 | 236 | class Meta: 237 | verbose_name = 'Параметр' 238 | verbose_name_plural = "Список параметров" 239 | constraints = [ 240 | models.UniqueConstraint(fields=['product_info', 'parameter'], name='unique_product_parameter'), 241 | ] 242 | 243 | 244 | class Contact(models.Model): 245 | user = models.ForeignKey(User, verbose_name='Пользователь', 246 | related_name='contacts', blank=True, 247 | on_delete=models.CASCADE) 248 | 249 | city = models.CharField(max_length=50, verbose_name='Город') 250 | street = models.CharField(max_length=100, verbose_name='Улица') 251 | house = models.CharField(max_length=15, verbose_name='Дом', blank=True) 252 | structure = models.CharField(max_length=15, verbose_name='Корпус', blank=True) 253 | building = models.CharField(max_length=15, verbose_name='Строение', blank=True) 254 | apartment = models.CharField(max_length=15, verbose_name='Квартира', blank=True) 255 | phone = models.CharField(max_length=20, verbose_name='Телефон') 256 | 257 | class Meta: 258 | verbose_name = 'Контакты пользователя' 259 | verbose_name_plural = "Список контактов пользователя" 260 | 261 | def __str__(self): 262 | return f'{self.city} {self.street} {self.house}' 263 | 264 | 265 | class Order(models.Model): 266 | user = models.ForeignKey(User, verbose_name='Пользователь', 267 | related_name='orders', blank=True, 268 | on_delete=models.CASCADE) 269 | dt = models.DateTimeField(auto_now_add=True) 270 | state = models.CharField(verbose_name='Статус', choices=STATE_CHOICES, max_length=15) 271 | contact = models.ForeignKey(Contact, verbose_name='Контакт', 272 | blank=True, null=True, 273 | on_delete=models.CASCADE) 274 | 275 | class Meta: 276 | verbose_name = 'Заказ' 277 | verbose_name_plural = "Список заказ" 278 | ordering = ('-dt',) 279 | 280 | def __str__(self): 281 | return str(self.dt) 282 | 283 | 284 | 285 | class OrderItem(models.Model): 286 | order = models.ForeignKey(Order, verbose_name='Заказ', related_name='ordered_items', blank=True, 287 | on_delete=models.CASCADE) 288 | 289 | product_info = models.ForeignKey(ProductInfo, verbose_name='Информация о продукте', related_name='ordered_items', 290 | blank=True, 291 | on_delete=models.CASCADE) 292 | quantity = models.PositiveIntegerField(verbose_name='Количество') 293 | 294 | class Meta: 295 | verbose_name = 'Заказанная позиция' 296 | verbose_name_plural = "Список заказанных позиций" 297 | constraints = [ 298 | models.UniqueConstraint(fields=['order_id', 'product_info'], name='unique_order_item'), 299 | ] 300 | 301 | 302 | class ConfirmEmailToken(models.Model): 303 | class Meta: 304 | verbose_name = 'Токен подтверждения Email' 305 | verbose_name_plural = 'Токены подтверждения Email' 306 | 307 | @staticmethod 308 | def generate_key(): 309 | """ generates a pseudo random code using os.urandom and binascii.hexlify """ 310 | return get_token_generator().generate_token() 311 | 312 | user = models.ForeignKey( 313 | User, 314 | related_name='confirm_email_tokens', 315 | on_delete=models.CASCADE, 316 | verbose_name=_("The User which is associated to this password reset token") 317 | ) 318 | 319 | created_at = models.DateTimeField( 320 | auto_now_add=True, 321 | verbose_name=_("When was this token generated") 322 | ) 323 | 324 | # Key field, though it is not the primary key of the model 325 | key = models.CharField( 326 | _("Key"), 327 | max_length=64, 328 | db_index=True, 329 | unique=True 330 | ) 331 | 332 | def save(self, *args, **kwargs): 333 | if not self.key: 334 | self.key = self.generate_key() 335 | return super(ConfirmEmailToken, self).save(*args, **kwargs) 336 | 337 | def __str__(self): 338 | return "Password reset token for user {user}".format(user=self.user) 339 | 340 | ``` 341 | 342 | -------------------------------------------------------------------------------- /reference/step-3.md: -------------------------------------------------------------------------------- 1 | # Этап 3. Реализация импорта товаров 2 | 3 | ## Критерии достижения: 4 | 5 | 1. Созданы функции загрузки товаров из приложенных файлов в модели Django. 6 | 2. Загружены товары из всех файлов для импорта. 7 | 8 | ## Реальный пример кода на Python: 9 | ```python 10 | 11 | class PartnerUpdate(APIView): 12 | """ 13 | Класс для обновления прайса от поставщика 14 | """ 15 | def post(self, request, *args, **kwargs): 16 | if not request.user.is_authenticated: 17 | return JsonResponse({'Status': False, 'Error': 'Log in required'}, status=403) 18 | 19 | if request.user.type != 'shop': 20 | return JsonResponse({'Status': False, 'Error': 'Только для магазинов'}, status=403) 21 | 22 | url = request.data.get('url') 23 | if url: 24 | validate_url = URLValidator() 25 | try: 26 | validate_url(url) 27 | except ValidationError as e: 28 | return JsonResponse({'Status': False, 'Error': str(e)}) 29 | else: 30 | stream = get(url).content 31 | 32 | data = load_yaml(stream, Loader=Loader) 33 | 34 | shop, _ = Shop.objects.get_or_create(name=data['shop'], user_id=request.user.id) 35 | for category in data['categories']: 36 | category_object, _ = Category.objects.get_or_create(id=category['id'], name=category['name']) 37 | category_object.shops.add(shop.id) 38 | category_object.save() 39 | ProductInfo.objects.filter(shop_id=shop.id).delete() 40 | for item in data['goods']: 41 | product, _ = Product.objects.get_or_create(name=item['name'], category_id=item['category']) 42 | 43 | product_info = ProductInfo.objects.create(product_id=product.id, 44 | external_id=item['id'], 45 | model=item['model'], 46 | price=item['price'], 47 | price_rrc=item['price_rrc'], 48 | quantity=item['quantity'], 49 | shop_id=shop.id) 50 | for name, value in item['parameters'].items(): 51 | parameter_object, _ = Parameter.objects.get_or_create(name=name) 52 | ProductParameter.objects.create(product_info_id=product_info.id, 53 | parameter_id=parameter_object.id, 54 | value=value) 55 | 56 | return JsonResponse({'Status': True}) 57 | 58 | return JsonResponse({'Status': False, 'Errors': 'Не указаны все необходимые аргументы'}) 59 | 60 | ``` -------------------------------------------------------------------------------- /reference/step-4.md: -------------------------------------------------------------------------------- 1 | # Этап 4. Реализация forms и views 2 | 3 | ## Критерии достижения: 4 | 5 | Реализованы API views для основных [точек входа](./screens.md) сервиса (без админки). 6 | - Вход 7 | - Регистрация 8 | - Список товаров 9 | - Корзина (просмотр, добавление, удаление) 10 | - Контакты (просмотр, добавление, удаление) 11 | - Подтверждение заказа 12 | - Заказы просмотр списка 13 | -------------------------------------------------------------------------------- /reference/step-5.md: -------------------------------------------------------------------------------- 1 | # Этап 5. Полностью готовый backend 2 | 3 | ## Критерии достижения: 4 | 5 | 1. Полностью работающие API Endpoint. 6 | 2. Корректно отрабатывает следующий сценарий: 7 | - пользователь может авторизоваться; 8 | - есть возможность отправки данных для регистрации и получения email с подтверждением регистрации; 9 | - пользователь может добавлять в корзину товары от разных магазинов; 10 | - пользователь может создавать контакты (адреса доставки) 11 | - пользователь может подтверждать заказ; 12 | - пользователь получает email с подтверждением после ввода адреса доставки; 13 | - пользователь может получать список заказов. 14 | -------------------------------------------------------------------------------- /reference/step-6-adv.md: -------------------------------------------------------------------------------- 1 | # Этап 6. Реализация админки склада через Django Admin 2 | 3 | ## Критерии достижения: 4 | * Страницы админки должны быть доступны для просмотра и взаимодействия 5 | * Все основные функциональности админки должны быть полностью реализованы и работать корректно 6 | * Админка должна быть интуитивно понятной и удобной для использования 7 | 8 | -------------------------------------------------------------------------------- /reference/step-7-adv.md: -------------------------------------------------------------------------------- 1 | # Этап 7. Вынос медленных методов в задачи Celery 2 | 3 | Критерии достижения: 4 | 5 | 1. Создано Celery-приложение c методами: 6 | - send_email 7 | - do_import 8 | 2. Создан view для запуска Celery-задачи do_import из админки. 9 | 10 | ## Реальный пример кода на Python: 11 | 12 | ```python 13 | 14 | def password_reset_token_created(sender, instance, reset_password_token, **kwargs): 15 | """ 16 | Отправляем письмо с токеном для сброса пароля 17 | When a token is created, an e-mail needs to be sent to the user 18 | :param sender: View Class that sent the signal 19 | :param instance: View Instance that sent the signal 20 | :param reset_password_token: Token Model Object 21 | :param kwargs: 22 | :return: 23 | """ 24 | # send an e-mail to the user 25 | 26 | msg = EmailMultiAlternatives( 27 | # title: 28 | f"Password Reset Token for {reset_password_token.user}", 29 | # message: 30 | reset_password_token.key, 31 | # from: 32 | settings.EMAIL_HOST_USER, 33 | # to: 34 | [reset_password_token.user.email] 35 | ) 36 | msg.send() 37 | 38 | 39 | ``` 40 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django~=5.0 2 | djangorestframework~=3.14.0 3 | celery~=5.3.0 4 | requests~=2.31.0 5 | ujson~=5.9.0 6 | pyyaml~=6.0.0 7 | django-rest-passwordreset>=1.3.0 8 | --------------------------------------------------------------------------------