jinja3kit 0.1.0__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-0.1.0/PKG-INFO +7 -0
- jinja3kit-0.1.0/pyproject.toml +23 -0
- jinja3kit-0.1.0/setup.cfg +4 -0
- jinja3kit-0.1.0/src/jinja3kit/__init__.py +1 -0
- jinja3kit-0.1.0/src/jinja3kit/cli.py +42 -0
- jinja3kit-0.1.0/src/jinja3kit/template/app.py +217 -0
- jinja3kit-0.1.0/src/jinja3kit/template/database/init.sql +45 -0
- jinja3kit-0.1.0/src/jinja3kit/template/db.py +38 -0
- jinja3kit-0.1.0/src/jinja3kit/template/requirements.txt +4 -0
- jinja3kit-0.1.0/src/jinja3kit/template/static/images/placeholder.png +0 -0
- jinja3kit-0.1.0/src/jinja3kit/template/static/style.css +169 -0
- jinja3kit-0.1.0/src/jinja3kit/template/templates/base.html +40 -0
- jinja3kit-0.1.0/src/jinja3kit/template/templates/login.html +18 -0
- jinja3kit-0.1.0/src/jinja3kit/template/templates/order_form.html +31 -0
- jinja3kit-0.1.0/src/jinja3kit/template/templates/orders.html +31 -0
- jinja3kit-0.1.0/src/jinja3kit/template/templates/product_form.html +29 -0
- jinja3kit-0.1.0/src/jinja3kit/template/templates/products.html +43 -0
- jinja3kit-0.1.0/src/jinja3kit.egg-info/PKG-INFO +7 -0
- jinja3kit-0.1.0/src/jinja3kit.egg-info/SOURCES.txt +20 -0
- jinja3kit-0.1.0/src/jinja3kit.egg-info/dependency_links.txt +1 -0
- jinja3kit-0.1.0/src/jinja3kit.egg-info/entry_points.txt +2 -0
- jinja3kit-0.1.0/src/jinja3kit.egg-info/top_level.txt +1 -0
jinja3kit-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "jinja3kit"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A small Flask and PostgreSQL starter generator"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Student" }
|
|
13
|
+
]
|
|
14
|
+
dependencies = []
|
|
15
|
+
|
|
16
|
+
[project.scripts]
|
|
17
|
+
jinja3-create = "jinja3kit.cli:main"
|
|
18
|
+
|
|
19
|
+
[tool.setuptools.packages.find]
|
|
20
|
+
where = ["src"]
|
|
21
|
+
|
|
22
|
+
[tool.setuptools.package-data]
|
|
23
|
+
jinja3kit = ["template/**/*"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import shutil
|
|
5
|
+
from importlib.resources import files
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def create_project(target: str, force: bool = False) -> Path:
|
|
10
|
+
target_path = Path(target).resolve()
|
|
11
|
+
template_path = files("jinja3kit").joinpath("template")
|
|
12
|
+
|
|
13
|
+
if target_path.exists():
|
|
14
|
+
if not force:
|
|
15
|
+
raise SystemExit(
|
|
16
|
+
f"Папка уже существует: {target_path}\n"
|
|
17
|
+
f"Используй --force, если хочешь перезаписать её."
|
|
18
|
+
)
|
|
19
|
+
shutil.rmtree(target_path)
|
|
20
|
+
|
|
21
|
+
shutil.copytree(template_path, target_path)
|
|
22
|
+
return target_path
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def main() -> None:
|
|
26
|
+
parser = argparse.ArgumentParser(
|
|
27
|
+
prog="jinja3-create",
|
|
28
|
+
description="Create a neutral Flask + PostgreSQL starter project."
|
|
29
|
+
)
|
|
30
|
+
parser.add_argument("name", help="Название папки нового проекта")
|
|
31
|
+
parser.add_argument("--force", action="store_true", help="Перезаписать папку, если она уже есть")
|
|
32
|
+
args = parser.parse_args()
|
|
33
|
+
|
|
34
|
+
project_path = create_project(args.name, args.force)
|
|
35
|
+
|
|
36
|
+
print(f"Проект создан: {project_path}")
|
|
37
|
+
print("")
|
|
38
|
+
print("Дальше:")
|
|
39
|
+
print(f" cd {project_path.name}")
|
|
40
|
+
print(" copy .env.sample .env")
|
|
41
|
+
print(" pip install -r requirements.txt")
|
|
42
|
+
print(" python app.py")
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from flask import Flask, render_template, request, redirect, url_for, session, flash
|
|
3
|
+
from werkzeug.security import check_password_hash
|
|
4
|
+
from dotenv import load_dotenv
|
|
5
|
+
|
|
6
|
+
from db import fetch_all, fetch_one, execute
|
|
7
|
+
|
|
8
|
+
load_dotenv()
|
|
9
|
+
|
|
10
|
+
app = Flask(__name__)
|
|
11
|
+
app.secret_key = os.getenv("SECRET_KEY", "dev-key")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def current_user():
|
|
15
|
+
if "user_id" not in session:
|
|
16
|
+
return None
|
|
17
|
+
return {
|
|
18
|
+
"id": session.get("user_id"),
|
|
19
|
+
"name": session.get("user_name"),
|
|
20
|
+
"role": session.get("role"),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@app.context_processor
|
|
25
|
+
def inject_user():
|
|
26
|
+
return {"current_user": current_user()}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def role_required(*roles):
|
|
30
|
+
def decorator(func):
|
|
31
|
+
def wrapper(*args, **kwargs):
|
|
32
|
+
user = current_user()
|
|
33
|
+
if not user or user["role"] not in roles:
|
|
34
|
+
flash("Недостаточно прав")
|
|
35
|
+
return redirect(url_for("index"))
|
|
36
|
+
return func(*args, **kwargs)
|
|
37
|
+
wrapper.__name__ = func.__name__
|
|
38
|
+
return wrapper
|
|
39
|
+
return decorator
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@app.route("/")
|
|
43
|
+
def index():
|
|
44
|
+
search = request.args.get("q", "").strip()
|
|
45
|
+
sort = request.args.get("sort", "name")
|
|
46
|
+
|
|
47
|
+
allowed_sort = {
|
|
48
|
+
"name": "name ASC",
|
|
49
|
+
"price_asc": "price ASC",
|
|
50
|
+
"price_desc": "price DESC",
|
|
51
|
+
"discount_desc": "discount DESC",
|
|
52
|
+
}
|
|
53
|
+
order_by = allowed_sort.get(sort, "name ASC")
|
|
54
|
+
|
|
55
|
+
params = []
|
|
56
|
+
where = ""
|
|
57
|
+
|
|
58
|
+
if search:
|
|
59
|
+
where = "WHERE LOWER(name) LIKE LOWER(%s) OR LOWER(category) LIKE LOWER(%s)"
|
|
60
|
+
params.extend([f"%{search}%", f"%{search}%"])
|
|
61
|
+
|
|
62
|
+
products = fetch_all(
|
|
63
|
+
f"""
|
|
64
|
+
SELECT id, name, category, price, discount, quantity, image
|
|
65
|
+
FROM products
|
|
66
|
+
{where}
|
|
67
|
+
ORDER BY {order_by}
|
|
68
|
+
""",
|
|
69
|
+
params,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
return render_template("products.html", products=products, search=search, sort=sort)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@app.route("/login", methods=["GET", "POST"])
|
|
76
|
+
def login():
|
|
77
|
+
if request.method == "POST":
|
|
78
|
+
login_value = request.form.get("login", "").strip()
|
|
79
|
+
password = request.form.get("password", "")
|
|
80
|
+
|
|
81
|
+
user = fetch_one(
|
|
82
|
+
"SELECT id, full_name, login, password_hash, role FROM users WHERE login = %s",
|
|
83
|
+
(login_value,),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if user and check_password_hash(user["password_hash"], password):
|
|
87
|
+
session["user_id"] = user["id"]
|
|
88
|
+
session["user_name"] = user["full_name"]
|
|
89
|
+
session["role"] = user["role"]
|
|
90
|
+
flash("Вход выполнен")
|
|
91
|
+
return redirect(url_for("index"))
|
|
92
|
+
|
|
93
|
+
flash("Неверный логин или пароль")
|
|
94
|
+
|
|
95
|
+
return render_template("login.html")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@app.route("/guest")
|
|
99
|
+
def guest():
|
|
100
|
+
session.clear()
|
|
101
|
+
session["role"] = "guest"
|
|
102
|
+
session["user_name"] = "Гость"
|
|
103
|
+
flash("Вы вошли как гость")
|
|
104
|
+
return redirect(url_for("index"))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@app.route("/logout")
|
|
108
|
+
def logout():
|
|
109
|
+
session.clear()
|
|
110
|
+
flash("Вы вышли")
|
|
111
|
+
return redirect(url_for("index"))
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@app.route("/products/new", methods=["GET", "POST"])
|
|
115
|
+
@role_required("admin")
|
|
116
|
+
def product_new():
|
|
117
|
+
if request.method == "POST":
|
|
118
|
+
execute(
|
|
119
|
+
"""
|
|
120
|
+
INSERT INTO products(name, category, price, discount, quantity, image)
|
|
121
|
+
VALUES (%s, %s, %s, %s, %s, %s)
|
|
122
|
+
""",
|
|
123
|
+
(
|
|
124
|
+
request.form["name"],
|
|
125
|
+
request.form["category"],
|
|
126
|
+
request.form["price"],
|
|
127
|
+
request.form["discount"],
|
|
128
|
+
request.form["quantity"],
|
|
129
|
+
request.form.get("image") or "placeholder.png",
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
flash("Товар добавлен")
|
|
133
|
+
return redirect(url_for("index"))
|
|
134
|
+
|
|
135
|
+
return render_template("product_form.html", product=None)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@app.route("/products/<int:product_id>/edit", methods=["GET", "POST"])
|
|
139
|
+
@role_required("admin")
|
|
140
|
+
def product_edit(product_id):
|
|
141
|
+
product = fetch_one("SELECT * FROM products WHERE id = %s", (product_id,))
|
|
142
|
+
if not product:
|
|
143
|
+
flash("Товар не найден")
|
|
144
|
+
return redirect(url_for("index"))
|
|
145
|
+
|
|
146
|
+
if request.method == "POST":
|
|
147
|
+
execute(
|
|
148
|
+
"""
|
|
149
|
+
UPDATE products
|
|
150
|
+
SET name=%s, category=%s, price=%s, discount=%s, quantity=%s, image=%s
|
|
151
|
+
WHERE id=%s
|
|
152
|
+
""",
|
|
153
|
+
(
|
|
154
|
+
request.form["name"],
|
|
155
|
+
request.form["category"],
|
|
156
|
+
request.form["price"],
|
|
157
|
+
request.form["discount"],
|
|
158
|
+
request.form["quantity"],
|
|
159
|
+
request.form.get("image") or "placeholder.png",
|
|
160
|
+
product_id,
|
|
161
|
+
),
|
|
162
|
+
)
|
|
163
|
+
flash("Товар изменён")
|
|
164
|
+
return redirect(url_for("index"))
|
|
165
|
+
|
|
166
|
+
return render_template("product_form.html", product=product)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@app.route("/products/<int:product_id>/delete", methods=["POST"])
|
|
170
|
+
@role_required("admin")
|
|
171
|
+
def product_delete(product_id):
|
|
172
|
+
execute("DELETE FROM products WHERE id = %s", (product_id,))
|
|
173
|
+
flash("Товар удалён")
|
|
174
|
+
return redirect(url_for("index"))
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@app.route("/orders")
|
|
178
|
+
@role_required("manager", "admin")
|
|
179
|
+
def orders():
|
|
180
|
+
rows = fetch_all(
|
|
181
|
+
"""
|
|
182
|
+
SELECT o.id, o.created_at, o.status, o.customer_name,
|
|
183
|
+
p.name AS product_name, o.quantity
|
|
184
|
+
FROM orders o
|
|
185
|
+
LEFT JOIN products p ON p.id = o.product_id
|
|
186
|
+
ORDER BY o.created_at DESC
|
|
187
|
+
"""
|
|
188
|
+
)
|
|
189
|
+
return render_template("orders.html", orders=rows)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@app.route("/orders/new", methods=["GET", "POST"])
|
|
193
|
+
@role_required("admin")
|
|
194
|
+
def order_new():
|
|
195
|
+
products = fetch_all("SELECT id, name FROM products ORDER BY name")
|
|
196
|
+
|
|
197
|
+
if request.method == "POST":
|
|
198
|
+
execute(
|
|
199
|
+
"""
|
|
200
|
+
INSERT INTO orders(customer_name, product_id, quantity, status)
|
|
201
|
+
VALUES (%s, %s, %s, %s)
|
|
202
|
+
""",
|
|
203
|
+
(
|
|
204
|
+
request.form["customer_name"],
|
|
205
|
+
request.form["product_id"],
|
|
206
|
+
request.form["quantity"],
|
|
207
|
+
request.form["status"],
|
|
208
|
+
),
|
|
209
|
+
)
|
|
210
|
+
flash("Заказ добавлен")
|
|
211
|
+
return redirect(url_for("orders"))
|
|
212
|
+
|
|
213
|
+
return render_template("order_form.html", products=products)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
if __name__ == "__main__":
|
|
217
|
+
app.run(debug=True)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
DROP TABLE IF EXISTS orders;
|
|
2
|
+
DROP TABLE IF EXISTS products;
|
|
3
|
+
DROP TABLE IF EXISTS users;
|
|
4
|
+
|
|
5
|
+
CREATE TABLE users (
|
|
6
|
+
id SERIAL PRIMARY KEY,
|
|
7
|
+
full_name VARCHAR(150) NOT NULL,
|
|
8
|
+
login VARCHAR(100) UNIQUE NOT NULL,
|
|
9
|
+
password_hash TEXT NOT NULL,
|
|
10
|
+
role VARCHAR(30) NOT NULL CHECK (role IN ('client', 'manager', 'admin'))
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
CREATE TABLE products (
|
|
14
|
+
id SERIAL PRIMARY KEY,
|
|
15
|
+
name VARCHAR(200) NOT NULL,
|
|
16
|
+
category VARCHAR(100) NOT NULL,
|
|
17
|
+
price NUMERIC(10, 2) NOT NULL DEFAULT 0,
|
|
18
|
+
discount NUMERIC(5, 2) NOT NULL DEFAULT 0,
|
|
19
|
+
quantity INTEGER NOT NULL DEFAULT 0,
|
|
20
|
+
image VARCHAR(255) NOT NULL DEFAULT 'placeholder.png'
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
CREATE TABLE orders (
|
|
24
|
+
id SERIAL PRIMARY KEY,
|
|
25
|
+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
26
|
+
customer_name VARCHAR(150) NOT NULL,
|
|
27
|
+
product_id INTEGER REFERENCES products(id) ON DELETE SET NULL,
|
|
28
|
+
quantity INTEGER NOT NULL DEFAULT 1,
|
|
29
|
+
status VARCHAR(50) NOT NULL DEFAULT 'new'
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
-- Пароль для всех тестовых пользователей: 12345
|
|
33
|
+
INSERT INTO users(full_name, login, password_hash, role) VALUES
|
|
34
|
+
('Администратор', 'admin', 'scrypt:32768:8:1$C03amjYCFIYpZ92w$c416a5a725fb16b73888a151896a170a6dbdd0d78658fa4a253b294b3d43eb022b18b55f61a2f3773f754dd3f041915c3d7c22118dd1f3fe6e14b4b55b08814d5', 'admin'),
|
|
35
|
+
('Менеджер', 'manager', 'scrypt:32768:8:1$C03amjYCFIYpZ92w$c416a5a725fb16b73888a151896a170a6dbdd0d78658fa4a253b294b3d43eb022b18b55f61a2f3773f754dd3f041915c3d7c22118dd1f3fe6e14b4b55b08814d5', 'manager'),
|
|
36
|
+
('Клиент', 'client', 'scrypt:32768:8:1$C03amjYCFIYpZ92w$c416a5a725fb16b73888a151896a170a6dbdd0d78658fa4a253b294b3d43eb022b18b55f61a2f3773f754dd3f041915c3d7c22118dd1f3fe6e14b4b55b08814d5', 'client');
|
|
37
|
+
|
|
38
|
+
INSERT INTO products(name, category, price, discount, quantity, image) VALUES
|
|
39
|
+
('Товар 1', 'Категория A', 2500, 5, 10, 'placeholder.png'),
|
|
40
|
+
('Товар 2', 'Категория B', 4200, 20, 7, 'placeholder.png'),
|
|
41
|
+
('Товар 3', 'Категория A', 1800, 0, 15, 'placeholder.png');
|
|
42
|
+
|
|
43
|
+
INSERT INTO orders(customer_name, product_id, quantity, status) VALUES
|
|
44
|
+
('Иван Петров', 1, 2, 'new'),
|
|
45
|
+
('Анна Смирнова', 2, 1, 'done');
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import psycopg2
|
|
3
|
+
from psycopg2.extras import RealDictCursor
|
|
4
|
+
from dotenv import load_dotenv
|
|
5
|
+
|
|
6
|
+
load_dotenv()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_connection():
|
|
10
|
+
return psycopg2.connect(
|
|
11
|
+
host=os.getenv("PGHOST", "localhost"),
|
|
12
|
+
port=os.getenv("PGPORT", "5432"),
|
|
13
|
+
dbname=os.getenv("PGDATABASE", "app_db"),
|
|
14
|
+
user=os.getenv("PGUSER", "postgres"),
|
|
15
|
+
password=os.getenv("PGPASSWORD", "postgres"),
|
|
16
|
+
cursor_factory=RealDictCursor,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def fetch_all(sql, params=None):
|
|
21
|
+
with get_connection() as conn:
|
|
22
|
+
with conn.cursor() as cur:
|
|
23
|
+
cur.execute(sql, params or ())
|
|
24
|
+
return cur.fetchall()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def fetch_one(sql, params=None):
|
|
28
|
+
with get_connection() as conn:
|
|
29
|
+
with conn.cursor() as cur:
|
|
30
|
+
cur.execute(sql, params or ())
|
|
31
|
+
return cur.fetchone()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def execute(sql, params=None):
|
|
35
|
+
with get_connection() as conn:
|
|
36
|
+
with conn.cursor() as cur:
|
|
37
|
+
cur.execute(sql, params or ())
|
|
38
|
+
conn.commit()
|
|
Binary file
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
* {
|
|
2
|
+
box-sizing: border-box;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
body {
|
|
6
|
+
margin: 0;
|
|
7
|
+
font-family: "Times New Roman", serif;
|
|
8
|
+
background: #f4f7f4;
|
|
9
|
+
color: #1c1c1c;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.top {
|
|
13
|
+
display: flex;
|
|
14
|
+
justify-content: space-between;
|
|
15
|
+
align-items: center;
|
|
16
|
+
padding: 18px 34px;
|
|
17
|
+
background: #ffffff;
|
|
18
|
+
border-bottom: 1px solid #dce8dc;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.brand {
|
|
22
|
+
font-size: 26px;
|
|
23
|
+
font-weight: bold;
|
|
24
|
+
color: #0b5f3a;
|
|
25
|
+
text-decoration: none;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
nav {
|
|
29
|
+
display: flex;
|
|
30
|
+
gap: 16px;
|
|
31
|
+
align-items: center;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
nav a {
|
|
35
|
+
color: #0b5f3a;
|
|
36
|
+
text-decoration: none;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.container {
|
|
40
|
+
max-width: 1100px;
|
|
41
|
+
margin: 28px auto;
|
|
42
|
+
padding: 0 20px;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.page-head {
|
|
46
|
+
display: flex;
|
|
47
|
+
justify-content: space-between;
|
|
48
|
+
align-items: center;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.messages {
|
|
52
|
+
margin-bottom: 18px;
|
|
53
|
+
padding: 12px;
|
|
54
|
+
background: #e8ffe8;
|
|
55
|
+
border: 1px solid #9be59b;
|
|
56
|
+
border-radius: 12px;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.filters {
|
|
60
|
+
display: flex;
|
|
61
|
+
gap: 12px;
|
|
62
|
+
margin: 20px 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
input,
|
|
66
|
+
select,
|
|
67
|
+
button,
|
|
68
|
+
.button {
|
|
69
|
+
padding: 10px 12px;
|
|
70
|
+
border-radius: 10px;
|
|
71
|
+
border: 1px solid #b9d8b9;
|
|
72
|
+
font-family: inherit;
|
|
73
|
+
font-size: 16px;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
button,
|
|
77
|
+
.button {
|
|
78
|
+
background: #00fa9a;
|
|
79
|
+
color: #06351f;
|
|
80
|
+
cursor: pointer;
|
|
81
|
+
text-decoration: none;
|
|
82
|
+
border: none;
|
|
83
|
+
display: inline-block;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.danger {
|
|
87
|
+
background: #ffb5b5;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.small {
|
|
91
|
+
padding: 8px 10px;
|
|
92
|
+
font-size: 14px;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.grid {
|
|
96
|
+
display: grid;
|
|
97
|
+
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
|
98
|
+
gap: 18px;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.product,
|
|
102
|
+
.card {
|
|
103
|
+
background: #ffffff;
|
|
104
|
+
border-radius: 18px;
|
|
105
|
+
padding: 18px;
|
|
106
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.product.big-discount {
|
|
110
|
+
background: #2e8b57;
|
|
111
|
+
color: #ffffff;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.product img {
|
|
115
|
+
width: 100%;
|
|
116
|
+
height: 150px;
|
|
117
|
+
object-fit: contain;
|
|
118
|
+
background: #f8fff8;
|
|
119
|
+
border-radius: 12px;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.actions {
|
|
123
|
+
display: flex;
|
|
124
|
+
gap: 8px;
|
|
125
|
+
align-items: center;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.narrow {
|
|
129
|
+
max-width: 460px;
|
|
130
|
+
margin: 0 auto;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
form label {
|
|
134
|
+
display: block;
|
|
135
|
+
margin-top: 12px;
|
|
136
|
+
margin-bottom: 6px;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
form input,
|
|
140
|
+
form select {
|
|
141
|
+
width: 100%;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
form button {
|
|
145
|
+
margin-top: 16px;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.hint {
|
|
149
|
+
color: #666;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
table {
|
|
153
|
+
width: 100%;
|
|
154
|
+
border-collapse: collapse;
|
|
155
|
+
background: #ffffff;
|
|
156
|
+
border-radius: 16px;
|
|
157
|
+
overflow: hidden;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
th,
|
|
161
|
+
td {
|
|
162
|
+
padding: 12px;
|
|
163
|
+
border-bottom: 1px solid #e5eee5;
|
|
164
|
+
text-align: left;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
th {
|
|
168
|
+
background: #7fff00;
|
|
169
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="ru">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>{{ title or "Jinja3 App" }}</title>
|
|
6
|
+
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<header class="top">
|
|
10
|
+
<a class="brand" href="{{ url_for('index') }}">Jinja3 App</a>
|
|
11
|
+
<nav>
|
|
12
|
+
<a href="{{ url_for('index') }}">Товары</a>
|
|
13
|
+
{% if current_user and current_user.role in ['manager', 'admin'] %}
|
|
14
|
+
<a href="{{ url_for('orders') }}">Заказы</a>
|
|
15
|
+
{% endif %}
|
|
16
|
+
{% if current_user and current_user.name %}
|
|
17
|
+
<span>{{ current_user.name }} / {{ current_user.role }}</span>
|
|
18
|
+
<a href="{{ url_for('logout') }}">Выход</a>
|
|
19
|
+
{% else %}
|
|
20
|
+
<a href="{{ url_for('login') }}">Вход</a>
|
|
21
|
+
<a href="{{ url_for('guest') }}">Гость</a>
|
|
22
|
+
{% endif %}
|
|
23
|
+
</nav>
|
|
24
|
+
</header>
|
|
25
|
+
|
|
26
|
+
<main class="container">
|
|
27
|
+
{% with messages = get_flashed_messages() %}
|
|
28
|
+
{% if messages %}
|
|
29
|
+
<div class="messages">
|
|
30
|
+
{% for message in messages %}
|
|
31
|
+
<div>{{ message }}</div>
|
|
32
|
+
{% endfor %}
|
|
33
|
+
</div>
|
|
34
|
+
{% endif %}
|
|
35
|
+
{% endwith %}
|
|
36
|
+
|
|
37
|
+
{% block content %}{% endblock %}
|
|
38
|
+
</main>
|
|
39
|
+
</body>
|
|
40
|
+
</html>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block content %}
|
|
4
|
+
<section class="card narrow">
|
|
5
|
+
<h1>Вход</h1>
|
|
6
|
+
<form method="post">
|
|
7
|
+
<label>Логин</label>
|
|
8
|
+
<input name="login" required>
|
|
9
|
+
|
|
10
|
+
<label>Пароль</label>
|
|
11
|
+
<input name="password" type="password" required>
|
|
12
|
+
|
|
13
|
+
<button>Войти</button>
|
|
14
|
+
</form>
|
|
15
|
+
|
|
16
|
+
<p class="hint">Тестовые логины: admin, manager, client. Пароль: 12345.</p>
|
|
17
|
+
</section>
|
|
18
|
+
{% endblock %}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block content %}
|
|
4
|
+
<section class="card narrow">
|
|
5
|
+
<h1>Новый заказ</h1>
|
|
6
|
+
|
|
7
|
+
<form method="post">
|
|
8
|
+
<label>Клиент</label>
|
|
9
|
+
<input name="customer_name" required>
|
|
10
|
+
|
|
11
|
+
<label>Товар</label>
|
|
12
|
+
<select name="product_id">
|
|
13
|
+
{% for product in products %}
|
|
14
|
+
<option value="{{ product.id }}">{{ product.name }}</option>
|
|
15
|
+
{% endfor %}
|
|
16
|
+
</select>
|
|
17
|
+
|
|
18
|
+
<label>Количество</label>
|
|
19
|
+
<input name="quantity" type="number" value="1" required>
|
|
20
|
+
|
|
21
|
+
<label>Статус</label>
|
|
22
|
+
<select name="status">
|
|
23
|
+
<option value="new">new</option>
|
|
24
|
+
<option value="done">done</option>
|
|
25
|
+
<option value="cancelled">cancelled</option>
|
|
26
|
+
</select>
|
|
27
|
+
|
|
28
|
+
<button>Сохранить</button>
|
|
29
|
+
</form>
|
|
30
|
+
</section>
|
|
31
|
+
{% endblock %}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block content %}
|
|
4
|
+
<div class="page-head">
|
|
5
|
+
<h1>Заказы</h1>
|
|
6
|
+
{% if current_user and current_user.role == 'admin' %}
|
|
7
|
+
<a class="button" href="{{ url_for('order_new') }}">Добавить заказ</a>
|
|
8
|
+
{% endif %}
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<table>
|
|
12
|
+
<tr>
|
|
13
|
+
<th>ID</th>
|
|
14
|
+
<th>Дата</th>
|
|
15
|
+
<th>Клиент</th>
|
|
16
|
+
<th>Товар</th>
|
|
17
|
+
<th>Количество</th>
|
|
18
|
+
<th>Статус</th>
|
|
19
|
+
</tr>
|
|
20
|
+
{% for order in orders %}
|
|
21
|
+
<tr>
|
|
22
|
+
<td>{{ order.id }}</td>
|
|
23
|
+
<td>{{ order.created_at }}</td>
|
|
24
|
+
<td>{{ order.customer_name }}</td>
|
|
25
|
+
<td>{{ order.product_name }}</td>
|
|
26
|
+
<td>{{ order.quantity }}</td>
|
|
27
|
+
<td>{{ order.status }}</td>
|
|
28
|
+
</tr>
|
|
29
|
+
{% endfor %}
|
|
30
|
+
</table>
|
|
31
|
+
{% endblock %}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block content %}
|
|
4
|
+
<section class="card narrow">
|
|
5
|
+
<h1>{% if product %}Редактирование{% else %}Новый товар{% endif %}</h1>
|
|
6
|
+
|
|
7
|
+
<form method="post">
|
|
8
|
+
<label>Название</label>
|
|
9
|
+
<input name="name" value="{{ product.name if product else '' }}" required>
|
|
10
|
+
|
|
11
|
+
<label>Категория</label>
|
|
12
|
+
<input name="category" value="{{ product.category if product else '' }}" required>
|
|
13
|
+
|
|
14
|
+
<label>Цена</label>
|
|
15
|
+
<input name="price" type="number" step="0.01" value="{{ product.price if product else 0 }}" required>
|
|
16
|
+
|
|
17
|
+
<label>Скидка</label>
|
|
18
|
+
<input name="discount" type="number" step="0.01" value="{{ product.discount if product else 0 }}" required>
|
|
19
|
+
|
|
20
|
+
<label>Количество</label>
|
|
21
|
+
<input name="quantity" type="number" value="{{ product.quantity if product else 0 }}" required>
|
|
22
|
+
|
|
23
|
+
<label>Картинка</label>
|
|
24
|
+
<input name="image" value="{{ product.image if product else 'placeholder.png' }}">
|
|
25
|
+
|
|
26
|
+
<button>Сохранить</button>
|
|
27
|
+
</form>
|
|
28
|
+
</section>
|
|
29
|
+
{% endblock %}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block content %}
|
|
4
|
+
<div class="page-head">
|
|
5
|
+
<h1>Товары</h1>
|
|
6
|
+
{% if current_user and current_user.role == 'admin' %}
|
|
7
|
+
<a class="button" href="{{ url_for('product_new') }}">Добавить товар</a>
|
|
8
|
+
{% endif %}
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<form class="filters" method="get">
|
|
12
|
+
<input name="q" placeholder="Поиск" value="{{ search }}">
|
|
13
|
+
<select name="sort">
|
|
14
|
+
<option value="name" {% if sort == 'name' %}selected{% endif %}>По названию</option>
|
|
15
|
+
<option value="price_asc" {% if sort == 'price_asc' %}selected{% endif %}>Цена ↑</option>
|
|
16
|
+
<option value="price_desc" {% if sort == 'price_desc' %}selected{% endif %}>Цена ↓</option>
|
|
17
|
+
<option value="discount_desc" {% if sort == 'discount_desc' %}selected{% endif %}>Скидка ↓</option>
|
|
18
|
+
</select>
|
|
19
|
+
<button>Применить</button>
|
|
20
|
+
</form>
|
|
21
|
+
|
|
22
|
+
<div class="grid">
|
|
23
|
+
{% for product in products %}
|
|
24
|
+
<article class="product {% if product.discount > 15 %}big-discount{% endif %}">
|
|
25
|
+
<img src="{{ url_for('static', filename='images/' + product.image) }}" alt="">
|
|
26
|
+
<h3>{{ product.name }}</h3>
|
|
27
|
+
<p>{{ product.category }}</p>
|
|
28
|
+
<p>Цена: {{ product.price }} ₽</p>
|
|
29
|
+
<p>Скидка: {{ product.discount }}%</p>
|
|
30
|
+
<p>На складе: {{ product.quantity }}</p>
|
|
31
|
+
|
|
32
|
+
{% if current_user and current_user.role == 'admin' %}
|
|
33
|
+
<div class="actions">
|
|
34
|
+
<a class="button small" href="{{ url_for('product_edit', product_id=product.id) }}">Редактировать</a>
|
|
35
|
+
<form method="post" action="{{ url_for('product_delete', product_id=product.id) }}">
|
|
36
|
+
<button class="danger small">Удалить</button>
|
|
37
|
+
</form>
|
|
38
|
+
</div>
|
|
39
|
+
{% endif %}
|
|
40
|
+
</article>
|
|
41
|
+
{% endfor %}
|
|
42
|
+
</div>
|
|
43
|
+
{% endblock %}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
src/jinja3kit/__init__.py
|
|
3
|
+
src/jinja3kit/cli.py
|
|
4
|
+
src/jinja3kit.egg-info/PKG-INFO
|
|
5
|
+
src/jinja3kit.egg-info/SOURCES.txt
|
|
6
|
+
src/jinja3kit.egg-info/dependency_links.txt
|
|
7
|
+
src/jinja3kit.egg-info/entry_points.txt
|
|
8
|
+
src/jinja3kit.egg-info/top_level.txt
|
|
9
|
+
src/jinja3kit/template/app.py
|
|
10
|
+
src/jinja3kit/template/db.py
|
|
11
|
+
src/jinja3kit/template/requirements.txt
|
|
12
|
+
src/jinja3kit/template/database/init.sql
|
|
13
|
+
src/jinja3kit/template/static/style.css
|
|
14
|
+
src/jinja3kit/template/static/images/placeholder.png
|
|
15
|
+
src/jinja3kit/template/templates/base.html
|
|
16
|
+
src/jinja3kit/template/templates/login.html
|
|
17
|
+
src/jinja3kit/template/templates/order_form.html
|
|
18
|
+
src/jinja3kit/template/templates/orders.html
|
|
19
|
+
src/jinja3kit/template/templates/product_form.html
|
|
20
|
+
src/jinja3kit/template/templates/products.html
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
jinja3kit
|