Admin: add title field, live preview, and delete; DB migration for title; show title in list; keep Admin button on public pages
This commit is contained in:
29
app.py
29
app.py
@@ -53,11 +53,19 @@ def create_app():
|
|||||||
CREATE TABLE IF NOT EXISTS pages (
|
CREATE TABLE IF NOT EXISTS pages (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
uuid TEXT NOT NULL UNIQUE,
|
uuid TEXT NOT NULL UNIQUE,
|
||||||
|
title TEXT NOT NULL DEFAULT '',
|
||||||
html TEXT NOT NULL,
|
html TEXT NOT NULL,
|
||||||
created_at TEXT NOT NULL
|
created_at TEXT NOT NULL
|
||||||
);
|
);
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
# Migration: add 'title' column for older DB versions
|
||||||
|
try:
|
||||||
|
cols = {row[1] for row in db.execute("PRAGMA table_info(pages)").fetchall()}
|
||||||
|
if "title" not in cols:
|
||||||
|
db.execute("ALTER TABLE pages ADD COLUMN title TEXT NOT NULL DEFAULT ''")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
@app.before_request
|
@app.before_request
|
||||||
@@ -120,29 +128,42 @@ def create_app():
|
|||||||
login_required()
|
login_required()
|
||||||
db = get_db()
|
db = get_db()
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
|
title = request.form.get("title", "").strip()
|
||||||
html = request.form.get("html", "").strip()
|
html = request.form.get("html", "").strip()
|
||||||
if not html:
|
if not html:
|
||||||
flash("HTML не может быть пустым.", "error")
|
flash("HTML не может быть пустым.", "error")
|
||||||
else:
|
else:
|
||||||
uid = uuid_lib.uuid4().hex
|
uid = uuid_lib.uuid4().hex
|
||||||
db.execute(
|
db.execute(
|
||||||
"INSERT INTO pages (uuid, html, created_at) VALUES (?, ?, ?)",
|
"INSERT INTO pages (uuid, title, html, created_at) VALUES (?, ?, ?, ?)",
|
||||||
(uid, html, datetime.utcnow().isoformat(timespec="seconds")),
|
(uid, title, html, datetime.utcnow().isoformat(timespec="seconds")),
|
||||||
)
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
flash("Страница опубликована.", "success")
|
flash("Страница опубликована.", "success")
|
||||||
return redirect(url_for("admin"))
|
return redirect(url_for("admin"))
|
||||||
|
|
||||||
pages = db.execute(
|
pages = db.execute(
|
||||||
"SELECT id, uuid, created_at FROM pages ORDER BY id DESC"
|
"SELECT id, uuid, title, created_at FROM pages ORDER BY id DESC"
|
||||||
).fetchall()
|
).fetchall()
|
||||||
base_url = request.host_url.rstrip("/")
|
base_url = request.host_url.rstrip("/")
|
||||||
return render_template("admin.html", pages=pages, base_url=base_url)
|
return render_template("admin.html", pages=pages, base_url=base_url)
|
||||||
|
|
||||||
|
@app.route("/admin/delete/<int:pid>", methods=["POST"])
|
||||||
|
def admin_delete(pid: int):
|
||||||
|
login_required()
|
||||||
|
db = get_db()
|
||||||
|
cur = db.execute("DELETE FROM pages WHERE id = ?", (pid,))
|
||||||
|
db.commit()
|
||||||
|
if cur.rowcount:
|
||||||
|
flash("Страница удалена.", "success")
|
||||||
|
else:
|
||||||
|
flash("Страница не найдена.", "error")
|
||||||
|
return redirect(url_for("admin"))
|
||||||
|
|
||||||
@app.route("/p/<string:uid>")
|
@app.route("/p/<string:uid>")
|
||||||
def view_page(uid: str):
|
def view_page(uid: str):
|
||||||
db = get_db()
|
db = get_db()
|
||||||
row = db.execute("SELECT html FROM pages WHERE uuid = ?", (uid,)).fetchone()
|
row = db.execute("SELECT html, title FROM pages WHERE uuid = ?", (uid,)).fetchone()
|
||||||
if row is None:
|
if row is None:
|
||||||
abort(404)
|
abort(404)
|
||||||
# Show a floating admin button for authenticated users, otherwise serve raw HTML
|
# Show a floating admin button for authenticated users, otherwise serve raw HTML
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
<h1>Публикация HTML-страниц</h1>
|
<h1>Публикация HTML-страниц</h1>
|
||||||
|
|
||||||
<form method="post">
|
<form method="post">
|
||||||
|
<div class="row">
|
||||||
|
<label for="title">Название страницы</label>
|
||||||
|
<input type="text" id="title" name="title" placeholder="Например: Презентация" />
|
||||||
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label for="html">HTML-код</label>
|
<label for="html">HTML-код</label>
|
||||||
<textarea id="html" name="html" placeholder="<h1>Заголовок</h1>\n<p>Мой контент...</p>" required></textarea>
|
<textarea id="html" name="html" placeholder="<h1>Заголовок</h1>\n<p>Мой контент...</p>" required></textarea>
|
||||||
@@ -11,24 +15,37 @@
|
|||||||
<span class="muted">После публикации создаётся ссылка с UUID.</span>
|
<span class="muted">После публикации создаётся ссылка с UUID.</span>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<div class="row" style="margin-top: 1.25rem;">
|
||||||
|
<div class="muted" style="margin-bottom: .5rem;">Моментальный предпросмотр</div>
|
||||||
|
<iframe id="preview" style="width:100%; height:320px; border:1px solid rgba(255,255,255,.08); border-radius:12px; background:#fff;"></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h2 style="margin-top:2rem;">Опубликованные страницы</h2>
|
<h2 style="margin-top:2rem;">Опубликованные страницы</h2>
|
||||||
{% if pages %}
|
{% if pages %}
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th>ID</th>
|
||||||
|
<th>Название</th>
|
||||||
<th>UUID</th>
|
<th>UUID</th>
|
||||||
<th>Ссылка</th>
|
<th>Ссылка</th>
|
||||||
<th>Создано</th>
|
<th>Создано</th>
|
||||||
|
<th>Действия</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for p in pages %}
|
{% for p in pages %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ p.id }}</td>
|
<td>{{ p.id }}</td>
|
||||||
|
<td>{{ p.title or "" }}</td>
|
||||||
<td><code>{{ p.uuid }}</code></td>
|
<td><code>{{ p.uuid }}</code></td>
|
||||||
<td><a href="{{ base_url }}/p/{{ p.uuid }}" target="_blank">{{ base_url }}/p/{{ p.uuid }}</a></td>
|
<td><a href="{{ base_url }}/p/{{ p.uuid }}" target="_blank">{{ base_url }}/p/{{ p.uuid }}</a></td>
|
||||||
<td>{{ p.created_at }}</td>
|
<td>{{ p.created_at }}</td>
|
||||||
|
<td>
|
||||||
|
<form method="post" action="{{ url_for('admin_delete', pid=p.id) }}" onsubmit="return confirm('Удалить страницу #' + {{ p.id }} + '?');">
|
||||||
|
<button class="btn secondary" type="submit">Удалить</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -36,5 +53,24 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<p class="muted">Страниц пока нет.</p>
|
<p class="muted">Страниц пока нет.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<script>
|
||||||
|
(function(){
|
||||||
|
const htmlEl = document.getElementById('html');
|
||||||
|
const titleEl = document.getElementById('title');
|
||||||
|
const iframe = document.getElementById('preview');
|
||||||
|
function render(){
|
||||||
|
if(!iframe || !htmlEl) return;
|
||||||
|
const doc = iframe.contentDocument || iframe.contentWindow.document;
|
||||||
|
const t = (titleEl && titleEl.value) ? `<title>${titleEl.value}</title>` : '';
|
||||||
|
doc.open();
|
||||||
|
doc.write(`<!doctype html><html lang="ru"><head><meta charset="utf-8">${t}</head><body>${htmlEl.value}</body></html>`);
|
||||||
|
doc.close();
|
||||||
|
}
|
||||||
|
if (htmlEl){
|
||||||
|
htmlEl.addEventListener('input', render);
|
||||||
|
if (titleEl) titleEl.addEventListener('input', render);
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user