jinja3kit 1.2.2__tar.gz → 1.2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {jinja3kit-1.2.2 → jinja3kit-1.2.4}/PKG-INFO +1 -1
- {jinja3kit-1.2.2 → jinja3kit-1.2.4}/pyproject.toml +1 -1
- jinja3kit-1.2.4/src/jinja3kit/__init__.py +1 -0
- jinja3kit-1.2.4/src/jinja3kit/cli.py +49 -0
- jinja3kit-1.2.4/src/jinja3kit/template/README_/320/227/320/220/320/237/320/243/320/241/320/232.md +57 -0
- {jinja3kit-1.2.2 → jinja3kit-1.2.4}/src/jinja3kit/template/app.py +11 -98
- jinja3kit-1.2.4/src/jinja3kit/template/db/full_init.sql +284 -0
- {jinja3kit-1.2.2 → jinja3kit-1.2.4}/src/jinja3kit/template/db/schema.sql +9 -27
- jinja3kit-1.2.4/src/jinja3kit/template/db/seed.sql +137 -0
- jinja3kit-1.2.4/src/jinja3kit/template/db.py +49 -0
- {jinja3kit-1.2.2 → jinja3kit-1.2.4}/src/jinja3kit/template/static/css/style.css +37 -21
- jinja3kit-1.2.4/src/jinja3kit/template/static/img/1.jpg +0 -0
- jinja3kit-1.2.4/src/jinja3kit/template/static/img/10.jpg +0 -0
- jinja3kit-1.2.4/src/jinja3kit/template/static/img/2.jpg +0 -0
- jinja3kit-1.2.4/src/jinja3kit/template/static/img/3.jpg +0 -0
- jinja3kit-1.2.4/src/jinja3kit/template/static/img/4.jpg +0 -0
- jinja3kit-1.2.4/src/jinja3kit/template/static/img/5.jpg +0 -0
- jinja3kit-1.2.4/src/jinja3kit/template/static/img/6.jpg +0 -0
- jinja3kit-1.2.4/src/jinja3kit/template/static/img/7.jpg +0 -0
- jinja3kit-1.2.4/src/jinja3kit/template/static/img/8.jpg +0 -0
- jinja3kit-1.2.4/src/jinja3kit/template/static/img/9.jpg +0 -0
- jinja3kit-1.2.4/src/jinja3kit/template/static/img/Icon.JPG +0 -0
- jinja3kit-1.2.4/src/jinja3kit/template/static/img/Icon.ico +0 -0
- jinja3kit-1.2.4/src/jinja3kit/template/static/img/Icon.png +0 -0
- {jinja3kit-1.2.2 → jinja3kit-1.2.4}/src/jinja3kit/template/templates/base.html +4 -5
- {jinja3kit-1.2.2 → jinja3kit-1.2.4}/src/jinja3kit/template/templates/error.html +0 -1
- {jinja3kit-1.2.2 → jinja3kit-1.2.4}/src/jinja3kit/template/templates/login.html +0 -1
- {jinja3kit-1.2.2 → jinja3kit-1.2.4}/src/jinja3kit/template/templates/order_form.html +1 -2
- {jinja3kit-1.2.2 → jinja3kit-1.2.4}/src/jinja3kit/template/templates/orders.html +1 -2
- {jinja3kit-1.2.2 → jinja3kit-1.2.4}/src/jinja3kit/template/templates/product_form.html +2 -3
- {jinja3kit-1.2.2 → jinja3kit-1.2.4}/src/jinja3kit/template/templates/products.html +32 -12
- {jinja3kit-1.2.2 → jinja3kit-1.2.4}/src/jinja3kit.egg-info/PKG-INFO +1 -1
- {jinja3kit-1.2.2 → jinja3kit-1.2.4}/src/jinja3kit.egg-info/SOURCES.txt +4 -1
- jinja3kit-1.2.2/src/jinja3kit/template/db/seed.sql +0 -155
- jinja3kit-1.2.2/src/jinja3kit/template/db/shpory_flask_pg_pillow.docx +0 -0
- jinja3kit-1.2.2/src/jinja3kit/template/db.py +0 -72
- jinja3kit-1.2.2/src/jinja3kit/template/static/img/1.jpg +0 -0
- jinja3kit-1.2.2/src/jinja3kit/template/static/img/10.jpg +0 -0
- jinja3kit-1.2.2/src/jinja3kit/template/static/img/2.jpg +0 -0
- jinja3kit-1.2.2/src/jinja3kit/template/static/img/3.jpg +0 -0
- jinja3kit-1.2.2/src/jinja3kit/template/static/img/4.jpg +0 -0
- jinja3kit-1.2.2/src/jinja3kit/template/static/img/5.jpg +0 -0
- jinja3kit-1.2.2/src/jinja3kit/template/static/img/6.jpg +0 -0
- jinja3kit-1.2.2/src/jinja3kit/template/static/img/7.jpg +0 -0
- jinja3kit-1.2.2/src/jinja3kit/template/static/img/8.jpg +0 -0
- jinja3kit-1.2.2/src/jinja3kit/template/static/img/9.jpg +0 -0
- jinja3kit-1.2.2/src/jinja3kit/template/static/img/Icon.JPG +0 -0
- jinja3kit-1.2.2/src/jinja3kit/template/static/img/Icon.ico +0 -0
- jinja3kit-1.2.2/src/jinja3kit/template/static/img/Icon.png +0 -0
- {jinja3kit-1.2.2 → jinja3kit-1.2.4}/README.md +0 -0
- {jinja3kit-1.2.2 → jinja3kit-1.2.4}/setup.cfg +0 -0
- {jinja3kit-1.2.2 → jinja3kit-1.2.4}/src/jinja3kit/template/static/img/picture.png +0 -0
- {jinja3kit-1.2.2 → jinja3kit-1.2.4}/src/jinja3kit.egg-info/dependency_links.txt +0 -0
- {jinja3kit-1.2.2 → jinja3kit-1.2.4}/src/jinja3kit.egg-info/entry_points.txt +0 -0
- {jinja3kit-1.2.2 → jinja3kit-1.2.4}/src/jinja3kit.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "jinja3kit"
|
|
7
|
-
version = "1.2.
|
|
7
|
+
version = "1.2.4"
|
|
8
8
|
description = "A simple Python toolkit for generating Flask-based web projects with PostgreSQL support."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
# cli.py нужен, если проект используется как шаблон-пакет.
|
|
4
|
+
# Команда jinja3-create копирует готовую папку template в новый проект.
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import shutil
|
|
8
|
+
from importlib.resources import files
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def create_project(target: str, force: bool = False) -> Path:
|
|
13
|
+
"""Создаёт новый проект из папки template."""
|
|
14
|
+
target_path = Path(target).resolve()
|
|
15
|
+
template_path = files("jinja3kit").joinpath("template")
|
|
16
|
+
|
|
17
|
+
# Если папка уже существует, без --force проект не перезаписывается.
|
|
18
|
+
if target_path.exists():
|
|
19
|
+
if not force:
|
|
20
|
+
raise SystemExit(
|
|
21
|
+
f"Папка уже существует: {target_path}\n"
|
|
22
|
+
f"Используй --force, если хочешь перезаписать её."
|
|
23
|
+
)
|
|
24
|
+
shutil.rmtree(target_path)
|
|
25
|
+
|
|
26
|
+
# Копируем весь шаблон: app.py, db.py, templates, static, sql-файлы.
|
|
27
|
+
shutil.copytree(template_path, target_path)
|
|
28
|
+
return target_path
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def main() -> None:
|
|
32
|
+
"""Точка входа для консольной команды jinja3-create."""
|
|
33
|
+
parser = argparse.ArgumentParser(
|
|
34
|
+
prog="jinja3-create",
|
|
35
|
+
description="Create a neutral Flask + PostgreSQL starter project."
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument("name", help="Название папки нового проекта")
|
|
38
|
+
parser.add_argument("--force", action="store_true", help="Перезаписать папку, если она уже есть")
|
|
39
|
+
args = parser.parse_args()
|
|
40
|
+
|
|
41
|
+
project_path = create_project(args.name, args.force)
|
|
42
|
+
|
|
43
|
+
print(f"Проект создан: {project_path}")
|
|
44
|
+
print("")
|
|
45
|
+
print("Дальше:")
|
|
46
|
+
print(f" cd {project_path.name}")
|
|
47
|
+
print(" copy .env.sample .env")
|
|
48
|
+
print(" pip install -r requirements.txt")
|
|
49
|
+
print(" python app.py")
|
jinja3kit-1.2.4/src/jinja3kit/template/README_/320/227/320/220/320/237/320/243/320/241/320/232.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# ООО «МирИгрушек» — Flask + PostgreSQL + Pillow
|
|
2
|
+
|
|
3
|
+
Готовый вариант по примеру: магазин игрушек, PostgreSQL/pgAdmin, Flask, загрузка и обработка изображений через Pillow.
|
|
4
|
+
|
|
5
|
+
## Что реализовано
|
|
6
|
+
|
|
7
|
+
- вход по логину и паролю из базы данных;
|
|
8
|
+
- режим гостя;
|
|
9
|
+
- роли: гость, авторизированный клиент, менеджер, администратор;
|
|
10
|
+
- просмотр каталога игрушек;
|
|
11
|
+
- для менеджера и администратора: поиск, фильтрация, сортировка;
|
|
12
|
+
- для администратора: добавление, редактирование, удаление игрушек;
|
|
13
|
+
- просмотр заказов;
|
|
14
|
+
- для администратора: добавление, редактирование, удаление заказов;
|
|
15
|
+
- загрузка фото с обработкой Pillow до 300×200;
|
|
16
|
+
- стиль по заданию: Arial, #FFFFFF, #F5DEB3, #DEB887, скидка больше 17% — #FFDEAD.
|
|
17
|
+
|
|
18
|
+
## Создание базы
|
|
19
|
+
|
|
20
|
+
В pgAdmin создай базу:
|
|
21
|
+
|
|
22
|
+
```sql
|
|
23
|
+
CREATE DATABASE toy_store;
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Потом открой Query Tool в базе `toy_store` и выполни файл:
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
db/full_init.sql
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Настройка подключения
|
|
33
|
+
|
|
34
|
+
В файле `db.py` поменяй пароль PostgreSQL:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
"password": "123456789"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Запуск
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
python -m venv .venv
|
|
44
|
+
.venv\Scripts\activate
|
|
45
|
+
pip install -r requirements.txt
|
|
46
|
+
python app.py
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Сайт: http://127.0.0.1:5000
|
|
50
|
+
|
|
51
|
+
## Тестовые логины
|
|
52
|
+
|
|
53
|
+
Администратор: 94d5ous@gmail.com / uzWC67
|
|
54
|
+
Менеджер: 1diph5e@tutanota.com / 8ntwUp
|
|
55
|
+
Клиент: 5d4zbu@tutanota.com / rwVDh9
|
|
56
|
+
|
|
57
|
+
Remove-Item (Get-PSReadLineOption).HistorySavePath
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
# Основной файл Flask-приложения.
|
|
2
|
-
# Здесь находятся настройки сайта, авторизация, маршруты, работа с товарами, заказами и изображениями.
|
|
3
|
-
|
|
4
1
|
import os, uuid
|
|
5
2
|
from functools import wraps
|
|
6
3
|
from pathlib import Path
|
|
@@ -13,39 +10,21 @@ from werkzeug.utils import secure_filename
|
|
|
13
10
|
|
|
14
11
|
from db import execute, fetch_all, fetch_one, get_conn
|
|
15
12
|
|
|
16
|
-
# BASE — путь к папке проекта. Нужен, чтобы правильно сохранять и удалять файлы.
|
|
17
13
|
BASE = Path(__file__).resolve().parent
|
|
18
|
-
# UPLOADS — папка для фотографий товаров, которые загружает администратор.
|
|
19
14
|
UPLOADS = BASE / "static" / "uploads"
|
|
20
|
-
# Названия ролей. По ним проверяются права доступа к разделам сайта.
|
|
21
15
|
ADMIN, MANAGER = "Администратор", "Менеджер"
|
|
22
|
-
# Разрешённые расширения изображений для загрузки через Pillow.
|
|
23
16
|
IMG_EXT = {"jpg", "jpeg", "png", "webp"}
|
|
24
|
-
# Поля таблицы products, которые используются при добавлении и редактировании товара.
|
|
25
|
-
# Если в новой БД изменятся поля товара, в первую очередь меняется этот список.
|
|
26
17
|
PRODUCT_FIELDS = "article_number product_name unit price stock_quantity discount_percent description photo_url id_category id_supplier id_manufacturer".split()
|
|
27
|
-
# Поля таблицы orders, которые используются при добавлении и редактировании заказа.
|
|
28
18
|
ORDER_FIELDS = "order_number order_date delivery_date pickup_code id_user id_pickup_point id_status".split()
|
|
29
19
|
|
|
30
|
-
# Создаём объект Flask-приложения.
|
|
31
20
|
app = Flask(__name__)
|
|
32
|
-
|
|
33
|
-
# MAX_CONTENT_LENGTH ограничивает размер загружаемого файла до 8 МБ.
|
|
34
|
-
app.config.update(SECRET_KEY=os.getenv("SECRET_KEY", "secret-key"), MAX_CONTENT_LENGTH=8 * 1024 * 1024)
|
|
35
|
-
|
|
21
|
+
app.config.update(SECRET_KEY=os.getenv("SECRET_KEY", "demo-exam-secret-key"), MAX_CONTENT_LENGTH=8 * 1024 * 1024)
|
|
36
22
|
|
|
37
23
|
|
|
38
|
-
# Возвращает текущего пользователя из session. Если никто не вошёл — вернёт None.
|
|
39
24
|
def me(): return session.get("user")
|
|
40
|
-
# Возвращает роль текущего пользователя: Гость, Клиент, Менеджер или Администратор.
|
|
41
25
|
def role(): return (me() or {}).get("role_name")
|
|
42
|
-
# Проверяет, входит ли текущая роль в список разрешённых ролей.
|
|
43
26
|
def has(*roles): return role() in roles
|
|
44
27
|
|
|
45
|
-
|
|
46
|
-
# Декоратор защиты страниц.
|
|
47
|
-
# Если пользователь не вошёл — отправляет на страницу входа.
|
|
48
|
-
# Если роль не подходит — запрещает действие и возвращает к товарам.
|
|
49
28
|
def need(*roles):
|
|
50
29
|
def deco(f):
|
|
51
30
|
@wraps(f)
|
|
@@ -61,9 +40,6 @@ def need(*roles):
|
|
|
61
40
|
return deco
|
|
62
41
|
|
|
63
42
|
|
|
64
|
-
|
|
65
|
-
# context_processor добавляет переменные и функции сразу во все HTML-шаблоны.
|
|
66
|
-
# Благодаря этому в templates можно использовать current_user, is_admin, product_image_url и т.д.
|
|
67
43
|
@app.context_processor
|
|
68
44
|
def ctx():
|
|
69
45
|
def img(p):
|
|
@@ -72,15 +48,10 @@ def ctx():
|
|
|
72
48
|
can_search_products=has(ADMIN, MANAGER), can_view_orders=has(ADMIN, MANAGER), product_image_url=img)
|
|
73
49
|
|
|
74
50
|
|
|
75
|
-
|
|
76
|
-
# Берёт значение из формы или GET-параметров и убирает лишние пробелы.
|
|
77
51
|
def val(name, default=""):
|
|
78
52
|
return request.values.get(name, default).strip()
|
|
79
53
|
|
|
80
54
|
|
|
81
|
-
|
|
82
|
-
# Проверяет числовые поля формы: цена, скидка, количество, id справочников.
|
|
83
|
-
# Если введено не число или число выходит за границы — выбрасывает понятную ошибку.
|
|
84
55
|
def num(name, title, kind=float, pos=False, maxv=None):
|
|
85
56
|
try:
|
|
86
57
|
x = kind(str(request.form.get(name, "0")).replace(",", ".")) if kind is float else kind(request.form.get(name, "0"))
|
|
@@ -93,10 +64,6 @@ def num(name, title, kind=float, pos=False, maxv=None):
|
|
|
93
64
|
return x
|
|
94
65
|
|
|
95
66
|
|
|
96
|
-
|
|
97
|
-
# Загружает справочники для форм.
|
|
98
|
-
# Для товара нужны категории, поставщики, производители.
|
|
99
|
-
# Для заказа нужны пользователи, пункты выдачи, статусы и товары.
|
|
100
67
|
def refs(kind):
|
|
101
68
|
if kind == "product":
|
|
102
69
|
return dict(categories=fetch_all("SELECT * FROM categories ORDER BY category_name"),
|
|
@@ -108,10 +75,6 @@ def refs(kind):
|
|
|
108
75
|
products=fetch_all("SELECT id_product, article_number, product_name, price FROM products ORDER BY article_number"))
|
|
109
76
|
|
|
110
77
|
|
|
111
|
-
|
|
112
|
-
# Сохраняет изображение товара через Pillow.
|
|
113
|
-
# Проверяет формат, уменьшает фото до 300x200 и сохраняет в static/uploads.
|
|
114
|
-
# При редактировании товара старое загруженное фото удаляется.
|
|
115
78
|
def save_img(file, old=None):
|
|
116
79
|
if not file or not file.filename:
|
|
117
80
|
return old
|
|
@@ -130,12 +93,9 @@ def save_img(file, old=None):
|
|
|
130
93
|
return f"uploads/{name}"
|
|
131
94
|
|
|
132
95
|
|
|
133
|
-
|
|
134
|
-
# Собирает данные товара из HTML-формы.
|
|
135
|
-
# Здесь проверяются обязательные поля, цена, скидка, количество и справочники.
|
|
136
96
|
def product_from_form(old_photo=None):
|
|
137
97
|
if not val("article_number") or not val("product_name"):
|
|
138
|
-
raise ValueError("Артикул и наименование
|
|
98
|
+
raise ValueError("Артикул и наименование игрушки обязательны")
|
|
139
99
|
return dict(article_number=val("article_number"), product_name=val("product_name"), unit=val("unit", "шт.") or "шт.",
|
|
140
100
|
price=num("price", "Цена"), stock_quantity=num("stock_quantity", "Кол-во на складе", int),
|
|
141
101
|
discount_percent=num("discount_percent", "Скидка", maxv=100), description=val("description"),
|
|
@@ -143,9 +103,6 @@ def product_from_form(old_photo=None):
|
|
|
143
103
|
id_supplier=num("id_supplier", "Поставщик", int, True), id_manufacturer=num("id_manufacturer", "Производитель", int, True))
|
|
144
104
|
|
|
145
105
|
|
|
146
|
-
|
|
147
|
-
# Универсальная функция для товара.
|
|
148
|
-
# Если pid есть — обновляет товар, если pid нет — добавляет новый товар.
|
|
149
106
|
def upsert_product(data, pid=None):
|
|
150
107
|
cols = ", ".join(PRODUCT_FIELDS)
|
|
151
108
|
if pid:
|
|
@@ -155,10 +112,6 @@ def upsert_product(data, pid=None):
|
|
|
155
112
|
execute(f"INSERT INTO products ({cols}) VALUES ({', '.join(f'%({c})s' for c in PRODUCT_FIELDS)})", data)
|
|
156
113
|
|
|
157
114
|
|
|
158
|
-
|
|
159
|
-
# Разбирает состав заказа из текстового поля.
|
|
160
|
-
# Формат: артикул, количество. Например: 12345, 2
|
|
161
|
-
# По артикулу ищет товар в БД и берёт его цену.
|
|
162
115
|
def parse_items(raw):
|
|
163
116
|
parts = [x.strip() for x in raw.replace(";", ",").replace("\n", ",").split(",") if x.strip()]
|
|
164
117
|
if not parts or len(parts) % 2:
|
|
@@ -167,7 +120,7 @@ def parse_items(raw):
|
|
|
167
120
|
for art, qty in zip(parts[::2], parts[1::2]):
|
|
168
121
|
p = fetch_one("SELECT id_product, price FROM products WHERE article_number=%s", (art,))
|
|
169
122
|
if not p:
|
|
170
|
-
raise ValueError(f"
|
|
123
|
+
raise ValueError(f"Игрушка с артикулом {art} не найден")
|
|
171
124
|
q = int(qty)
|
|
172
125
|
if q <= 0:
|
|
173
126
|
raise ValueError(f"Количество для {art} должно быть больше нуля")
|
|
@@ -175,8 +128,6 @@ def parse_items(raw):
|
|
|
175
128
|
return items
|
|
176
129
|
|
|
177
130
|
|
|
178
|
-
|
|
179
|
-
# Собирает данные заказа из HTML-формы и проверяет обязательные поля.
|
|
180
131
|
def order_from_form():
|
|
181
132
|
if not val("pickup_code") or not val("order_date"):
|
|
182
133
|
raise ValueError("Номер заказа, дата заказа и код получения обязательны")
|
|
@@ -187,9 +138,6 @@ def order_from_form():
|
|
|
187
138
|
items=parse_items(raw), items_text=raw)
|
|
188
139
|
|
|
189
140
|
|
|
190
|
-
|
|
191
|
-
# Сохраняет заказ и его состав.
|
|
192
|
-
# При редактировании старые позиции заказа удаляются и записываются заново.
|
|
193
141
|
def save_order(data, oid=None):
|
|
194
142
|
with get_conn() as conn, conn.cursor(cursor_factory=RealDictCursor) as cur:
|
|
195
143
|
if oid:
|
|
@@ -203,15 +151,10 @@ def save_order(data, oid=None):
|
|
|
203
151
|
[(oid, pid, qty, price) for pid, qty, price in data["items"]])
|
|
204
152
|
|
|
205
153
|
|
|
206
|
-
|
|
207
|
-
# Главная страница: если пользователь вошёл — открывает товары, иначе страницу входа.
|
|
208
154
|
@app.route("/")
|
|
209
155
|
def index(): return redirect(url_for("products" if me() else "login"))
|
|
210
156
|
|
|
211
157
|
|
|
212
|
-
|
|
213
|
-
# Страница входа.
|
|
214
|
-
# GET показывает форму, POST проверяет логин и пароль в таблице users.
|
|
215
158
|
@app.route("/login", methods=["GET", "POST"])
|
|
216
159
|
def login():
|
|
217
160
|
if request.method == "POST":
|
|
@@ -225,18 +168,13 @@ def login():
|
|
|
225
168
|
return render_template("login.html")
|
|
226
169
|
|
|
227
170
|
|
|
228
|
-
|
|
229
|
-
# Вход без логина и пароля.
|
|
230
|
-
# Гость может только просматривать товары, без поиска, заказов и админских действий.
|
|
231
171
|
@app.route("/guest")
|
|
232
172
|
def guest_login():
|
|
233
173
|
session["user"] = dict(id_user=0, last_name="", first_name="Гость", middle_name="", login="guest", role_name="Гость")
|
|
234
|
-
flash("Вы вошли в режиме гостя. Доступен только просмотр
|
|
174
|
+
flash("Вы вошли в режиме гостя. Доступен только просмотр игрушек", "info")
|
|
235
175
|
return redirect(url_for("products"))
|
|
236
176
|
|
|
237
177
|
|
|
238
|
-
|
|
239
|
-
# Выход из аккаунта: очищает session и возвращает на страницу входа.
|
|
240
178
|
@app.route("/logout")
|
|
241
179
|
def logout():
|
|
242
180
|
session.clear()
|
|
@@ -244,10 +182,6 @@ def logout():
|
|
|
244
182
|
return redirect(url_for("login"))
|
|
245
183
|
|
|
246
184
|
|
|
247
|
-
|
|
248
|
-
# Каталог товаров.
|
|
249
|
-
# Администратор и менеджер могут искать, фильтровать и сортировать товары.
|
|
250
|
-
# Гость и клиент видят только список товаров.
|
|
251
185
|
@app.route("/products")
|
|
252
186
|
@need()
|
|
253
187
|
def products():
|
|
@@ -264,48 +198,39 @@ def products():
|
|
|
264
198
|
suppliers=fetch_all("SELECT * FROM suppliers ORDER BY supplier_name"), search=search, selected_supplier=supplier, sort=sort)
|
|
265
199
|
|
|
266
200
|
|
|
267
|
-
|
|
268
|
-
# Одна функция сразу для добавления и редактирования товара.
|
|
269
|
-
# /products/add — добавление, /products/<id>/edit — редактирование.
|
|
270
201
|
@app.route("/products/add", methods=["GET", "POST"], defaults={"product_id": None}, endpoint="product_add")
|
|
271
202
|
@app.route("/products/<int:product_id>/edit", methods=["GET", "POST"], endpoint="product_edit")
|
|
272
203
|
@need(ADMIN)
|
|
273
204
|
def product_form(product_id):
|
|
274
205
|
product = fetch_one("SELECT * FROM products WHERE id_product=%s", (product_id,)) if product_id else None
|
|
275
206
|
if product_id and not product:
|
|
276
|
-
flash("
|
|
207
|
+
flash("Игрушка не найдена", "danger"); return redirect(url_for("products"))
|
|
277
208
|
if request.method == "POST":
|
|
278
209
|
try:
|
|
279
210
|
upsert_product(product_from_form(product["photo_url"] if product else None), product_id)
|
|
280
|
-
flash("
|
|
211
|
+
flash("Игрушка обновлена" if product_id else "Игрушка добавлена", "success")
|
|
281
212
|
return redirect(url_for("products"))
|
|
282
213
|
except errors.UniqueViolation:
|
|
283
|
-
flash("
|
|
214
|
+
flash("Игрушка с таким артикулом уже существует", "danger")
|
|
284
215
|
except Exception as e:
|
|
285
216
|
flash(str(e), "danger")
|
|
286
|
-
return render_template("product_form.html", product=product, refs=refs("product"), title="Редактирование
|
|
287
|
-
|
|
217
|
+
return render_template("product_form.html", product=product, refs=refs("product"), title="Редактирование игрушки" if product_id else "Добавление игрушки")
|
|
288
218
|
|
|
289
219
|
|
|
290
|
-
# Удаление товара.
|
|
291
|
-
# Если товар уже есть в заказах, удалить его нельзя, чтобы не сломать историю заказов.
|
|
292
220
|
@app.post("/products/<int:product_id>/delete")
|
|
293
221
|
@need(ADMIN)
|
|
294
222
|
def product_delete(product_id):
|
|
295
223
|
p = fetch_one("SELECT photo_url FROM products WHERE id_product=%s", (product_id,))
|
|
296
224
|
used = fetch_one("SELECT COUNT(*) cnt FROM order_items WHERE id_product=%s", (product_id,))
|
|
297
|
-
if not p: flash("
|
|
298
|
-
elif used and used["cnt"]: flash("Нельзя удалить
|
|
225
|
+
if not p: flash("Игрушка не найдена", "danger")
|
|
226
|
+
elif used and used["cnt"]: flash("Нельзя удалить игрушку: она уже есть в заказах", "danger")
|
|
299
227
|
else:
|
|
300
228
|
execute("DELETE FROM products WHERE id_product=%s", (product_id,))
|
|
301
229
|
if p["photo_url"] and p["photo_url"].startswith("uploads/"): (BASE / "static" / p["photo_url"]).unlink(missing_ok=True)
|
|
302
|
-
flash("
|
|
230
|
+
flash("Игрушка удалена", "success")
|
|
303
231
|
return redirect(url_for("products"))
|
|
304
232
|
|
|
305
233
|
|
|
306
|
-
|
|
307
|
-
# Список заказов.
|
|
308
|
-
# Доступен только менеджеру и администратору.
|
|
309
234
|
@app.route("/orders")
|
|
310
235
|
@need(ADMIN, MANAGER)
|
|
311
236
|
def orders():
|
|
@@ -316,9 +241,6 @@ def orders():
|
|
|
316
241
|
return render_template("orders.html", orders=fetch_all(f"SELECT * FROM v_orders_details {where} ORDER BY order_date DESC, order_number DESC", params), search=search)
|
|
317
242
|
|
|
318
243
|
|
|
319
|
-
|
|
320
|
-
# Одна функция сразу для добавления и редактирования заказа.
|
|
321
|
-
# /orders/add — добавление, /orders/<id>/edit — редактирование.
|
|
322
244
|
@app.route("/orders/add", methods=["GET", "POST"], defaults={"order_id": None}, endpoint="order_add")
|
|
323
245
|
@app.route("/orders/<int:order_id>/edit", methods=["GET", "POST"], endpoint="order_edit")
|
|
324
246
|
@need(ADMIN)
|
|
@@ -339,8 +261,6 @@ def order_form(order_id):
|
|
|
339
261
|
return render_template("order_form.html", order=order, refs=refs("order"), title="Редактирование заказа" if order_id else "Добавление заказа", items_text="\n".join(f"{r['article_number']}, {r['quantity']}" for r in rows))
|
|
340
262
|
|
|
341
263
|
|
|
342
|
-
|
|
343
|
-
# Удаление заказа. Позиции заказа удаляются автоматически по связи в БД или вручную через каскад.
|
|
344
264
|
@app.post("/orders/<int:order_id>/delete")
|
|
345
265
|
@need(ADMIN)
|
|
346
266
|
def order_delete(order_id):
|
|
@@ -349,20 +269,13 @@ def order_delete(order_id):
|
|
|
349
269
|
return redirect(url_for("orders"))
|
|
350
270
|
|
|
351
271
|
|
|
352
|
-
|
|
353
|
-
# Обработчик ошибки 404 — страница не найдена.
|
|
354
272
|
@app.errorhandler(404)
|
|
355
273
|
def not_found(_): return render_template("error.html", title="404", message="Страница не найдена"), 404
|
|
356
274
|
|
|
357
275
|
|
|
358
|
-
|
|
359
|
-
# Обработчик ошибки 500 — внутренняя ошибка сервера.
|
|
360
276
|
@app.errorhandler(500)
|
|
361
277
|
def server_error(e): return render_template("error.html", title="500", message=f"Ошибка сервера: {e}"), 500
|
|
362
278
|
|
|
363
279
|
|
|
364
|
-
|
|
365
|
-
# Запуск приложения командой: python app.py
|
|
366
|
-
# debug=True показывает ошибки в браузере, что удобно при разработке и на экзамене.
|
|
367
280
|
if __name__ == "__main__":
|
|
368
281
|
app.run(debug=True)
|