165 lines
5.3 KiB
Python
165 lines
5.3 KiB
Python
from fastapi import APIRouter, Depends, Request, HTTPException
|
|
from fastapi.responses import HTMLResponse
|
|
from pymysql.cursors import DictCursor
|
|
import json
|
|
from collections import Counter
|
|
from typing import List, Dict, Any, Optional
|
|
|
|
from deps import get_db, current_user
|
|
from services.config_loader import ConfigLoader
|
|
|
|
router = APIRouter()
|
|
|
|
@router.get("/crumbforest/pulse", response_class=HTMLResponse)
|
|
async def pulse_dashboard(req: Request, tag: Optional[str] = None, user = Depends(current_user)):
|
|
"""
|
|
Show the Pulse (Blog) dashboard with tag cloud.
|
|
"""
|
|
# 1. Auth & Config
|
|
if not user:
|
|
# Public access allowed? User said "Neuigkeiten...".
|
|
# But usually Crumbforest is internal.
|
|
# Let's assume login required for now to access specific group templates.
|
|
lang = req.query_params.get("lang", "de")
|
|
return req.app.state.render(req, "pages/login.html", error="Login required for Pulse")
|
|
# Actually Redirect is better
|
|
# return RedirectResponse(f"/{lang}/login", status_code=302)
|
|
|
|
user_group = user.get('user_group', 'demo')
|
|
config = ConfigLoader.load_config()
|
|
group_config = config.get('groups', {}).get(user_group, {})
|
|
lang = req.query_params.get("lang") or req.session.get("lang") or "de"
|
|
req.session["lang"] = lang
|
|
|
|
# 2. Fetch Posts
|
|
conn = get_db()
|
|
try:
|
|
with conn.cursor() as cur:
|
|
# Fetch all published posts
|
|
base_query = """
|
|
SELECT id, title, slug, excerpt, tags, author, created_at
|
|
FROM posts
|
|
WHERE is_published = 1
|
|
"""
|
|
cur.execute(base_query)
|
|
posts_raw = cur.fetchall()
|
|
finally:
|
|
conn.close()
|
|
|
|
# 3. Process Tags & Filter
|
|
posts = []
|
|
all_tags = []
|
|
|
|
for p in posts_raw:
|
|
# Parse JSON tags
|
|
if isinstance(p['tags'], str):
|
|
p['tags'] = json.loads(p['tags'])
|
|
elif p['tags'] is None:
|
|
p['tags'] = []
|
|
|
|
all_tags.extend(p['tags'])
|
|
|
|
# Filter (if tag selected)
|
|
if tag and tag not in p['tags']:
|
|
continue
|
|
|
|
posts.append(p)
|
|
|
|
# 4. Build Tag Cloud
|
|
# Count tags
|
|
tag_counts = Counter(all_tags)
|
|
# Convert to list of dicts for template: [{'name': 'RAG', 'count': 5, 'size': '...'}, ...]
|
|
# Simple sizing logic: 1..10
|
|
tag_cloud = []
|
|
if tag_counts:
|
|
max_count = max(tag_counts.values())
|
|
min_count = min(tag_counts.values())
|
|
delta = max_count - min_count or 1
|
|
|
|
for t_name, count in tag_counts.items():
|
|
# Linear scaling 1.0 to 2.0em
|
|
size = 0.8 + (1.2 * (count - min_count) / delta)
|
|
tag_cloud.append({
|
|
'name': t_name,
|
|
'count': count,
|
|
'size': f"{size:.1f}em",
|
|
'active': t_name == tag
|
|
})
|
|
|
|
# Sort cloud alphabetically
|
|
tag_cloud.sort(key=lambda x: x['name'])
|
|
|
|
return req.app.state.render(
|
|
req,
|
|
"crumbforest/blog.html",
|
|
posts=posts,
|
|
tag_cloud=tag_cloud,
|
|
current_tag=tag,
|
|
user_group=user_group,
|
|
group_config=group_config,
|
|
lang=lang
|
|
)
|
|
|
|
|
|
@router.get("/crumbforest/pulse/{slug}", response_class=HTMLResponse)
|
|
async def pulse_post(req: Request, slug: str, user = Depends(current_user)):
|
|
"""
|
|
Show a single blog post.
|
|
"""
|
|
if not user:
|
|
# Redirect
|
|
lang = req.query_params.get("lang", "de")
|
|
return req.app.state.render(req, "pages/login.html") # Simplified
|
|
|
|
user_group = user.get('user_group', 'demo')
|
|
config = ConfigLoader.load_config()
|
|
group_config = config.get('groups', {}).get(user_group, {})
|
|
lang = req.query_params.get("lang") or req.session.get("lang") or "de"
|
|
|
|
conn = get_db()
|
|
try:
|
|
with conn.cursor() as cur:
|
|
cur.execute(
|
|
"""
|
|
SELECT id, title, slug, excerpt, tags, author, body_md, created_at, updated_at
|
|
FROM posts
|
|
WHERE slug=%s AND is_published=1
|
|
""",
|
|
(slug,)
|
|
)
|
|
post = cur.fetchone()
|
|
finally:
|
|
conn.close()
|
|
|
|
if not post:
|
|
raise HTTPException(status_code=404, detail="Post not found")
|
|
|
|
# Parse tags
|
|
if isinstance(post['tags'], str):
|
|
post['tags'] = json.loads(post['tags'])
|
|
elif post['tags'] is None:
|
|
post['tags'] = []
|
|
|
|
# Markdown rendering handled in template via filter?
|
|
# Or pre-render here.
|
|
# Usually we pass markdown and let template render it if using a JS lib,
|
|
# OR we use `markdown` lib here.
|
|
# checking existing templates... we likely use `markdown` filter or lib.
|
|
# Code Tour mentioned "body_md".
|
|
# I will assume we render MD in Python for safety/consistency?
|
|
# Or use a client-side renderer (Zero-MD, Marked)?
|
|
# Looking at `diary_rag.py` / `rag_service.py` -> we just store MD.
|
|
# If I verify `app/templates/`?
|
|
# I'll use `markdown` python lib if available.
|
|
# Assume it is available or basic text for now.
|
|
import markdown
|
|
post['html_content'] = markdown.markdown(post['body_md'])
|
|
|
|
return req.app.state.render(
|
|
req,
|
|
"crumbforest/post.html",
|
|
post=post,
|
|
group_config=group_config,
|
|
lang=lang
|
|
)
|