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.
Files changed (55) hide show
  1. {jinja3kit-1.2.2 → jinja3kit-1.2.4}/PKG-INFO +1 -1
  2. {jinja3kit-1.2.2 → jinja3kit-1.2.4}/pyproject.toml +1 -1
  3. jinja3kit-1.2.4/src/jinja3kit/__init__.py +1 -0
  4. jinja3kit-1.2.4/src/jinja3kit/cli.py +49 -0
  5. jinja3kit-1.2.4/src/jinja3kit/template/README_/320/227/320/220/320/237/320/243/320/241/320/232.md +57 -0
  6. {jinja3kit-1.2.2 → jinja3kit-1.2.4}/src/jinja3kit/template/app.py +11 -98
  7. jinja3kit-1.2.4/src/jinja3kit/template/db/full_init.sql +284 -0
  8. {jinja3kit-1.2.2 → jinja3kit-1.2.4}/src/jinja3kit/template/db/schema.sql +9 -27
  9. jinja3kit-1.2.4/src/jinja3kit/template/db/seed.sql +137 -0
  10. jinja3kit-1.2.4/src/jinja3kit/template/db.py +49 -0
  11. {jinja3kit-1.2.2 → jinja3kit-1.2.4}/src/jinja3kit/template/static/css/style.css +37 -21
  12. jinja3kit-1.2.4/src/jinja3kit/template/static/img/1.jpg +0 -0
  13. jinja3kit-1.2.4/src/jinja3kit/template/static/img/10.jpg +0 -0
  14. jinja3kit-1.2.4/src/jinja3kit/template/static/img/2.jpg +0 -0
  15. jinja3kit-1.2.4/src/jinja3kit/template/static/img/3.jpg +0 -0
  16. jinja3kit-1.2.4/src/jinja3kit/template/static/img/4.jpg +0 -0
  17. jinja3kit-1.2.4/src/jinja3kit/template/static/img/5.jpg +0 -0
  18. jinja3kit-1.2.4/src/jinja3kit/template/static/img/6.jpg +0 -0
  19. jinja3kit-1.2.4/src/jinja3kit/template/static/img/7.jpg +0 -0
  20. jinja3kit-1.2.4/src/jinja3kit/template/static/img/8.jpg +0 -0
  21. jinja3kit-1.2.4/src/jinja3kit/template/static/img/9.jpg +0 -0
  22. jinja3kit-1.2.4/src/jinja3kit/template/static/img/Icon.JPG +0 -0
  23. jinja3kit-1.2.4/src/jinja3kit/template/static/img/Icon.ico +0 -0
  24. jinja3kit-1.2.4/src/jinja3kit/template/static/img/Icon.png +0 -0
  25. {jinja3kit-1.2.2 → jinja3kit-1.2.4}/src/jinja3kit/template/templates/base.html +4 -5
  26. {jinja3kit-1.2.2 → jinja3kit-1.2.4}/src/jinja3kit/template/templates/error.html +0 -1
  27. {jinja3kit-1.2.2 → jinja3kit-1.2.4}/src/jinja3kit/template/templates/login.html +0 -1
  28. {jinja3kit-1.2.2 → jinja3kit-1.2.4}/src/jinja3kit/template/templates/order_form.html +1 -2
  29. {jinja3kit-1.2.2 → jinja3kit-1.2.4}/src/jinja3kit/template/templates/orders.html +1 -2
  30. {jinja3kit-1.2.2 → jinja3kit-1.2.4}/src/jinja3kit/template/templates/product_form.html +2 -3
  31. {jinja3kit-1.2.2 → jinja3kit-1.2.4}/src/jinja3kit/template/templates/products.html +32 -12
  32. {jinja3kit-1.2.2 → jinja3kit-1.2.4}/src/jinja3kit.egg-info/PKG-INFO +1 -1
  33. {jinja3kit-1.2.2 → jinja3kit-1.2.4}/src/jinja3kit.egg-info/SOURCES.txt +4 -1
  34. jinja3kit-1.2.2/src/jinja3kit/template/db/seed.sql +0 -155
  35. jinja3kit-1.2.2/src/jinja3kit/template/db/shpory_flask_pg_pillow.docx +0 -0
  36. jinja3kit-1.2.2/src/jinja3kit/template/db.py +0 -72
  37. jinja3kit-1.2.2/src/jinja3kit/template/static/img/1.jpg +0 -0
  38. jinja3kit-1.2.2/src/jinja3kit/template/static/img/10.jpg +0 -0
  39. jinja3kit-1.2.2/src/jinja3kit/template/static/img/2.jpg +0 -0
  40. jinja3kit-1.2.2/src/jinja3kit/template/static/img/3.jpg +0 -0
  41. jinja3kit-1.2.2/src/jinja3kit/template/static/img/4.jpg +0 -0
  42. jinja3kit-1.2.2/src/jinja3kit/template/static/img/5.jpg +0 -0
  43. jinja3kit-1.2.2/src/jinja3kit/template/static/img/6.jpg +0 -0
  44. jinja3kit-1.2.2/src/jinja3kit/template/static/img/7.jpg +0 -0
  45. jinja3kit-1.2.2/src/jinja3kit/template/static/img/8.jpg +0 -0
  46. jinja3kit-1.2.2/src/jinja3kit/template/static/img/9.jpg +0 -0
  47. jinja3kit-1.2.2/src/jinja3kit/template/static/img/Icon.JPG +0 -0
  48. jinja3kit-1.2.2/src/jinja3kit/template/static/img/Icon.ico +0 -0
  49. jinja3kit-1.2.2/src/jinja3kit/template/static/img/Icon.png +0 -0
  50. {jinja3kit-1.2.2 → jinja3kit-1.2.4}/README.md +0 -0
  51. {jinja3kit-1.2.2 → jinja3kit-1.2.4}/setup.cfg +0 -0
  52. {jinja3kit-1.2.2 → jinja3kit-1.2.4}/src/jinja3kit/template/static/img/picture.png +0 -0
  53. {jinja3kit-1.2.2 → jinja3kit-1.2.4}/src/jinja3kit.egg-info/dependency_links.txt +0 -0
  54. {jinja3kit-1.2.2 → jinja3kit-1.2.4}/src/jinja3kit.egg-info/entry_points.txt +0 -0
  55. {jinja3kit-1.2.2 → jinja3kit-1.2.4}/src/jinja3kit.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jinja3kit
3
- Version: 1.2.2
3
+ Version: 1.2.4
4
4
  Summary: A simple Python toolkit for generating Flask-based web projects with PostgreSQL support.
5
5
  Author: JINJA
6
6
  Requires-Python: >=3.10
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "jinja3kit"
7
- version = "1.2.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")
@@ -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
- # SECRET_KEY нужен для сессий и flash-сообщений.
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"Товар с артикулом {art} не найден")
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("Вы вошли в режиме гостя. Доступен только просмотр товаров", "info")
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("Товар не найден", "danger"); return redirect(url_for("products"))
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("Товар обновлен" if product_id else "Товар добавлен", "success")
211
+ flash("Игрушка обновлена" if product_id else "Игрушка добавлена", "success")
281
212
  return redirect(url_for("products"))
282
213
  except errors.UniqueViolation:
283
- flash("Товар с таким артикулом уже существует", "danger")
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="Редактирование товара" if product_id else "Добавление товара")
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("Товар не найден", "danger")
298
- elif used and used["cnt"]: flash("Нельзя удалить товар: он уже есть в заказах", "danger")
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("Товар удален", "success")
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)