From 13b7a41a021ada9f0f8f6a9a4e49d0eceee32195 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Mon, 20 Jul 2026 10:46:20 +0400 Subject: [PATCH] =?UTF-8?q?init:=20Flask=20CRUD=20app=20v1.0.0=20=E2=80=94?= =?UTF-8?q?=20same=20design=20as=20Lucee,=20PG=20params,=20CREATE=20IF=20N?= =?UTF-8?q?OT=20EXISTS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 12 +++++ app.py | 116 +++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 3 ++ static/favicon.svg | 55 ++++++++++++++++++++ static/logo.svg | 25 ++++++++++ static/style.css | 111 +++++++++++++++++++++++++++++++++++++++++ templates/index.html | 108 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 430 insertions(+) create mode 100644 .gitignore create mode 100644 app.py create mode 100644 requirements.txt create mode 100644 static/favicon.svg create mode 100755 static/logo.svg create mode 100644 static/style.css create mode 100644 templates/index.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2530054 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +__pycache__/ +*.pyc +*.pyo +.venv/ +venv/ +.env +*.egg-info/ +dist/ +*.log +.DS_Store +.idea/ +.vscode/ diff --git a/app.py b/app.py new file mode 100644 index 0000000..3ff301b --- /dev/null +++ b/app.py @@ -0,0 +1,116 @@ +""" +app.py — Flask CRUD-интерфейс для PostgreSQL. + +Переменные окружения (из json_env): + - PGHOST, PGPORT, PGDATABASE, PGUSER, PGPASSWORD — подключение к БД + - TABLE_NAME — имя таблицы (по умолчанию crud_items) + +При старте: CREATE TABLE IF NOT EXISTS (БЕЗ DROP). +""" + +import os +import psycopg2 +import psycopg2.extras +from flask import Flask, render_template, request, redirect, url_for, g + +VERSION = "1.0.0" +TABLE_NAME = os.environ.get("TABLE_NAME", "crud_items") + +app = Flask(__name__) + + +def get_db(): + """Ленивое подключение к PostgreSQL. Один коннект на запрос.""" + if "db" not in g: + g.db = psycopg2.connect( + host=os.environ.get("PGHOST", "127.0.0.1"), + port=os.environ.get("PGPORT", "5432"), + dbname=os.environ.get("PGDATABASE", "postgres"), + user=os.environ.get("PGUSER", "postgres"), + password=os.environ.get("PGPASSWORD", ""), + ) + g.db.autocommit = True + return g.db + + +def init_db(): + """CREATE TABLE IF NOT EXISTS — никогда не дропает существующую таблицу.""" + db = get_db() + cur = db.cursor() + cur.execute( + f""" + CREATE TABLE IF NOT EXISTS {TABLE_NAME} ( + id SERIAL PRIMARY KEY, + value TEXT NOT NULL DEFAULT '' + ) + """ + ) + cur.close() + + +@app.teardown_appcontext +def close_db(exc): + db = g.pop("db", None) + if db is not None: + db.close() + + +@app.route("/", methods=["GET", "POST"]) +def index(): + db = get_db() + cur = db.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + + # --- Обработка POST (create / update / delete) — POST-redirect-GET --- + if request.method == "POST": + action = request.form.get("action", "") + + if action == "create": + cur.execute( + f"INSERT INTO {TABLE_NAME} (value) VALUES (%s)", + (request.form.get("value", ""),), + ) + + elif action == "update": + cur.execute( + f"UPDATE {TABLE_NAME} SET value = %s WHERE id = %s", + (request.form.get("value", ""), int(request.form.get("id", 0))), + ) + + elif action == "delete": + cur.execute( + f"DELETE FROM {TABLE_NAME} WHERE id = %s", + (int(request.form.get("id", 0)),), + ) + + cur.close() + return redirect(url_for("index")) + + # --- GET: список + форма редактирования --- + edit_row = {} + edit_id = request.args.get("edit") + if edit_id: + cur.execute( + f"SELECT id, value FROM {TABLE_NAME} WHERE id = %s", + (int(edit_id),), + ) + row = cur.fetchone() + if row: + edit_row = dict(row) + + # Список всех записей + cur.execute(f"SELECT id, value FROM {TABLE_NAME} ORDER BY id") + rows = cur.fetchall() + cur.close() + + return render_template( + "index.html", + table_name=TABLE_NAME, + rows=rows, + edit_row=edit_row, + version=VERSION, + ) + + +if __name__ == "__main__": + init_db() + app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 5000)), debug=False) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8f25183 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +Flask>=3.0 +psycopg2-binary>=2.9 +gunicorn>=21.2 diff --git a/static/favicon.svg b/static/favicon.svg new file mode 100644 index 0000000..11a9ece --- /dev/null +++ b/static/favicon.svg @@ -0,0 +1,55 @@ + + diff --git a/static/logo.svg b/static/logo.svg new file mode 100755 index 0000000..d76eef6 --- /dev/null +++ b/static/logo.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..8a5eb67 --- /dev/null +++ b/static/style.css @@ -0,0 +1,111 @@ +:root { + --brand-primary: #2563eb; + --brand-primary-dark: #1d4ed8; + --brand-gray: #d1d5db; + --brand-grey-light: #f3f4f6; + --brand-destructive: #ef4444; + --brand-success: #22c55e; + --text-primary: #1a1a1a; + --text-muted: #6b7280; + --bg-card: #ffffff; + --radius: 12px; +} +* { box-sizing: border-box; margin: 0; padding: 0; } +body { + font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; + font-size: 14px; + color: var(--text-primary); + background: var(--brand-grey-light); + min-height: 100vh; + padding: 40px 16px; +} +.container { max-width: 720px; margin: 0 auto; } + +/* Header */ +.header { + display: flex; align-items: center; gap: 12px; + margin-bottom: 24px; +} +.header img { height: 36px; } +.header h1 { font-size: 20px; font-weight: 600; color: #001C34; } + +/* Card */ +.card { + background: var(--bg-card); + border-radius: var(--radius); + border: 1px solid var(--brand-gray); + box-shadow: 0 1px 2px rgba(0,0,0,0.05); + overflow: hidden; + margin-bottom: 20px; +} +.card-header { + background: var(--brand-grey-light); + padding: 12px 16px; + font-weight: 600; + font-size: 16px; +} +.card-body { padding: 16px; } + +/* Table */ +table { width: 100%; border-collapse: collapse; } +th { + background: var(--brand-grey-light); + text-transform: uppercase; + font-size: 12px; + font-weight: 600; + color: var(--text-muted); + padding: 8px 12px; + text-align: left; + border-right: 1px solid var(--brand-gray); +} +th:last-child { border-right: none; text-align: center; width: 60px; } +td { + padding: 8px 12px; + border-bottom: 1px solid var(--brand-gray); + border-right: 1px solid var(--brand-gray); +} +td:last-child { border-right: none; text-align: center; width: 60px; } +tr:hover { background: rgba(243,244,246,0.5); } +tr:last-child td { border-bottom: none; } + +/* Buttons */ +.btn { + display: inline-flex; align-items: center; gap: 4px; + height: 32px; padding: 0 12px; + border: 1px solid var(--brand-gray); + border-radius: 6px; + background: var(--bg-card); + font-size: 14px; font-family: inherit; + cursor: pointer; white-space: nowrap; + text-decoration: none; + color: var(--text-primary); +} +.btn:hover { background: var(--brand-grey-light); } +.btn-primary { + background: var(--brand-primary); color: #fff; border-color: var(--brand-primary); +} +.btn-primary:hover { background: var(--brand-primary-dark); } +.btn-sm { height: 28px; padding: 0 8px; font-size: 13px; } + +/* Form */ +.form-grid { + display: grid; + grid-template-columns: 1fr; + gap: 12px; +} +.form-row { + display: flex; gap: 8px; align-items: flex-end; +} +.form-row input { flex: 1; } +input[type="text"] { + height: 36px; padding: 0 12px; + border: 1px solid var(--brand-gray); + border-radius: 6px; font-size: 14px; font-family: inherit; +} +input[type="text"]:focus { outline: none; border-color: var(--brand-primary); box-shadow: 0 0 0 2px rgba(37,99,235,0.15); } + +/* Footer */ +.footer { text-align: center; padding: 16px; color: #9ca3af; font-size: 12px; } + +/* Empty state */ +.empty-cell { color: var(--text-muted); padding: 24px; text-align: center; } diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..a579c95 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,108 @@ + + + + + + CRUD Flask Example — {{ table_name }} + + + + +
+ +
+ Nubes +

CRUD Flask Example

+
+ +
+
Записи
+
+ + + + + + + + + + + {% for row in rows %} + + + + + + + {% endfor %} + +
IDVALUE
{{ row.id }}{{ row.value }} + ✏️ + +
+ + + +
+
+ {% if not rows %} +
Нет записей. Создайте первую.
+ {% endif %} +
+
+ +
+ {% if edit_row %} +
Редактировать #{{ edit_row.id }}
+
+
+ + +
+ + + Отмена +
+
+
+ {% else %} +
Добавить
+
+
+ +
+ + +
+
+
+ {% endif %} +
+ +
+ + + +