- Implemented admin page for manual articles with fields for title, module, difficulty, tags, summary, content, steps, and relations. - Added preview functionality for markdown content. - Created list view for recent manuals with edit and view options. - Developed detail view for individual manuals displaying content, steps, and related guides. - Established database schema for manual articles, steps, and relations with appropriate indexing. - Seeded initial manual articles and steps for core functionalities. - Normalized newline characters in existing manual content. - Added additional manuals and steps for enhanced user guidance.
175 lines
5.2 KiB
Python
175 lines
5.2 KiB
Python
import logging
|
|
import html
|
|
import re
|
|
from typing import Any, List, Optional
|
|
|
|
from fastapi import APIRouter, Query, Request
|
|
from fastapi.responses import HTMLResponse
|
|
from fastapi.templating import Jinja2Templates
|
|
|
|
from app.core.database import execute_query, execute_query_single
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter()
|
|
templates = Jinja2Templates(directory="app")
|
|
|
|
|
|
def _normalize_tag_list(value: Any) -> List[str]:
|
|
if isinstance(value, list):
|
|
return [str(v).strip() for v in value if str(v).strip()]
|
|
return []
|
|
|
|
|
|
def _normalize_manual_text(value: Any) -> str:
|
|
text = str(value or "")
|
|
text = html.unescape(text)
|
|
text = re.sub(r"(?i)<br\s*/?>", "\n", text)
|
|
text = text.replace("\\n", "\n")
|
|
return text
|
|
|
|
|
|
@router.get("/manual", response_class=HTMLResponse)
|
|
async def manual_index(
|
|
request: Request,
|
|
module: Optional[str] = Query(default=None),
|
|
difficulty: Optional[str] = Query(default=None),
|
|
tag: Optional[str] = Query(default=None),
|
|
search: Optional[str] = Query(default=None),
|
|
):
|
|
where_parts = ["deleted_at IS NULL"]
|
|
params: List[Any] = []
|
|
|
|
if module:
|
|
where_parts.append("LOWER(module) = LOWER(%s)")
|
|
params.append(module.strip())
|
|
|
|
if difficulty in {"beginner", "advanced"}:
|
|
where_parts.append("difficulty = %s")
|
|
params.append(difficulty)
|
|
|
|
if search:
|
|
needle = f"%{search.strip()}%"
|
|
where_parts.append("(title ILIKE %s OR summary ILIKE %s OR content ILIKE %s)")
|
|
params.extend([needle, needle, needle])
|
|
|
|
if tag:
|
|
where_parts.append(
|
|
"EXISTS (SELECT 1 FROM jsonb_array_elements_text(COALESCE(tags, '[]'::jsonb)) t(tag) WHERE LOWER(t.tag) = LOWER(%s))"
|
|
)
|
|
params.append(tag.strip())
|
|
|
|
rows = execute_query(
|
|
f"""
|
|
SELECT id, title, slug, summary, module, difficulty, tags, use_count, updated_at
|
|
FROM manual_articles
|
|
WHERE {' AND '.join(where_parts)}
|
|
ORDER BY use_count DESC, updated_at DESC
|
|
LIMIT 300
|
|
""",
|
|
tuple(params),
|
|
) or []
|
|
|
|
modules = execute_query(
|
|
"""
|
|
SELECT DISTINCT module
|
|
FROM manual_articles
|
|
WHERE deleted_at IS NULL
|
|
ORDER BY module ASC
|
|
"""
|
|
) or []
|
|
|
|
all_tags: List[str] = []
|
|
for row in rows:
|
|
all_tags.extend(_normalize_tag_list(row.get("tags")))
|
|
unique_tags = sorted(list(set(all_tags)))
|
|
|
|
return templates.TemplateResponse(
|
|
"modules/manual/templates/list.html",
|
|
{
|
|
"request": request,
|
|
"articles": rows,
|
|
"available_modules": modules,
|
|
"available_tags": unique_tags,
|
|
"filters": {
|
|
"module": module or "",
|
|
"difficulty": difficulty or "",
|
|
"tag": tag or "",
|
|
"search": search or "",
|
|
},
|
|
},
|
|
)
|
|
|
|
|
|
@router.get("/manual/admin", response_class=HTMLResponse)
|
|
async def manual_admin(request: Request):
|
|
articles = execute_query(
|
|
"""
|
|
SELECT id, title, slug, module, difficulty, use_count, updated_at
|
|
FROM manual_articles
|
|
WHERE deleted_at IS NULL
|
|
ORDER BY updated_at DESC
|
|
LIMIT 200
|
|
"""
|
|
) or []
|
|
return templates.TemplateResponse(
|
|
"modules/manual/templates/admin.html",
|
|
{"request": request, "articles": articles},
|
|
)
|
|
|
|
|
|
@router.get("/manual/{slug}", response_class=HTMLResponse)
|
|
async def manual_detail(request: Request, slug: str):
|
|
article = execute_query_single(
|
|
"""
|
|
SELECT id, title, slug, content, summary, module, tags, difficulty, use_count, created_at, updated_at
|
|
FROM manual_articles
|
|
WHERE slug = %s AND deleted_at IS NULL
|
|
""",
|
|
(slug,),
|
|
)
|
|
|
|
if not article:
|
|
return templates.TemplateResponse(
|
|
"modules/manual/templates/detail.html",
|
|
{"request": request, "article": None, "steps": [], "related": []},
|
|
status_code=404,
|
|
)
|
|
|
|
execute_query(
|
|
"UPDATE manual_articles SET use_count = COALESCE(use_count, 0) + 1 WHERE id = %s",
|
|
(article["id"],),
|
|
)
|
|
article["use_count"] = int(article.get("use_count") or 0) + 1
|
|
article["content_normalized"] = _normalize_manual_text(article.get("content"))
|
|
article["summary_normalized"] = _normalize_manual_text(article.get("summary"))
|
|
|
|
steps = execute_query(
|
|
"""
|
|
SELECT step_number, title, content, image_url, video_url
|
|
FROM manual_steps
|
|
WHERE manual_id = %s
|
|
ORDER BY step_number ASC
|
|
""",
|
|
(article["id"],),
|
|
) or []
|
|
for step in steps:
|
|
step["content_normalized"] = _normalize_manual_text(step.get("content"))
|
|
|
|
related = execute_query(
|
|
"""
|
|
SELECT rm.slug, rm.title, rm.summary, rm.module
|
|
FROM manual_relations mr
|
|
JOIN manual_articles rm ON rm.id = mr.related_manual_id
|
|
WHERE mr.manual_id = %s
|
|
AND rm.deleted_at IS NULL
|
|
ORDER BY rm.use_count DESC, rm.updated_at DESC
|
|
LIMIT 12
|
|
""",
|
|
(article["id"],),
|
|
) or []
|
|
|
|
return templates.TemplateResponse(
|
|
"modules/manual/templates/detail.html",
|
|
{"request": request, "article": article, "steps": steps, "related": related},
|
|
)
|