Simplify category settings UI
This commit is contained in:
-3
@@ -1,6 +1,3 @@
|
|||||||
ALTER TABLE content_categories
|
|
||||||
ADD COLUMN IF NOT EXISTS description TEXT NOT NULL DEFAULT '';
|
|
||||||
|
|
||||||
INSERT INTO app_settings(key, value_json, value_type, title, description, category)
|
INSERT INTO app_settings(key, value_json, value_type, title, description, category)
|
||||||
VALUES (
|
VALUES (
|
||||||
'publication_common_tags',
|
'publication_common_tags',
|
||||||
+17
-50
@@ -1325,21 +1325,13 @@ async def writer_category_payload() -> list[dict[str, Any]]:
|
|||||||
pool = await get_pool()
|
pool = await get_pool()
|
||||||
rows = await pool.fetch(
|
rows = await pool.fetch(
|
||||||
"""
|
"""
|
||||||
SELECT sort_order AS id, name, tag, COALESCE(description, '') AS description
|
SELECT sort_order AS id, name, tag
|
||||||
FROM content_categories
|
FROM content_categories
|
||||||
WHERE is_active=TRUE
|
WHERE is_active=TRUE
|
||||||
ORDER BY sort_order, name
|
ORDER BY sort_order, name
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
categories = [
|
categories = [{"id": int(row["id"]), "name": str(row["name"]), "tag": str(row["tag"])} for row in rows]
|
||||||
{
|
|
||||||
"id": int(row["id"]),
|
|
||||||
"name": str(row["name"]),
|
|
||||||
"tag": str(row["tag"]),
|
|
||||||
"description": str(row["description"] or ""),
|
|
||||||
}
|
|
||||||
for row in rows
|
|
||||||
]
|
|
||||||
if categories:
|
if categories:
|
||||||
return categories
|
return categories
|
||||||
return [
|
return [
|
||||||
@@ -1352,8 +1344,7 @@ async def category_rows() -> list[dict[str, Any]]:
|
|||||||
pool = await get_pool()
|
pool = await get_pool()
|
||||||
rows = await pool.fetch(
|
rows = await pool.fetch(
|
||||||
"""
|
"""
|
||||||
SELECT id, name, tag, COALESCE(description, '') AS description,
|
SELECT id, name, tag, site_name, site_slug, site_enabled,
|
||||||
site_name, site_slug, site_enabled,
|
|
||||||
is_active, sort_order, created_at, updated_at
|
is_active, sort_order, created_at, updated_at
|
||||||
FROM content_categories
|
FROM content_categories
|
||||||
ORDER BY is_active DESC, sort_order, name
|
ORDER BY is_active DESC, sort_order, name
|
||||||
@@ -3639,10 +3630,6 @@ async def category_create(
|
|||||||
csrf_token: str = Form(...),
|
csrf_token: str = Form(...),
|
||||||
name: str = Form(...),
|
name: str = Form(...),
|
||||||
tag: str = Form(""),
|
tag: str = Form(""),
|
||||||
description: str = Form(""),
|
|
||||||
site_name: str = Form(...),
|
|
||||||
site_slug: str = Form(...),
|
|
||||||
site_enabled: str | None = Form(None),
|
|
||||||
):
|
):
|
||||||
user = await get_current_user(request)
|
user = await get_current_user(request)
|
||||||
if not user:
|
if not user:
|
||||||
@@ -3652,35 +3639,28 @@ async def category_create(
|
|||||||
if not name:
|
if not name:
|
||||||
return redirect("/workers")
|
return redirect("/workers")
|
||||||
tag = normalize_hash_tag(tag or name, "category")
|
tag = normalize_hash_tag(tag or name, "category")
|
||||||
description = description.strip()
|
site_name = name
|
||||||
site_name = site_name.strip()
|
|
||||||
site_slug = re.sub(r"[^a-z0-9-]+", "-", site_slug.strip().lower().replace("_", "-")).strip("-")
|
|
||||||
if not site_name or not site_slug:
|
|
||||||
return redirect("/workers")
|
|
||||||
pool = await get_pool()
|
pool = await get_pool()
|
||||||
sort_order = int(await pool.fetchval("SELECT COALESCE(MAX(sort_order), 0) + 1 FROM content_categories") or 1)
|
sort_order = int(await pool.fetchval("SELECT COALESCE(MAX(sort_order), 0) + 1 FROM content_categories") or 1)
|
||||||
|
site_slug = re.sub(r"[^a-z0-9-]+", "-", tag.strip().lower().replace("_", "-")).strip("-") or f"category-{sort_order}"
|
||||||
await pool.execute(
|
await pool.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO content_categories(name, tag, sort_order, description, site_name, site_slug, site_enabled)
|
INSERT INTO content_categories(name, tag, sort_order, site_name, site_slug, site_enabled)
|
||||||
VALUES($1, $2, $3, $4, $5, $6, $7)
|
VALUES($1, $2, $3, $4, $5, FALSE)
|
||||||
ON CONFLICT (name) DO UPDATE
|
ON CONFLICT (name) DO UPDATE
|
||||||
SET tag=$2,
|
SET tag=$2,
|
||||||
description=$4,
|
site_name=$4,
|
||||||
site_name=$5,
|
site_slug=$5,
|
||||||
site_slug=$6,
|
|
||||||
site_enabled=$7,
|
|
||||||
is_active=TRUE,
|
is_active=TRUE,
|
||||||
updated_at=NOW()
|
updated_at=NOW()
|
||||||
""",
|
""",
|
||||||
name,
|
name,
|
||||||
tag,
|
tag,
|
||||||
sort_order,
|
sort_order,
|
||||||
description,
|
|
||||||
site_name,
|
site_name,
|
||||||
site_slug,
|
site_slug,
|
||||||
site_enabled is not None,
|
|
||||||
)
|
)
|
||||||
await audit(user["id"], "category.create", "content_category", None, {"name": name, "tag": tag, "description": description, "site_name": site_name, "site_slug": site_slug, "site_enabled": site_enabled is not None})
|
await audit(user["id"], "category.create", "content_category", None, {"name": name, "tag": tag})
|
||||||
return redirect("/workers")
|
return redirect("/workers")
|
||||||
|
|
||||||
|
|
||||||
@@ -3691,10 +3671,6 @@ async def category_update(
|
|||||||
csrf_token: str = Form(...),
|
csrf_token: str = Form(...),
|
||||||
name: str = Form(...),
|
name: str = Form(...),
|
||||||
tag: str = Form(""),
|
tag: str = Form(""),
|
||||||
description: str = Form(""),
|
|
||||||
site_name: str = Form(...),
|
|
||||||
site_slug: str = Form(...),
|
|
||||||
site_enabled: str | None = Form(None),
|
|
||||||
):
|
):
|
||||||
user = await get_current_user(request)
|
user = await get_current_user(request)
|
||||||
if not user:
|
if not user:
|
||||||
@@ -3704,33 +3680,26 @@ async def category_update(
|
|||||||
if not name:
|
if not name:
|
||||||
return redirect("/workers")
|
return redirect("/workers")
|
||||||
tag = normalize_hash_tag(tag or name, "category")
|
tag = normalize_hash_tag(tag or name, "category")
|
||||||
description = description.strip()
|
site_name = name
|
||||||
site_name = site_name.strip()
|
site_slug = re.sub(r"[^a-z0-9-]+", "-", tag.strip().lower().replace("_", "-")).strip("-") or f"category-{category_id}"
|
||||||
site_slug = re.sub(r"[^a-z0-9-]+", "-", site_slug.strip().lower().replace("_", "-")).strip("-")
|
|
||||||
if not site_name or not site_slug:
|
|
||||||
return redirect("/workers")
|
|
||||||
pool = await get_pool()
|
pool = await get_pool()
|
||||||
await pool.execute(
|
await pool.execute(
|
||||||
"""
|
"""
|
||||||
UPDATE content_categories
|
UPDATE content_categories
|
||||||
SET name=$2,
|
SET name=$2,
|
||||||
tag=$3,
|
tag=$3,
|
||||||
description=$4,
|
site_name=$4,
|
||||||
site_name=$5,
|
site_slug=$5,
|
||||||
site_slug=$6,
|
|
||||||
site_enabled=$7,
|
|
||||||
updated_at=NOW()
|
updated_at=NOW()
|
||||||
WHERE id=$1
|
WHERE id=$1
|
||||||
""",
|
""",
|
||||||
category_id,
|
category_id,
|
||||||
name,
|
name,
|
||||||
tag,
|
tag,
|
||||||
description,
|
|
||||||
site_name,
|
site_name,
|
||||||
site_slug,
|
site_slug,
|
||||||
site_enabled is not None,
|
|
||||||
)
|
)
|
||||||
await audit(user["id"], "category.update", "content_category", category_id, {"name": name, "tag": tag, "description": description, "site_name": site_name, "site_slug": site_slug, "site_enabled": site_enabled is not None})
|
await audit(user["id"], "category.update", "content_category", category_id, {"name": name, "tag": tag})
|
||||||
return redirect("/workers")
|
return redirect("/workers")
|
||||||
|
|
||||||
|
|
||||||
@@ -3770,15 +3739,13 @@ async def category_delete(request: Request, category_id: int, csrf_token: str =
|
|||||||
pool = await get_pool()
|
pool = await get_pool()
|
||||||
row = await pool.fetchrow(
|
row = await pool.fetchrow(
|
||||||
"""
|
"""
|
||||||
UPDATE content_categories
|
DELETE FROM content_categories
|
||||||
SET is_active=FALSE,
|
|
||||||
updated_at=NOW()
|
|
||||||
WHERE id=$1
|
WHERE id=$1
|
||||||
RETURNING name
|
RETURNING name
|
||||||
""",
|
""",
|
||||||
category_id,
|
category_id,
|
||||||
)
|
)
|
||||||
await audit(user["id"], "category.archive", "content_category", category_id, {"name": row["name"] if row else None})
|
await audit(user["id"], "category.delete", "content_category", category_id, {"name": row["name"] if row else None})
|
||||||
return redirect("/workers")
|
return redirect("/workers")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,7 @@
|
|||||||
</summary>
|
</summary>
|
||||||
<div class="p-4 flex flex-col gap-6 bg-app-bg/30">
|
<div class="p-4 flex flex-col gap-6 bg-app-bg/30">
|
||||||
<div class="text-sm text-app-textMuted">
|
<div class="text-sm text-app-textMuted">
|
||||||
Название и описание помогают AI выбрать категорию, тэг используется в соцсетях.
|
Название помогает AI выбрать категорию, тэг используется в соцсетях.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="post" action="/categories/create" class="flex flex-col gap-4 bg-app-surface p-4 rounded-xl border border-app-border">
|
<form method="post" action="/categories/create" class="flex flex-col gap-4 bg-app-surface p-4 rounded-xl border border-app-border">
|
||||||
@@ -88,24 +88,8 @@
|
|||||||
<label class="block text-[10px] uppercase font-bold text-app-textMuted mb-1">Тэг (для хэштегов)</label>
|
<label class="block text-[10px] uppercase font-bold text-app-textMuted mb-1">Тэг (для хэштегов)</label>
|
||||||
<input name="tag" class="input w-full">
|
<input name="tag" class="input w-full">
|
||||||
</div>
|
</div>
|
||||||
<div class="md:col-span-2">
|
|
||||||
<label class="block text-[10px] uppercase font-bold text-app-textMuted mb-1">Описание (для AI)</label>
|
|
||||||
<textarea name="description" class="textarea w-full min-h-20"></textarea>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-[10px] uppercase font-bold text-app-textMuted mb-1">На сайте</label>
|
|
||||||
<input name="site_name" class="input w-full" required>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-[10px] uppercase font-bold text-app-textMuted mb-1">URL-slug</label>
|
|
||||||
<input name="site_slug" class="input w-full" pattern="[a-z0-9-]+" required>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between mt-2">
|
<div class="flex items-center justify-between mt-2">
|
||||||
<label class="cursor-pointer flex items-center gap-2 group">
|
|
||||||
<input type="checkbox" name="site_enabled" class="checkbox" checked>
|
|
||||||
<span class="text-sm font-medium text-app-textMain group-hover:text-white transition-colors">Публиковать</span>
|
|
||||||
</label>
|
|
||||||
<button class="btn btn-primary btn-sm" type="submit">Добавить</button>
|
<button class="btn btn-primary btn-sm" type="submit">Добавить</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -115,7 +99,6 @@
|
|||||||
<thead class="bg-app-bg border-b border-app-border text-app-textMuted uppercase tracking-wider text-[10px]">
|
<thead class="bg-app-bg border-b border-app-border text-app-textMuted uppercase tracking-wider text-[10px]">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-3 py-2">Название / Тэг</th>
|
<th class="px-3 py-2">Название / Тэг</th>
|
||||||
<th class="px-3 py-2">Сайт / Slug</th>
|
|
||||||
<th class="px-3 py-2 text-center">ID</th>
|
<th class="px-3 py-2 text-center">ID</th>
|
||||||
<th class="px-3 py-2">Статус</th>
|
<th class="px-3 py-2">Статус</th>
|
||||||
<th class="px-3 py-2 w-16"></th>
|
<th class="px-3 py-2 w-16"></th>
|
||||||
@@ -131,23 +114,12 @@
|
|||||||
<div class="flex flex-col gap-1.5">
|
<div class="flex flex-col gap-1.5">
|
||||||
<input form="category-update-{{ c.id }}" name="name" value="{{ c.name }}" class="input input-sm w-full bg-app-bg text-white h-7">
|
<input form="category-update-{{ c.id }}" name="name" value="{{ c.name }}" class="input input-sm w-full bg-app-bg text-white h-7">
|
||||||
<input form="category-update-{{ c.id }}" name="tag" value="{{ c.tag }}" class="input input-sm w-full bg-app-bg text-blue-400 font-mono h-7">
|
<input form="category-update-{{ c.id }}" name="tag" value="{{ c.tag }}" class="input input-sm w-full bg-app-bg text-blue-400 font-mono h-7">
|
||||||
<textarea form="category-update-{{ c.id }}" name="description" class="textarea textarea-sm w-full bg-app-bg text-app-textMain min-h-16">{{ c.description }}</textarea>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="px-3 py-2">
|
|
||||||
<div class="flex flex-col gap-1.5">
|
|
||||||
<input form="category-update-{{ c.id }}" name="site_name" value="{{ c.site_name }}" class="input input-sm w-full bg-app-bg text-white h-7" required>
|
|
||||||
<input form="category-update-{{ c.id }}" name="site_slug" value="{{ c.site_slug }}" pattern="[a-z0-9-]+" class="input input-sm w-full bg-app-bg text-app-textMuted font-mono h-7" required>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-3 py-2 text-center font-mono text-app-textMuted text-[10px]">{{ c.sort_order }}</td>
|
<td class="px-3 py-2 text-center font-mono text-app-textMuted text-[10px]">{{ c.sort_order }}</td>
|
||||||
<td class="px-3 py-2">
|
<td class="px-3 py-2">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<span class="badge {% if c.is_active %}badge-success{% else %}badge-neutral{% endif %} w-full py-0.5 text-[10px]">{% if c.is_active %}Активна{% else %}Выкл{% endif %}</span>
|
<span class="badge {% if c.is_active %}badge-success{% else %}badge-neutral{% endif %} w-full py-0.5 text-[10px]">{% if c.is_active %}Активна{% else %}Выкл{% endif %}</span>
|
||||||
<label class="cursor-pointer flex items-center gap-2 bg-app-bg px-2 py-1 rounded border border-app-border">
|
|
||||||
<input form="category-update-{{ c.id }}" type="checkbox" name="site_enabled" class="checkbox w-3.5 h-3.5 rounded-sm" {% if c.site_enabled %}checked{% endif %}>
|
|
||||||
<span class="text-[10px] font-medium text-app-textMuted">Сайт</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-3 py-2">
|
<td class="px-3 py-2">
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ OUTPUT SCHEMA (return array matching input order):
|
|||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
- Input is a JSON object with key "posts" containing accepted posts.
|
- Input is a JSON object with key "posts" containing accepted posts.
|
||||||
- Input has key "categories" with objects: id, name, tag, description.
|
- Input has key "categories" with objects: id, name, tag.
|
||||||
- Each post includes producer_name and producer_tag. producer_name can be a manufacturer, shop, or publishing source.
|
- Each post includes producer_name and producer_tag. producer_name can be a manufacturer, shop, or publishing source.
|
||||||
- The "text" field for every rewrite must explicitly mention producer_name at least once. Do not satisfy this only through producer_tag, hashtags, notes, or metadata.
|
- The "text" field for every rewrite must explicitly mention producer_name at least once. Do not satisfy this only through producer_tag, hashtags, notes, or metadata.
|
||||||
- Pick exactly one category from the categories list in input and return its numeric id as category_id.
|
- Pick exactly one category from the categories list in input and return its numeric id as category_id.
|
||||||
@@ -208,7 +208,7 @@ async def load_writer_categories(pool) -> list[dict[str, Any]]:
|
|||||||
try:
|
try:
|
||||||
rows = await pool.fetch(
|
rows = await pool.fetch(
|
||||||
"""
|
"""
|
||||||
SELECT sort_order AS id, name, tag, COALESCE(description, '') AS description
|
SELECT sort_order AS id, name, tag
|
||||||
FROM content_categories
|
FROM content_categories
|
||||||
WHERE is_active=TRUE
|
WHERE is_active=TRUE
|
||||||
ORDER BY sort_order, name
|
ORDER BY sort_order, name
|
||||||
@@ -216,15 +216,7 @@ async def load_writer_categories(pool) -> list[dict[str, Any]]:
|
|||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
rows = []
|
rows = []
|
||||||
categories = [
|
categories = [{"id": int(row["id"]), "name": str(row["name"]), "tag": str(row["tag"])} for row in rows]
|
||||||
{
|
|
||||||
"id": int(row["id"]),
|
|
||||||
"name": str(row["name"]),
|
|
||||||
"tag": str(row["tag"]),
|
|
||||||
"description": str(row["description"] or ""),
|
|
||||||
}
|
|
||||||
for row in rows
|
|
||||||
]
|
|
||||||
if categories:
|
if categories:
|
||||||
return categories
|
return categories
|
||||||
legacy = parse_categories(await fetch_setting("ai_writer_categories", []))
|
legacy = parse_categories(await fetch_setting("ai_writer_categories", []))
|
||||||
|
|||||||
Reference in New Issue
Block a user