flask-core-utils 1.0.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.
- flask_core_utils-1.0.0/PKG-INFO +32 -0
- flask_core_utils-1.0.0/README.md +11 -0
- flask_core_utils-1.0.0/flask_core_utils/__init__.py +826 -0
- flask_core_utils-1.0.0/flask_core_utils.egg-info/PKG-INFO +32 -0
- flask_core_utils-1.0.0/flask_core_utils.egg-info/SOURCES.txt +7 -0
- flask_core_utils-1.0.0/flask_core_utils.egg-info/dependency_links.txt +1 -0
- flask_core_utils-1.0.0/flask_core_utils.egg-info/top_level.txt +1 -0
- flask_core_utils-1.0.0/setup.cfg +4 -0
- flask_core_utils-1.0.0/setup.py +19 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flask-core-utils
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Standard utility components and scaffolding for Flask applications
|
|
5
|
+
Home-page: https://github.com/flask-core-utils/scaffold
|
|
6
|
+
Author: Open Source Community
|
|
7
|
+
Author-email: support@flaskutils.org
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Framework :: Flask
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.6
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Dynamic: author
|
|
14
|
+
Dynamic: author-email
|
|
15
|
+
Dynamic: classifier
|
|
16
|
+
Dynamic: description
|
|
17
|
+
Dynamic: description-content-type
|
|
18
|
+
Dynamic: home-page
|
|
19
|
+
Dynamic: requires-python
|
|
20
|
+
Dynamic: summary
|
|
21
|
+
|
|
22
|
+
# Flask Core Utils
|
|
23
|
+
|
|
24
|
+
A standard utility extension for quick Flask project generation, boilerplate scaffolding, and development cheatsheets.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
bash
|
|
28
|
+
pip install flask-core-utils
|
|
29
|
+
|
|
30
|
+
## Usage:
|
|
31
|
+
import flask_core_utils
|
|
32
|
+
flask_core_utils.help()
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Flask Core Utils
|
|
2
|
+
|
|
3
|
+
A standard utility extension for quick Flask project generation, boilerplate scaffolding, and development cheatsheets.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
bash
|
|
7
|
+
pip install flask-core-utils
|
|
8
|
+
|
|
9
|
+
## Usage:
|
|
10
|
+
import flask_core_utils
|
|
11
|
+
flask_core_utils.help()
|
|
@@ -0,0 +1,826 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
def help():
|
|
4
|
+
"""Outputs a complete list of available utility functions."""
|
|
5
|
+
print("--- Flask Core Utils API ---")
|
|
6
|
+
print("init_project() - Scaffolds the entire Flask application structure.")
|
|
7
|
+
print("create_file(filename) - Generates a specific file (e.g., 'app.py').")
|
|
8
|
+
print("get_regex_patterns() - Prints standard regex validation patterns.")
|
|
9
|
+
print("get_db_class() - Prints psycopg2 database wrapper snippet.")
|
|
10
|
+
print("get_jinja_hints() - Prints Jinja2 syntax cheatsheet.")
|
|
11
|
+
print("get_auth_logic() - Prints standard Werkzeug auth logic.")
|
|
12
|
+
print("get_flexbox_tricks() - Prints CSS flexbox and grid alignment tricks.")
|
|
13
|
+
print("get_cyrillic_encoding() - Prints solutions for Cyrillic encoding issues.")
|
|
14
|
+
print("get_sql_crud() - Prints standard SQL INSERT/UPDATE/DELETE snippets.")
|
|
15
|
+
print("get_flask_setup() - Prints basic Flask initialization code.")
|
|
16
|
+
print("----------------------------")
|
|
17
|
+
|
|
18
|
+
def get_regex_patterns():
|
|
19
|
+
print("---Regex Patterns ---")
|
|
20
|
+
print("1. Phone Number (8(XXX) XXX-XX-XX format):")
|
|
21
|
+
print(r" HTML: pattern=\"8\(\d{3}\) \d{3}-\d{2}-\d{2}\"")
|
|
22
|
+
print(r" Python: re.match(r'^8\(\d{3}\) \d{3}-\d{2}-\d{2}$', phone)")
|
|
23
|
+
print("\n2. Username (Latin & Digits, min 6 chars):")
|
|
24
|
+
print(r" HTML: pattern=\"[A-Za-z0-9]{6,}\"")
|
|
25
|
+
print(r" Python: re.match(r'^[A-Za-z0-9]{6,}$', login)")
|
|
26
|
+
print("\n3. Full Name (Cyrillic & Spaces):")
|
|
27
|
+
print(r" HTML: pattern=\"^[А-Яа-яЁё\s]+$\"")
|
|
28
|
+
|
|
29
|
+
def get_db_class():
|
|
30
|
+
print("---Database Wrapper ---")
|
|
31
|
+
print("""class Database:
|
|
32
|
+
def __init__(self):
|
|
33
|
+
self.conn_str = "dbname='exam_db' user='postgres' password='' host='127.0.0.1'"
|
|
34
|
+
|
|
35
|
+
def query(self, sql, params=(), fetchone=False, fetchall=False):
|
|
36
|
+
with psycopg2.connect(self.conn_str) as conn:
|
|
37
|
+
with conn.cursor(cursor_factory=DictCursor) as cur:
|
|
38
|
+
cur.execute(sql, params)
|
|
39
|
+
if fetchone: return cur.fetchone()
|
|
40
|
+
if fetchall: return cur.fetchall()
|
|
41
|
+
conn.commit()""")
|
|
42
|
+
|
|
43
|
+
def get_jinja_hints():
|
|
44
|
+
print("---Jinja2 Syntax ---")
|
|
45
|
+
print("1. Static files:")
|
|
46
|
+
print("{{ url_for('static', filename='css/style.css') }}")
|
|
47
|
+
print("\n2. For Loop:")
|
|
48
|
+
print("{% for item in items %}\n {{ item.name }}\n{% endfor %}")
|
|
49
|
+
print("\n3. If Condition:")
|
|
50
|
+
print("{% if session.get('role_id') == 1 %}\n Success\n{% endif %}")
|
|
51
|
+
|
|
52
|
+
def get_auth_logic():
|
|
53
|
+
print("---Auth & Hashing ---")
|
|
54
|
+
print("""from werkzeug.security import generate_password_hash, check_password_hash
|
|
55
|
+
|
|
56
|
+
# Registration
|
|
57
|
+
hashed_pw = generate_password_hash(password)
|
|
58
|
+
db.query("INSERT INTO users (password) VALUES (%s)", (hashed_pw,))
|
|
59
|
+
|
|
60
|
+
# Login
|
|
61
|
+
user = db.query("SELECT * FROM users WHERE login=%s", (login,), fetchone=True)
|
|
62
|
+
if user and check_password_hash(user['password'], password_input):
|
|
63
|
+
session['user_id'] = user['id']""")
|
|
64
|
+
|
|
65
|
+
def get_flexbox_tricks():
|
|
66
|
+
print("---CSS Layouts ---")
|
|
67
|
+
print("1. Perfect Center (Flexbox):")
|
|
68
|
+
print("display: flex;\njustify-content: center;\nalign-items: center;")
|
|
69
|
+
print("\n2. Responsive Grid (Auto-fit):")
|
|
70
|
+
print("display: grid;\ngrid-template-columns: repeat(auto-fit, minmax(280px, 1fr));\ngap: 20px;")
|
|
71
|
+
|
|
72
|
+
def get_cyrillic_encoding():
|
|
73
|
+
print("---Cyrillic Encoding Fixes ---")
|
|
74
|
+
print("1. Flask JSON Response (prevent \\u0430 escape):")
|
|
75
|
+
print("app.config['JSON_AS_ASCII'] = False")
|
|
76
|
+
print("\n2. HTML Meta Tag (Put in <head>):")
|
|
77
|
+
print("<meta charset=\"UTF-8\">")
|
|
78
|
+
print("\n3. PostgreSQL DB Connection encoding:")
|
|
79
|
+
print("conn = psycopg2.connect(..., client_encoding='utf8')")
|
|
80
|
+
print("\n4. Python Open File with Cyrillic:")
|
|
81
|
+
print("with open('file.txt', 'w', encoding='utf-8') as f:")
|
|
82
|
+
|
|
83
|
+
def get_sql_crud():
|
|
84
|
+
print("---Standard SQL CRUD ---")
|
|
85
|
+
print("1. INSERT:")
|
|
86
|
+
print("INSERT INTO users (login, role_id) VALUES (%s, %s)")
|
|
87
|
+
print("\n2. UPDATE:")
|
|
88
|
+
print("UPDATE requests SET status_id=%s WHERE id=%s")
|
|
89
|
+
print("\n3. DELETE:")
|
|
90
|
+
print("DELETE FROM items WHERE id=%s")
|
|
91
|
+
print("\n4. SELECT with JOIN:")
|
|
92
|
+
print("SELECT r.id, u.fio FROM requests r JOIN users u ON r.user_id = u.id")
|
|
93
|
+
|
|
94
|
+
def get_flask_setup():
|
|
95
|
+
print("---Flask Initialization ---")
|
|
96
|
+
print("""from flask import Flask, render_template, request, session, redirect, flash
|
|
97
|
+
|
|
98
|
+
app = Flask(__name__)
|
|
99
|
+
app.secret_key = 'super_secret_key'
|
|
100
|
+
|
|
101
|
+
if __name__ == '__main__':
|
|
102
|
+
app.run(debug=True, host='0.0.0.0', port=5000)""")
|
|
103
|
+
|
|
104
|
+
def _get_files_dict():
|
|
105
|
+
return {
|
|
106
|
+
'script.sql': SQL_CONTENT,
|
|
107
|
+
'app.py': APP_CONTENT,
|
|
108
|
+
'static/css/style.css': CSS_CONTENT,
|
|
109
|
+
'templates/base.html': BASE_HTML,
|
|
110
|
+
'templates/index.html': INDEX_HTML,
|
|
111
|
+
'templates/login.html': LOGIN_HTML,
|
|
112
|
+
'templates/register.html': REGISTER_HTML,
|
|
113
|
+
'templates/account.html': ACCOUNT_HTML,
|
|
114
|
+
'templates/admin.html': ADMIN_HTML
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
def create_file(filename):
|
|
118
|
+
"""Generates a specific file from the boilerplate templates."""
|
|
119
|
+
files = _get_files_dict()
|
|
120
|
+
if filename in files:
|
|
121
|
+
if os.path.dirname(filename):
|
|
122
|
+
os.makedirs(os.path.dirname(filename), exist_ok=True)
|
|
123
|
+
with open(filename, 'w', encoding='utf-8') as f:
|
|
124
|
+
f.write(files[filename].strip() + '\n')
|
|
125
|
+
print(f"[*] Successfully generated: {filename}")
|
|
126
|
+
else:
|
|
127
|
+
print(f"[!] Error: File '{filename}' not found.")
|
|
128
|
+
print(f"Available files: {', '.join(files.keys())}")
|
|
129
|
+
|
|
130
|
+
def init_project():
|
|
131
|
+
"""Scaffolds the entire Flask application structure."""
|
|
132
|
+
dirs = ['templates', 'static/css', 'static/img']
|
|
133
|
+
for d in dirs:
|
|
134
|
+
os.makedirs(d, exist_ok=True)
|
|
135
|
+
|
|
136
|
+
files = _get_files_dict()
|
|
137
|
+
|
|
138
|
+
for filepath, content in files.items():
|
|
139
|
+
with open(filepath, 'w', encoding='utf-8') as f:
|
|
140
|
+
f.write(content.strip() + '\n')
|
|
141
|
+
|
|
142
|
+
print(">>> Flask project structure generated successfully!")
|
|
143
|
+
|
|
144
|
+
# ==========================================
|
|
145
|
+
# BOILERPLATE CONTENTS (RAW STRINGS)
|
|
146
|
+
# ==========================================
|
|
147
|
+
|
|
148
|
+
SQL_CONTENT = r'''
|
|
149
|
+
-- Справочники
|
|
150
|
+
CREATE TABLE roles (
|
|
151
|
+
id SERIAL PRIMARY KEY,
|
|
152
|
+
name VARCHAR(50) UNIQUE NOT NULL
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
CREATE TABLE statuses (
|
|
156
|
+
id SERIAL PRIMARY KEY,
|
|
157
|
+
name VARCHAR(50) UNIQUE NOT NULL
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
CREATE TABLE payment_methods (
|
|
161
|
+
id SERIAL PRIMARY KEY,
|
|
162
|
+
name VARCHAR(50) UNIQUE NOT NULL
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
CREATE TABLE courses (
|
|
166
|
+
id SERIAL PRIMARY KEY,
|
|
167
|
+
title VARCHAR(150) UNIQUE NOT NULL
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
-- Основные таблицы
|
|
171
|
+
CREATE TABLE users (
|
|
172
|
+
id SERIAL PRIMARY KEY,
|
|
173
|
+
login VARCHAR(50) UNIQUE NOT NULL,
|
|
174
|
+
password VARCHAR(255) NOT NULL,
|
|
175
|
+
fio VARCHAR(150) NOT NULL,
|
|
176
|
+
phone VARCHAR(20) NOT NULL,
|
|
177
|
+
email VARCHAR(100) NOT NULL,
|
|
178
|
+
role_id INTEGER REFERENCES roles(id) DEFAULT 1
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
CREATE TABLE requests (
|
|
182
|
+
id SERIAL PRIMARY KEY,
|
|
183
|
+
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
|
184
|
+
course_id INTEGER REFERENCES courses(id) ON DELETE CASCADE,
|
|
185
|
+
start_date DATE NOT NULL,
|
|
186
|
+
payment_method_id INTEGER REFERENCES payment_methods(id),
|
|
187
|
+
status_id INTEGER REFERENCES statuses(id) DEFAULT 1
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
CREATE TABLE reviews (
|
|
191
|
+
id SERIAL PRIMARY KEY,
|
|
192
|
+
request_id INTEGER REFERENCES requests(id) ON DELETE CASCADE,
|
|
193
|
+
review_text TEXT NOT NULL,
|
|
194
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
-- Заполнение базовыми данными
|
|
198
|
+
INSERT INTO roles (name) VALUES ('user'), ('admin');
|
|
199
|
+
|
|
200
|
+
INSERT INTO statuses (name) VALUES
|
|
201
|
+
('Новая'),
|
|
202
|
+
('Идет обучение'),
|
|
203
|
+
('Обучение завершено');
|
|
204
|
+
|
|
205
|
+
INSERT INTO payment_methods (name) VALUES
|
|
206
|
+
('Наличными'),
|
|
207
|
+
('Переводом по номеру телефона');
|
|
208
|
+
|
|
209
|
+
INSERT INTO courses (title) VALUES
|
|
210
|
+
('Основы алгоритмизации и программирования'),
|
|
211
|
+
('Основы веб-дизайна'),
|
|
212
|
+
('Основы проектирования баз данных');
|
|
213
|
+
|
|
214
|
+
INSERT INTO users (login, password, fio, phone, email, role_id)
|
|
215
|
+
VALUES ('Admin', 'KorokNET', 'Администратор', '8(999) 999-99-99', 'admin@system.local', 2);
|
|
216
|
+
'''
|
|
217
|
+
|
|
218
|
+
APP_CONTENT = r'''
|
|
219
|
+
from flask import Flask, render_template, request, redirect, session, flash
|
|
220
|
+
import psycopg2
|
|
221
|
+
from psycopg2.extras import DictCursor
|
|
222
|
+
from werkzeug.security import generate_password_hash, check_password_hash
|
|
223
|
+
import re
|
|
224
|
+
|
|
225
|
+
app = Flask(__name__)
|
|
226
|
+
app.secret_key = 'dev_key_123'
|
|
227
|
+
|
|
228
|
+
class Database:
|
|
229
|
+
def __init__(self):
|
|
230
|
+
self.conn_str = "dbname='exam_db' user='postgres' password='' host='127.0.0.1'"
|
|
231
|
+
|
|
232
|
+
def query(self, sql, params=(), fetchone=False, fetchall=False):
|
|
233
|
+
with psycopg2.connect(self.conn_str) as conn:
|
|
234
|
+
with conn.cursor(cursor_factory=DictCursor) as cur:
|
|
235
|
+
cur.execute(sql, params)
|
|
236
|
+
if fetchone: return cur.fetchone()
|
|
237
|
+
if fetchall: return cur.fetchall()
|
|
238
|
+
conn.commit()
|
|
239
|
+
|
|
240
|
+
db = Database()
|
|
241
|
+
|
|
242
|
+
@app.route('/')
|
|
243
|
+
def index():
|
|
244
|
+
reviews = db.query("""
|
|
245
|
+
SELECT rv.review_text, u.fio, c.title as course
|
|
246
|
+
FROM reviews rv
|
|
247
|
+
JOIN requests r ON rv.request_id = r.id
|
|
248
|
+
JOIN users u ON r.user_id = u.id
|
|
249
|
+
JOIN courses c ON r.course_id = c.id
|
|
250
|
+
ORDER BY rv.created_at DESC LIMIT 3
|
|
251
|
+
""", fetchall=True)
|
|
252
|
+
return render_template('index.html', reviews=reviews)
|
|
253
|
+
|
|
254
|
+
@app.route('/register', methods=['GET', 'POST'])
|
|
255
|
+
def register():
|
|
256
|
+
if request.method == 'POST':
|
|
257
|
+
login = request.form.get('login', '').strip()
|
|
258
|
+
password = request.form.get('password', '')
|
|
259
|
+
fio = request.form.get('fio', '').strip()
|
|
260
|
+
phone = request.form.get('phone', '').strip()
|
|
261
|
+
email = request.form.get('email', '').strip()
|
|
262
|
+
|
|
263
|
+
if not re.match(r'^[A-Za-z0-9]{6,}$', login):
|
|
264
|
+
flash("Логин должен состоять из латиницы и цифр (не менее 6 символов)", "error")
|
|
265
|
+
return redirect('/register')
|
|
266
|
+
if not re.match(r'^8\(\d{3}\) \d{3}-\d{2}-\d{2}$', phone):
|
|
267
|
+
flash("Укажите телефон в формате 8(XXX) XXX-XX-XX", "error")
|
|
268
|
+
return redirect('/register')
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
hashed_password = generate_password_hash(password)
|
|
272
|
+
db.query("""
|
|
273
|
+
INSERT INTO users (login, password, fio, phone, email, role_id)
|
|
274
|
+
VALUES (%s, %s, %s, %s, %s, 1)
|
|
275
|
+
""", (login, hashed_password, fio, phone, email))
|
|
276
|
+
flash("Регистрация успешна!", "success")
|
|
277
|
+
return redirect('/login')
|
|
278
|
+
except Exception:
|
|
279
|
+
flash("Данный логин уже зарегистрирован в системе", "error")
|
|
280
|
+
|
|
281
|
+
return render_template('register.html')
|
|
282
|
+
|
|
283
|
+
@app.route('/login', methods=['GET', 'POST'])
|
|
284
|
+
def login():
|
|
285
|
+
if request.method == 'POST':
|
|
286
|
+
login_input = request.form.get('login')
|
|
287
|
+
password_input = request.form.get('password')
|
|
288
|
+
|
|
289
|
+
user = db.query("SELECT * FROM users WHERE login=%s", (login_input,), fetchone=True)
|
|
290
|
+
|
|
291
|
+
if user:
|
|
292
|
+
is_valid = False
|
|
293
|
+
if user['login'] == 'Admin' and user['password'] == password_input:
|
|
294
|
+
is_valid = True
|
|
295
|
+
elif user['login'] != 'Admin' and check_password_hash(user['password'], password_input):
|
|
296
|
+
is_valid = True
|
|
297
|
+
|
|
298
|
+
if is_valid:
|
|
299
|
+
session['user_id'] = user['id']
|
|
300
|
+
session['role_id'] = user['role_id']
|
|
301
|
+
session['fio'] = user['fio']
|
|
302
|
+
return redirect('/admin') if user['role_id'] == 2 else redirect('/account')
|
|
303
|
+
|
|
304
|
+
flash("Неверный логин или пароль", "error")
|
|
305
|
+
return render_template('login.html')
|
|
306
|
+
|
|
307
|
+
@app.route('/account', methods=['GET', 'POST'])
|
|
308
|
+
def account():
|
|
309
|
+
if 'user_id' not in session or session.get('role_id') != 1:
|
|
310
|
+
return redirect('/login')
|
|
311
|
+
|
|
312
|
+
if request.method == 'POST':
|
|
313
|
+
db.query("""
|
|
314
|
+
INSERT INTO requests (user_id, course_id, start_date, payment_method_id, status_id)
|
|
315
|
+
VALUES (%s, %s, %s, %s, 1)
|
|
316
|
+
""", (session['user_id'], request.form.get('course_id'),
|
|
317
|
+
request.form.get('start_date'), request.form.get('payment_method_id')))
|
|
318
|
+
flash("Заявка успешно отправлена", "success")
|
|
319
|
+
return redirect('/account')
|
|
320
|
+
|
|
321
|
+
courses = db.query("SELECT * FROM courses", fetchall=True)
|
|
322
|
+
payments = db.query("SELECT * FROM payment_methods", fetchall=True)
|
|
323
|
+
|
|
324
|
+
user_requests = db.query("""
|
|
325
|
+
SELECT r.id, c.title as course, r.start_date, s.name as status, s.id as status_id, rv.review_text
|
|
326
|
+
FROM requests r
|
|
327
|
+
JOIN courses c ON r.course_id = c.id
|
|
328
|
+
JOIN statuses s ON r.status_id = s.id
|
|
329
|
+
LEFT JOIN reviews rv ON r.id = rv.request_id
|
|
330
|
+
WHERE r.user_id = %s ORDER BY r.id DESC
|
|
331
|
+
""", (session['user_id'],), fetchall=True)
|
|
332
|
+
|
|
333
|
+
return render_template('account.html', courses=courses, payments=payments, requests=user_requests)
|
|
334
|
+
|
|
335
|
+
@app.route('/leave_review', methods=['POST'])
|
|
336
|
+
def leave_review():
|
|
337
|
+
if 'user_id' in session:
|
|
338
|
+
db.query("INSERT INTO reviews (request_id, review_text) VALUES (%s, %s)",
|
|
339
|
+
(request.form.get('req_id'), request.form.get('review_text')))
|
|
340
|
+
return redirect('/account')
|
|
341
|
+
|
|
342
|
+
@app.route('/admin', methods=['GET', 'POST'])
|
|
343
|
+
def admin():
|
|
344
|
+
if session.get('role_id') != 2:
|
|
345
|
+
return redirect('/login')
|
|
346
|
+
|
|
347
|
+
if request.method == 'POST':
|
|
348
|
+
db.query("UPDATE requests SET status_id=%s WHERE id=%s",
|
|
349
|
+
(request.form.get('status_id'), request.form.get('req_id')))
|
|
350
|
+
return redirect('/admin')
|
|
351
|
+
|
|
352
|
+
statuses = db.query("SELECT * FROM statuses", fetchall=True)
|
|
353
|
+
all_requests = db.query("""
|
|
354
|
+
SELECT r.id, u.fio, u.phone, c.title as course, r.start_date, pm.name as payment, s.id as status_id, s.name as status
|
|
355
|
+
FROM requests r
|
|
356
|
+
JOIN users u ON r.user_id = u.id
|
|
357
|
+
JOIN courses c ON r.course_id = c.id
|
|
358
|
+
JOIN payment_methods pm ON r.payment_method_id = pm.id
|
|
359
|
+
JOIN statuses s ON r.status_id = s.id
|
|
360
|
+
ORDER BY r.id DESC
|
|
361
|
+
""", fetchall=True)
|
|
362
|
+
|
|
363
|
+
return render_template('admin.html', requests=all_requests, statuses=statuses)
|
|
364
|
+
|
|
365
|
+
@app.route('/logout')
|
|
366
|
+
def logout():
|
|
367
|
+
session.clear()
|
|
368
|
+
return redirect('/')
|
|
369
|
+
|
|
370
|
+
if __name__ == '__main__':
|
|
371
|
+
app.run(debug=True)
|
|
372
|
+
'''
|
|
373
|
+
|
|
374
|
+
CSS_CONTENT = r'''
|
|
375
|
+
:root {
|
|
376
|
+
--primary: #111827;
|
|
377
|
+
--primary-hover: #374151;
|
|
378
|
+
--bg: #f9fafb;
|
|
379
|
+
--surface: #ffffff;
|
|
380
|
+
--border: #e5e7eb;
|
|
381
|
+
--text-main: #111827;
|
|
382
|
+
--text-muted: #6b7280;
|
|
383
|
+
--radius: 12px;
|
|
384
|
+
--shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
388
|
+
|
|
389
|
+
body {
|
|
390
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
391
|
+
background-color: var(--bg);
|
|
392
|
+
color: var(--text-main);
|
|
393
|
+
display: flex;
|
|
394
|
+
flex-direction: column;
|
|
395
|
+
min-height: 100vh;
|
|
396
|
+
line-height: 1.5;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.container { width: 100%; max-width: 1200px; margin: 0 auto; padding: 0 20px; }
|
|
400
|
+
.main-content { flex: 1; padding: 40px 0; }
|
|
401
|
+
|
|
402
|
+
.navbar {
|
|
403
|
+
background: var(--surface);
|
|
404
|
+
border-bottom: 1px solid var(--border);
|
|
405
|
+
padding: 16px 0;
|
|
406
|
+
position: sticky; top: 0; z-index: 100;
|
|
407
|
+
}
|
|
408
|
+
.navbar .container { display: flex; justify-content: space-between; align-items: center; gap: 20px; }
|
|
409
|
+
.navbar-brand { font-size: 1.25rem; font-weight: 800; text-decoration: none; color: var(--text-main); }
|
|
410
|
+
|
|
411
|
+
.nav-links { display: flex; gap: 24px; align-items: center; flex-wrap: wrap; }
|
|
412
|
+
.nav-links a { color: var(--text-muted); text-decoration: none; font-weight: 500; position: relative; padding-bottom: 4px;}
|
|
413
|
+
.nav-links a::after {
|
|
414
|
+
content: ''; position: absolute; width: 0; height: 2px;
|
|
415
|
+
bottom: 0; left: 0; background-color: var(--primary);
|
|
416
|
+
transition: width 0.3s ease;
|
|
417
|
+
}
|
|
418
|
+
.nav-links a:hover { color: var(--primary); }
|
|
419
|
+
.nav-links a:hover::after { width: 100%; }
|
|
420
|
+
|
|
421
|
+
.card {
|
|
422
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
423
|
+
border-radius: var(--radius); padding: 32px;
|
|
424
|
+
box-shadow: var(--shadow); width: 100%; max-width: 450px; margin: 0 auto;
|
|
425
|
+
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
|
426
|
+
}
|
|
427
|
+
.card:hover { transform: translateY(-4px); box-shadow: 0 12px 24px rgba(0, 0, 0, 0.08); }
|
|
428
|
+
.card h3 { margin-bottom: 24px; font-size: 1.5rem; text-align: center; }
|
|
429
|
+
|
|
430
|
+
.grid-container {
|
|
431
|
+
display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
432
|
+
gap: 24px; margin-top: 20px;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
.form-group { margin-bottom: 20px; }
|
|
436
|
+
.form-group label { display: block; margin-bottom: 8px; font-size: 0.9rem; font-weight: 600; }
|
|
437
|
+
.form-control {
|
|
438
|
+
width: 100%; padding: 12px 16px; border: 1px solid var(--border);
|
|
439
|
+
border-radius: 8px; font-size: 1rem; font-family: inherit;
|
|
440
|
+
background-color: var(--bg); transition: all 0.2s ease;
|
|
441
|
+
}
|
|
442
|
+
.form-control:focus { outline: none; border-color: var(--primary); background-color: var(--surface); box-shadow: 0 0 0 3px rgba(17, 24, 39, 0.1); }
|
|
443
|
+
|
|
444
|
+
.btn {
|
|
445
|
+
display: inline-block; width: 100%; padding: 12px 20px;
|
|
446
|
+
background-color: var(--primary); color: #ffffff; border: none;
|
|
447
|
+
border-radius: 8px; font-size: 1rem; font-weight: 600; cursor: pointer;
|
|
448
|
+
text-align: center; text-decoration: none; transition: all 0.2s ease;
|
|
449
|
+
}
|
|
450
|
+
.btn:hover { background-color: var(--primary-hover); }
|
|
451
|
+
|
|
452
|
+
.table-responsive { width: 100%; overflow-x: auto; margin-top: 16px; }
|
|
453
|
+
table { width: 100%; border-collapse: collapse; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
|
|
454
|
+
th, td { padding: 16px; text-align: left; border-bottom: 1px solid var(--border); font-size: 0.95rem; }
|
|
455
|
+
th { background: var(--bg); font-weight: 600; color: var(--text-muted); text-transform: uppercase; font-size: 0.8rem; }
|
|
456
|
+
tr:hover td { background-color: var(--bg); }
|
|
457
|
+
|
|
458
|
+
.badge { padding: 6px 12px; border-radius: 20px; font-size: 0.75rem; font-weight: 700; display: inline-block; }
|
|
459
|
+
.status-1 { background-color: #fef3c7; color: #b45309; }
|
|
460
|
+
.status-2 { background-color: #e0e7ff; color: #4338ca; }
|
|
461
|
+
.status-3 { background-color: #d1fae5; color: #047857; }
|
|
462
|
+
|
|
463
|
+
.alert { padding: 16px; border-radius: 8px; margin-bottom: 24px; font-weight: 500; }
|
|
464
|
+
.alert-error { background: #fee2e2; color: #b91c1c; border: 1px solid #fecaca; }
|
|
465
|
+
.alert-success { background: #d1fae5; color: #047857; border: 1px solid #a7f3d0; }
|
|
466
|
+
|
|
467
|
+
.account-grid { display: flex; gap: 30px; align-items: flex-start; }
|
|
468
|
+
.account-sidebar { width: 35%; }
|
|
469
|
+
.account-content { width: 65%; }
|
|
470
|
+
|
|
471
|
+
.slider-container {
|
|
472
|
+
width: 100%; max-width: 1000px; margin: 0 auto 40px auto;
|
|
473
|
+
position: relative; overflow: hidden; border-radius: 16px; box-shadow: var(--shadow);
|
|
474
|
+
}
|
|
475
|
+
.slides { display: flex; transition: transform 0.5s ease-in-out; }
|
|
476
|
+
.slides img { width: 100%; height: 400px; object-fit: cover; flex-shrink: 0; }
|
|
477
|
+
.prev, .next {
|
|
478
|
+
position: absolute; top: 50%; transform: translateY(-50%); background: rgba(255, 255, 255, 0.9);
|
|
479
|
+
border: none; width: 44px; height: 44px; border-radius: 50%; cursor: pointer; font-size: 20px;
|
|
480
|
+
color: var(--text-main); display: flex; align-items: center; justify-content: center;
|
|
481
|
+
box-shadow: 0 4px 6px rgba(0,0,0,0.1); transition: all 0.2s ease;
|
|
482
|
+
}
|
|
483
|
+
.prev:hover, .next:hover { background: #ffffff; transform: translateY(-50%) scale(1.1); }
|
|
484
|
+
.next { right: 20px; } .prev { left: 20px; }
|
|
485
|
+
|
|
486
|
+
.slider-overlay { position: absolute; bottom: 30px; left: 50%; transform: translateX(-50%); z-index: 10; width: max-content; }
|
|
487
|
+
.btn-slider {
|
|
488
|
+
display: inline-block; background: #ffffff; color: var(--text-main); padding: 14px 36px;
|
|
489
|
+
font-size: 1rem; font-weight: 700; text-decoration: none; border-radius: 50px;
|
|
490
|
+
box-shadow: 0 10px 25px rgba(0,0,0,0.15); transition: all 0.2s ease;
|
|
491
|
+
}
|
|
492
|
+
.btn-slider:hover { transform: scale(1.05); color: var(--primary); }
|
|
493
|
+
|
|
494
|
+
.footer { border-top: 1px solid var(--border); padding: 30px 0; margin-top: auto; text-align: center; color: var(--text-muted); font-size: 0.9rem; }
|
|
495
|
+
|
|
496
|
+
@media (max-width: 768px) {
|
|
497
|
+
.navbar .container { flex-direction: column; gap: 12px; }
|
|
498
|
+
.nav-links { justify-content: center; gap: 16px; }
|
|
499
|
+
.card { padding: 24px; margin: 20px auto; }
|
|
500
|
+
.account-grid { flex-direction: column; gap: 20px; }
|
|
501
|
+
.account-sidebar, .account-content { width: 100%; }
|
|
502
|
+
.slides img { height: 250px; }
|
|
503
|
+
th, td { padding: 12px; font-size: 0.85rem; }
|
|
504
|
+
}
|
|
505
|
+
'''
|
|
506
|
+
|
|
507
|
+
BASE_HTML = r'''
|
|
508
|
+
<!DOCTYPE html>
|
|
509
|
+
<html lang="ru">
|
|
510
|
+
<head>
|
|
511
|
+
<meta charset="UTF-8">
|
|
512
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
513
|
+
<title>Информационная система</title>
|
|
514
|
+
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='img/favicon.ico') }}">
|
|
515
|
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
|
516
|
+
</head>
|
|
517
|
+
<body>
|
|
518
|
+
<nav class="navbar">
|
|
519
|
+
<div class="container">
|
|
520
|
+
<a href="/" class="navbar-brand">Главная панель</a>
|
|
521
|
+
<div class="nav-links">
|
|
522
|
+
<a href="/">Главная</a>
|
|
523
|
+
{% if session.get('user_id') %}
|
|
524
|
+
{% if session.get('role_id') == 1 %}
|
|
525
|
+
<a href="/account">Личный кабинет</a>
|
|
526
|
+
{% else %}
|
|
527
|
+
<a href="/admin">Панель управления</a>
|
|
528
|
+
{% endif %}
|
|
529
|
+
<a href="/logout">Выйти</a>
|
|
530
|
+
{% else %}
|
|
531
|
+
<a href="/login">Вход</a>
|
|
532
|
+
<a href="/register">Регистрация</a>
|
|
533
|
+
{% endif %}
|
|
534
|
+
</div>
|
|
535
|
+
</div>
|
|
536
|
+
</nav>
|
|
537
|
+
|
|
538
|
+
<div class="main-content">
|
|
539
|
+
<div class="container">
|
|
540
|
+
{% with messages = get_flashed_messages(with_categories=true) %}
|
|
541
|
+
{% if messages %}
|
|
542
|
+
{% for category, message in messages %}
|
|
543
|
+
<div class="alert alert-{{ category }}">{{ message }}</div>
|
|
544
|
+
{% endfor %}
|
|
545
|
+
{% endif %}
|
|
546
|
+
{% endwith %}
|
|
547
|
+
|
|
548
|
+
{% block content %}{% endblock %}
|
|
549
|
+
</div>
|
|
550
|
+
</div>
|
|
551
|
+
|
|
552
|
+
<footer class="footer">
|
|
553
|
+
<div class="container">
|
|
554
|
+
<p>© 2026 Разработка информационной системы.</p>
|
|
555
|
+
</div>
|
|
556
|
+
</footer>
|
|
557
|
+
</body>
|
|
558
|
+
</html>
|
|
559
|
+
'''
|
|
560
|
+
|
|
561
|
+
INDEX_HTML = r'''
|
|
562
|
+
{% extends 'base.html' %}
|
|
563
|
+
{% block content %}
|
|
564
|
+
|
|
565
|
+
<div style="text-align: center; margin-bottom: 40px;">
|
|
566
|
+
<h1>Информационный портал</h1>
|
|
567
|
+
<p style="color: #6b7280; margin-top: 10px;">Удобный сервис для оформления и отслеживания заявок.</p>
|
|
568
|
+
</div>
|
|
569
|
+
|
|
570
|
+
<div class="slider-container">
|
|
571
|
+
<div class="slides" id="slides">
|
|
572
|
+
<img src="{{ url_for('static', filename='img/image07.jpg') }}" alt="Иллюстрация 1">
|
|
573
|
+
<img src="{{ url_for('static', filename='img/image08.webp') }}" alt="Иллюстрация 2">
|
|
574
|
+
<img src="{{ url_for('static', filename='img/image10.webp') }}" alt="Иллюстрация 3">
|
|
575
|
+
<img src="{{ url_for('static', filename='img/image13.webp') }}" alt="Иллюстрация 4">
|
|
576
|
+
</div>
|
|
577
|
+
|
|
578
|
+
{% if session.get('role_id') != 2 %}
|
|
579
|
+
<div class="slider-overlay">
|
|
580
|
+
<a href="{% if session.get('user_id') %}/account{% else %}/login{% endif %}" class="btn-slider">Начать работу</a>
|
|
581
|
+
</div>
|
|
582
|
+
{% endif %}
|
|
583
|
+
|
|
584
|
+
<button class="prev" onclick="moveSlide(-1)">❮</button>
|
|
585
|
+
<button class="next" onclick="moveSlide(1)">❯</button>
|
|
586
|
+
</div>
|
|
587
|
+
|
|
588
|
+
{% if reviews %}
|
|
589
|
+
<div style="margin-top: 60px;">
|
|
590
|
+
<h3 style="text-align: center; margin-bottom: 20px;">Отзывы пользователей</h3>
|
|
591
|
+
<div class="grid-container">
|
|
592
|
+
{% for r in reviews %}
|
|
593
|
+
<div class="card" style="margin: 0; max-width: 100%;">
|
|
594
|
+
<strong>{{ r.fio }}</strong><br>
|
|
595
|
+
<small style="color: #6b7280;">Объект: {{ r.course }}</small>
|
|
596
|
+
<p style="margin-top: 10px; font-style: italic;">«{{ r.review_text }}»</p>
|
|
597
|
+
</div>
|
|
598
|
+
{% endfor %}
|
|
599
|
+
</div>
|
|
600
|
+
</div>
|
|
601
|
+
{% endif %}
|
|
602
|
+
|
|
603
|
+
<script>
|
|
604
|
+
let currentIndex = 0;
|
|
605
|
+
const slides = document.getElementById('slides');
|
|
606
|
+
const totalSlides = 4;
|
|
607
|
+
|
|
608
|
+
function moveSlide(step) {
|
|
609
|
+
currentIndex += step;
|
|
610
|
+
if (currentIndex >= totalSlides) currentIndex = 0;
|
|
611
|
+
if (currentIndex < 0) currentIndex = totalSlides - 1;
|
|
612
|
+
slides.style.transform = `translateX(-${currentIndex * 100}%)`;
|
|
613
|
+
}
|
|
614
|
+
setInterval(() => { moveSlide(1); }, 3000);
|
|
615
|
+
</script>
|
|
616
|
+
{% endblock %}
|
|
617
|
+
'''
|
|
618
|
+
|
|
619
|
+
LOGIN_HTML = r'''
|
|
620
|
+
{% extends 'base.html' %}
|
|
621
|
+
{% block content %}
|
|
622
|
+
<div class="card">
|
|
623
|
+
<h3>Вход в систему</h3>
|
|
624
|
+
<form method="POST">
|
|
625
|
+
<div class="form-group">
|
|
626
|
+
<label>Логин</label>
|
|
627
|
+
<input type="text" name="login" class="form-control" required>
|
|
628
|
+
</div>
|
|
629
|
+
<div class="form-group">
|
|
630
|
+
<label>Пароль</label>
|
|
631
|
+
<input type="password" name="password" class="form-control" required>
|
|
632
|
+
</div>
|
|
633
|
+
<button type="submit" class="btn">Войти</button>
|
|
634
|
+
</form>
|
|
635
|
+
<div style="text-align: center; margin-top: 15px;">
|
|
636
|
+
<a href="/register" style="text-decoration: none; color: #3498db;">Еще не зарегистрированы? Регистрация</a>
|
|
637
|
+
</div>
|
|
638
|
+
</div>
|
|
639
|
+
{% endblock %}
|
|
640
|
+
'''
|
|
641
|
+
|
|
642
|
+
REGISTER_HTML = r'''
|
|
643
|
+
{% extends 'base.html' %}
|
|
644
|
+
{% block content %}
|
|
645
|
+
<div class="card">
|
|
646
|
+
<h3>Регистрация</h3>
|
|
647
|
+
<div id="jsError" class="alert alert-error" style="display: none;"></div>
|
|
648
|
+
|
|
649
|
+
<form method="POST" onsubmit="return validateForm()">
|
|
650
|
+
<div class="form-group">
|
|
651
|
+
<label>Логин</label>
|
|
652
|
+
<input type="text" name="login" id="login" class="form-control" pattern="[A-Za-z0-9]{6,}" placeholder="Латиница и цифры (мин. 6)" required>
|
|
653
|
+
</div>
|
|
654
|
+
<div class="form-group">
|
|
655
|
+
<label>Пароль</label>
|
|
656
|
+
<input type="password" name="password" class="form-control" minlength="8" required>
|
|
657
|
+
</div>
|
|
658
|
+
<div class="form-group">
|
|
659
|
+
<label>ФИО</label>
|
|
660
|
+
<input type="text" name="fio" class="form-control" pattern="^[А-Яа-яЁё\s]+$" placeholder="Иванов Иван Иванович" required>
|
|
661
|
+
</div>
|
|
662
|
+
<div class="form-group">
|
|
663
|
+
<label>Телефон</label>
|
|
664
|
+
<input type="text" name="phone" id="phone" class="form-control" pattern="8\(\d{3}\) \d{3}-\d{2}-\d{2}" placeholder="8(XXX) XXX-XX-XX" required>
|
|
665
|
+
</div>
|
|
666
|
+
<div class="form-group">
|
|
667
|
+
<label>Email</label>
|
|
668
|
+
<input type="email" name="email" class="form-control" required>
|
|
669
|
+
</div>
|
|
670
|
+
<button type="submit" class="btn">Создать аккаунт</button>
|
|
671
|
+
</form>
|
|
672
|
+
</div>
|
|
673
|
+
|
|
674
|
+
<script>
|
|
675
|
+
function validateForm() {
|
|
676
|
+
const phone = document.getElementById('phone').value;
|
|
677
|
+
const errorBox = document.getElementById('jsError');
|
|
678
|
+
const phoneRegex = /^8\(\d{3}\) \d{3}-\d{2}-\d{2}$/;
|
|
679
|
+
|
|
680
|
+
if (!phoneRegex.test(phone)) {
|
|
681
|
+
errorBox.textContent = "Укажите телефон в формате 8(XXX) XXX-XX-XX";
|
|
682
|
+
errorBox.style.display = "block";
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
685
|
+
return true;
|
|
686
|
+
}
|
|
687
|
+
</script>
|
|
688
|
+
{% endblock %}
|
|
689
|
+
'''
|
|
690
|
+
|
|
691
|
+
ACCOUNT_HTML = r'''
|
|
692
|
+
{% extends 'base.html' %}
|
|
693
|
+
{% block content %}
|
|
694
|
+
<div class="account-grid">
|
|
695
|
+
<div class="card account-sidebar" style="margin: 0; max-width: 100%;">
|
|
696
|
+
<h4 style="margin-bottom: 16px;">Оформление заявки</h4>
|
|
697
|
+
<form method="POST">
|
|
698
|
+
<div class="form-group">
|
|
699
|
+
<label>Выберите объект</label>
|
|
700
|
+
<select name="course_id" class="form-control" required>
|
|
701
|
+
{% for course in courses %}
|
|
702
|
+
<option value="{{ course.id }}">{{ course.title }}</option>
|
|
703
|
+
{% endfor %}
|
|
704
|
+
</select>
|
|
705
|
+
</div>
|
|
706
|
+
<div class="form-group">
|
|
707
|
+
<label>Требуемая дата</label>
|
|
708
|
+
<input type="date" name="start_date" class="form-control" required>
|
|
709
|
+
</div>
|
|
710
|
+
<div class="form-group">
|
|
711
|
+
<label>Способ оплаты</label>
|
|
712
|
+
<select name="payment_method_id" class="form-control" required>
|
|
713
|
+
{% for p in payments %}
|
|
714
|
+
<option value="{{ p.id }}">{{ p.name }}</option>
|
|
715
|
+
{% endfor %}
|
|
716
|
+
</select>
|
|
717
|
+
</div>
|
|
718
|
+
<button type="submit" class="btn">Отправить заявку</button>
|
|
719
|
+
</form>
|
|
720
|
+
</div>
|
|
721
|
+
|
|
722
|
+
<div class="account-content">
|
|
723
|
+
<h3 style="margin-bottom: 16px;">История обращений</h3>
|
|
724
|
+
<div class="table-responsive">
|
|
725
|
+
<table>
|
|
726
|
+
<thead>
|
|
727
|
+
<tr>
|
|
728
|
+
<th>Объект</th>
|
|
729
|
+
<th>Дата</th>
|
|
730
|
+
<th>Статус</th>
|
|
731
|
+
<th>Оценка</th>
|
|
732
|
+
</tr>
|
|
733
|
+
</thead>
|
|
734
|
+
<tbody>
|
|
735
|
+
{% for r in requests %}
|
|
736
|
+
<tr>
|
|
737
|
+
<td>{{ r.course }}</td>
|
|
738
|
+
<td>{{ r.start_date }}</td>
|
|
739
|
+
<td><span class="badge status-{{ r.status_id }}">{{ r.status }}</span></td>
|
|
740
|
+
<td>
|
|
741
|
+
{% if r.status_id == 3 %}
|
|
742
|
+
{% if r.review_text %}
|
|
743
|
+
<small>«{{ r.review_text }}»</small>
|
|
744
|
+
{% else %}
|
|
745
|
+
<form method="POST" action="/leave_review" style="margin: 0; display: flex; gap: 5px;">
|
|
746
|
+
<input type="hidden" name="req_id" value="{{ r.id }}">
|
|
747
|
+
<input type="text" name="review_text" class="form-control" style="padding: 4px;" required>
|
|
748
|
+
<button type="submit" class="btn" style="padding: 4px;">✔</button>
|
|
749
|
+
</form>
|
|
750
|
+
{% endif %}
|
|
751
|
+
{% else %}
|
|
752
|
+
<span style="font-size: 0.85rem; color: #9ca3af;">Ожидание</span>
|
|
753
|
+
{% endif %}
|
|
754
|
+
</td>
|
|
755
|
+
</tr>
|
|
756
|
+
{% endfor %}
|
|
757
|
+
</tbody>
|
|
758
|
+
</table>
|
|
759
|
+
</div>
|
|
760
|
+
</div>
|
|
761
|
+
</div>
|
|
762
|
+
{% endblock %}
|
|
763
|
+
'''
|
|
764
|
+
|
|
765
|
+
ADMIN_HTML = r'''
|
|
766
|
+
{% extends 'base.html' %}
|
|
767
|
+
{% block content %}
|
|
768
|
+
<div class="card" style="max-width: 100%; margin: 0; padding: 20px;">
|
|
769
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; flex-wrap: wrap; gap: 10px;">
|
|
770
|
+
<h3 style="margin: 0;">Управление заявками</h3>
|
|
771
|
+
<input type="text" id="searchInput" onkeyup="filterTable()" placeholder="Поиск по таблице..." class="form-control" style="width: 250px;">
|
|
772
|
+
</div>
|
|
773
|
+
|
|
774
|
+
<div class="table-responsive">
|
|
775
|
+
<table id="adminTable">
|
|
776
|
+
<thead>
|
|
777
|
+
<tr>
|
|
778
|
+
<th>Пользователь</th>
|
|
779
|
+
<th>Объект / Дата</th>
|
|
780
|
+
<th>Оплата</th>
|
|
781
|
+
<th>Статус</th>
|
|
782
|
+
</tr>
|
|
783
|
+
</thead>
|
|
784
|
+
<tbody>
|
|
785
|
+
{% for r in requests %}
|
|
786
|
+
<tr>
|
|
787
|
+
<td>
|
|
788
|
+
<strong>{{ r.fio }}</strong><br>
|
|
789
|
+
<small>{{ r.phone }}</small>
|
|
790
|
+
</td>
|
|
791
|
+
<td>
|
|
792
|
+
{{ r.course }}<br>
|
|
793
|
+
<small>{{ r.start_date }}</small>
|
|
794
|
+
</td>
|
|
795
|
+
<td>{{ r.payment }}</td>
|
|
796
|
+
<td>
|
|
797
|
+
<form method="POST" style="display: flex; gap: 5px; margin: 0;">
|
|
798
|
+
<input type="hidden" name="req_id" value="{{ r.id }}">
|
|
799
|
+
<select name="status_id" class="form-control" style="padding: 4px; font-size: 0.85rem;">
|
|
800
|
+
{% for s in statuses %}
|
|
801
|
+
<option value="{{ s.id }}" {% if s.id == r.status_id %}selected{% endif %}>{{ s.name }}</option>
|
|
802
|
+
{% endfor %}
|
|
803
|
+
</select>
|
|
804
|
+
<button type="submit" class="btn" style="width: auto; padding: 4px 10px;">✓</button>
|
|
805
|
+
</form>
|
|
806
|
+
</td>
|
|
807
|
+
</tr>
|
|
808
|
+
{% endfor %}
|
|
809
|
+
</tbody>
|
|
810
|
+
</table>
|
|
811
|
+
</div>
|
|
812
|
+
</div>
|
|
813
|
+
|
|
814
|
+
<script>
|
|
815
|
+
function filterTable() {
|
|
816
|
+
let input = document.getElementById("searchInput").value.toLowerCase();
|
|
817
|
+
let rows = document.querySelectorAll("#adminTable tbody tr");
|
|
818
|
+
|
|
819
|
+
rows.forEach(row => {
|
|
820
|
+
let text = row.innerText.toLowerCase();
|
|
821
|
+
row.style.display = text.includes(input) ? "" : "none";
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
</script>
|
|
825
|
+
{% endblock %}
|
|
826
|
+
'''
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flask-core-utils
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Standard utility components and scaffolding for Flask applications
|
|
5
|
+
Home-page: https://github.com/flask-core-utils/scaffold
|
|
6
|
+
Author: Open Source Community
|
|
7
|
+
Author-email: support@flaskutils.org
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Framework :: Flask
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.6
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Dynamic: author
|
|
14
|
+
Dynamic: author-email
|
|
15
|
+
Dynamic: classifier
|
|
16
|
+
Dynamic: description
|
|
17
|
+
Dynamic: description-content-type
|
|
18
|
+
Dynamic: home-page
|
|
19
|
+
Dynamic: requires-python
|
|
20
|
+
Dynamic: summary
|
|
21
|
+
|
|
22
|
+
# Flask Core Utils
|
|
23
|
+
|
|
24
|
+
A standard utility extension for quick Flask project generation, boilerplate scaffolding, and development cheatsheets.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
bash
|
|
28
|
+
pip install flask-core-utils
|
|
29
|
+
|
|
30
|
+
## Usage:
|
|
31
|
+
import flask_core_utils
|
|
32
|
+
flask_core_utils.help()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
flask_core_utils
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name='flask-core-utils',
|
|
5
|
+
version='1.0.0',
|
|
6
|
+
packages=find_packages(),
|
|
7
|
+
description='Standard utility components and scaffolding for Flask applications',
|
|
8
|
+
long_description=open('README.md', encoding='utf-8').read(),
|
|
9
|
+
long_description_content_type='text/markdown',
|
|
10
|
+
author='Open Source Community',
|
|
11
|
+
author_email='support@flaskutils.org',
|
|
12
|
+
url='https://github.com/flask-core-utils/scaffold',
|
|
13
|
+
classifiers=[
|
|
14
|
+
'Programming Language :: Python :: 3',
|
|
15
|
+
'Framework :: Flask',
|
|
16
|
+
'Operating System :: OS Independent',
|
|
17
|
+
],
|
|
18
|
+
python_requires='>=3.6',
|
|
19
|
+
)
|