update
This commit is contained in:
@@ -10,6 +10,7 @@ Kein Deploy in diesem Block — Routen werden lokal registriert.
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sqlite3
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
@@ -54,7 +55,187 @@ def ensure_published_doku_prompts_schema(con: sqlite3.Connection) -> None:
|
||||
"CREATE INDEX IF NOT EXISTS idx_pub_doku_type_status "
|
||||
"ON published_doku_prompts(doc_type, status, updated_at)"
|
||||
)
|
||||
cols = {r[1] for r in con.execute("PRAGMA table_info(published_doku_prompts)").fetchall()}
|
||||
if "city" not in cols:
|
||||
con.execute("ALTER TABLE published_doku_prompts ADD COLUMN city TEXT DEFAULT ''")
|
||||
con.commit()
|
||||
ensure_published_doku_prompt_ratings_schema(con)
|
||||
|
||||
|
||||
VALID_HALF_STAR_RATINGS: Tuple[float, ...] = (
|
||||
0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0,
|
||||
)
|
||||
|
||||
|
||||
def ensure_published_doku_prompt_ratings_schema(con: sqlite3.Connection) -> None:
|
||||
con.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS published_doku_prompt_ratings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
public_prompt_id TEXT NOT NULL,
|
||||
rater_user_id TEXT NOT NULL,
|
||||
rating_value REAL NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
UNIQUE(public_prompt_id, rater_user_id)
|
||||
)
|
||||
"""
|
||||
)
|
||||
con.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_pub_doku_ratings_prompt "
|
||||
"ON published_doku_prompt_ratings(public_prompt_id)"
|
||||
)
|
||||
con.commit()
|
||||
|
||||
|
||||
def normalize_rating_value(value: Any) -> Optional[float]:
|
||||
try:
|
||||
v = float(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
if abs(v * 2 - round(v * 2)) > 1e-9:
|
||||
return None
|
||||
v = round(v * 2) / 2.0
|
||||
if v not in VALID_HALF_STAR_RATINGS:
|
||||
return None
|
||||
return float(v)
|
||||
|
||||
|
||||
def _round_rating_average(avg: Optional[float]) -> Optional[float]:
|
||||
if avg is None:
|
||||
return None
|
||||
try:
|
||||
return round(float(avg) * 10) / 10.0
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def fetch_rating_aggregates_batch(
|
||||
con: sqlite3.Connection,
|
||||
public_ids: List[str],
|
||||
current_user_id: str,
|
||||
) -> Dict[str, Dict[str, Any]]:
|
||||
"""Aggregate + my_rating fuer mehrere public_prompt_ids (kein N+1)."""
|
||||
ids = [str(i).strip() for i in (public_ids or []) if str(i).strip()]
|
||||
out: Dict[str, Dict[str, Any]] = {
|
||||
pid: {"rating_average": None, "rating_count": 0, "my_rating": None}
|
||||
for pid in ids
|
||||
}
|
||||
if not ids:
|
||||
return out
|
||||
placeholders = ",".join("?" for _ in ids)
|
||||
rows = con.execute(
|
||||
f"""
|
||||
SELECT public_prompt_id, AVG(rating_value) AS avg_v, COUNT(*) AS cnt
|
||||
FROM published_doku_prompt_ratings
|
||||
WHERE public_prompt_id IN ({placeholders})
|
||||
GROUP BY public_prompt_id
|
||||
""",
|
||||
ids,
|
||||
).fetchall()
|
||||
for pid, avg_v, cnt in rows:
|
||||
out[str(pid)] = {
|
||||
"rating_average": _round_rating_average(avg_v),
|
||||
"rating_count": int(cnt or 0),
|
||||
"my_rating": None,
|
||||
}
|
||||
uid = (current_user_id or "").strip()
|
||||
if uid:
|
||||
mine = con.execute(
|
||||
f"""
|
||||
SELECT public_prompt_id, rating_value
|
||||
FROM published_doku_prompt_ratings
|
||||
WHERE public_prompt_id IN ({placeholders}) AND rater_user_id = ?
|
||||
""",
|
||||
ids + [uid],
|
||||
).fetchall()
|
||||
for pid, rv in mine:
|
||||
key = str(pid)
|
||||
if key in out:
|
||||
out[key]["my_rating"] = float(rv)
|
||||
return out
|
||||
|
||||
|
||||
def compute_can_rate(*, author_user_id: str, current_user_id: str, published: bool) -> bool:
|
||||
if not published:
|
||||
return False
|
||||
au = (author_user_id or "").strip()
|
||||
cu = (current_user_id or "").strip()
|
||||
if not cu or not au:
|
||||
return False
|
||||
return au != cu
|
||||
|
||||
|
||||
def apply_rating_fields_to_public_item(
|
||||
item: Dict[str, Any],
|
||||
*,
|
||||
current_user_id: str,
|
||||
agg: Optional[Dict[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
author_uid = str(item.get("author_user_id") or item.get("user_id") or "").strip()
|
||||
published = str(item.get("status") or "published").strip().lower() == "published"
|
||||
if not published and "status" not in item:
|
||||
published = True
|
||||
base = dict(item)
|
||||
a = agg or {}
|
||||
avg = a.get("rating_average", item.get("rating_average"))
|
||||
cnt = int(a.get("rating_count", item.get("rating_count") or 0) or 0)
|
||||
my = a.get("my_rating", item.get("my_rating"))
|
||||
base["rating_average"] = _round_rating_average(avg) if cnt else None
|
||||
base["rating_count"] = cnt
|
||||
base["my_rating"] = normalize_rating_value(my) if my is not None else None
|
||||
base["can_rate"] = compute_can_rate(
|
||||
author_user_id=author_uid,
|
||||
current_user_id=current_user_id,
|
||||
published=published,
|
||||
)
|
||||
return base
|
||||
|
||||
|
||||
def upsert_public_prompt_rating(
|
||||
con: sqlite3.Connection,
|
||||
*,
|
||||
public_prompt_id: str,
|
||||
rater_user_id: str,
|
||||
rating_value: float,
|
||||
) -> Dict[str, Any]:
|
||||
pid = (public_prompt_id or "").strip()
|
||||
uid = (rater_user_id or "").strip()
|
||||
rv = normalize_rating_value(rating_value)
|
||||
if not pid or not uid:
|
||||
raise ValueError("missing ids")
|
||||
if rv is None:
|
||||
raise ValueError("invalid rating")
|
||||
row = con.execute(
|
||||
"SELECT user_id, status FROM published_doku_prompts WHERE id = ?",
|
||||
(pid,),
|
||||
).fetchone()
|
||||
if not row:
|
||||
raise LookupError("not found")
|
||||
author_uid, status = str(row[0] or ""), str(row[1] or "")
|
||||
if status != "published":
|
||||
raise PermissionError("not published")
|
||||
if author_uid == uid:
|
||||
raise PermissionError("own template")
|
||||
now = _now_ts()
|
||||
con.execute(
|
||||
"""
|
||||
INSERT INTO published_doku_prompt_ratings
|
||||
(public_prompt_id, rater_user_id, rating_value, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT(public_prompt_id, rater_user_id) DO UPDATE SET
|
||||
rating_value = excluded.rating_value,
|
||||
updated_at = excluded.updated_at
|
||||
""",
|
||||
(pid, uid, rv, now, now),
|
||||
)
|
||||
con.commit()
|
||||
agg = fetch_rating_aggregates_batch(con, [pid], uid).get(pid, {})
|
||||
return apply_rating_fields_to_public_item(
|
||||
{"id": pid, "author_user_id": author_uid, "status": status},
|
||||
current_user_id=uid,
|
||||
agg=agg,
|
||||
)
|
||||
|
||||
|
||||
def _now_ts() -> int:
|
||||
@@ -62,8 +243,19 @@ def _now_ts() -> int:
|
||||
|
||||
|
||||
def _sanitize_server_text(text: str) -> str:
|
||||
from aza_doku_vorlagen import sanitize_prompt_text
|
||||
return sanitize_prompt_text(text)
|
||||
"""Plaintext only — serverseitig ohne Desktop/tkinter-Import."""
|
||||
if not text:
|
||||
return ""
|
||||
s = str(text)
|
||||
s = re.sub(r"(?is)<script[^>]*>.*?</script>", "", s)
|
||||
s = re.sub(r"(?is)<style[^>]*>.*?</style>", "", s)
|
||||
s = re.sub(r"<[^>]+>", "", s)
|
||||
s = re.sub(r"(?i)javascript\s*:", "", s)
|
||||
s = re.sub(r"(?i)data\s*:", "", s)
|
||||
s = s.replace("\x00", "")
|
||||
if len(s) > MAX_PROMPT_CHARS:
|
||||
s = s[:MAX_PROMPT_CHARS]
|
||||
return s.strip()
|
||||
|
||||
|
||||
def _resolve_auth_context(con: sqlite3.Connection, request: Request) -> Tuple[str, str]:
|
||||
@@ -87,6 +279,12 @@ class PublishDokuPromptIn(BaseModel):
|
||||
server_id: Optional[str] = ""
|
||||
language: Optional[str] = ""
|
||||
specialty: Optional[str] = ""
|
||||
city: Optional[str] = ""
|
||||
visibility: Optional[str] = "public"
|
||||
|
||||
|
||||
class RateDokuPromptIn(BaseModel):
|
||||
rating: float
|
||||
|
||||
|
||||
def register_doku_prompt_routes(app, *, stripe_db_path_fn) -> None:
|
||||
@@ -99,31 +297,72 @@ def register_doku_prompt_routes(app, *, stripe_db_path_fn) -> None:
|
||||
raise HTTPException(status_code=503, detail="database missing")
|
||||
with sqlite3.connect(db_path) as con:
|
||||
ensure_published_doku_prompts_schema(con)
|
||||
_resolve_auth_context(con, request)
|
||||
user_id, _practice_id = _resolve_auth_context(con, request)
|
||||
rows = con.execute(
|
||||
"""
|
||||
SELECT id, doc_type, title, description, content, author_display,
|
||||
revision, updated_at
|
||||
SELECT id, user_id, practice_id, doc_type, title, description, content,
|
||||
author_display, revision, updated_at, specialty, city
|
||||
FROM published_doku_prompts
|
||||
WHERE status = 'published'
|
||||
ORDER BY doc_type, updated_at DESC
|
||||
"""
|
||||
).fetchall()
|
||||
items = [
|
||||
{
|
||||
pub_ids = [str(r[0]) for r in rows]
|
||||
agg_map = fetch_rating_aggregates_batch(con, pub_ids, user_id)
|
||||
items = []
|
||||
for r in rows:
|
||||
raw = normalize_public_doku_item({
|
||||
"id": r[0],
|
||||
"doc_type": r[1],
|
||||
"title": r[2],
|
||||
"description": r[3] or "",
|
||||
"content": r[4],
|
||||
"author_display": r[5] or "",
|
||||
"revision": int(r[6] or 1),
|
||||
"updated_at": int(r[7] or 0),
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
"user_id": r[1],
|
||||
"practice_id": r[2],
|
||||
"doc_type": r[3],
|
||||
"title": r[4],
|
||||
"description": r[5] or "",
|
||||
"content": r[6],
|
||||
"author_display": r[7] or "",
|
||||
"revision": int(r[8] or 1),
|
||||
"updated_at": int(r[9] or 0),
|
||||
"specialty": r[10] or "",
|
||||
"city": r[11] or "",
|
||||
"visibility": "public",
|
||||
"status": "published",
|
||||
})
|
||||
items.append(
|
||||
apply_rating_fields_to_public_item(
|
||||
raw,
|
||||
current_user_id=user_id,
|
||||
agg=agg_map.get(str(r[0])),
|
||||
)
|
||||
)
|
||||
return {"ok": True, "items": items, "count": len(items)}
|
||||
|
||||
@app.post("/v1/doku-prompts/rate/{pub_id}", dependencies=[Depends(require_api_token)])
|
||||
def doku_prompts_rate(pub_id: str, request: Request, body: RateDokuPromptIn):
|
||||
db_path = stripe_db_path_fn()
|
||||
if not Path(db_path).exists():
|
||||
raise HTTPException(status_code=503, detail="database missing")
|
||||
rv = normalize_rating_value(body.rating)
|
||||
if rv is None:
|
||||
raise HTTPException(status_code=400, detail="invalid rating")
|
||||
with sqlite3.connect(db_path) as con:
|
||||
ensure_published_doku_prompts_schema(con)
|
||||
user_id, _practice_id = _resolve_auth_context(con, request)
|
||||
try:
|
||||
result = upsert_public_prompt_rating(
|
||||
con,
|
||||
public_prompt_id=pub_id,
|
||||
rater_user_id=user_id,
|
||||
rating_value=rv,
|
||||
)
|
||||
except LookupError:
|
||||
raise HTTPException(status_code=404, detail="not found") from None
|
||||
except PermissionError as exc:
|
||||
msg = str(exc)
|
||||
if msg == "own template":
|
||||
raise HTTPException(status_code=403, detail="cannot rate own template") from None
|
||||
raise HTTPException(status_code=403, detail="not published") from None
|
||||
return {"ok": True, **result}
|
||||
|
||||
@app.post("/v1/doku-prompts/publish", dependencies=[Depends(require_api_token)])
|
||||
def doku_prompts_publish(request: Request, body: PublishDokuPromptIn):
|
||||
db_path = stripe_db_path_fn()
|
||||
@@ -149,21 +388,22 @@ def register_doku_prompt_routes(app, *, stripe_db_path_fn) -> None:
|
||||
"""
|
||||
INSERT INTO published_doku_prompts
|
||||
(id, user_id, practice_id, doc_type, title, description, content,
|
||||
author_display, revision, status, language, specialty, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'published', ?, ?, ?, ?)
|
||||
author_display, revision, status, language, specialty, city,
|
||||
created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'published', ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
doc_type=excluded.doc_type, title=excluded.title, description=excluded.description,
|
||||
content=excluded.content, author_display=excluded.author_display,
|
||||
revision=excluded.revision, status='published',
|
||||
language=excluded.language, specialty=excluded.specialty,
|
||||
updated_at=excluded.updated_at
|
||||
city=excluded.city, updated_at=excluded.updated_at
|
||||
""",
|
||||
(
|
||||
pub_id, user_id, practice_id, body.doc_type.strip(),
|
||||
body.title.strip(), body.description.strip(), content,
|
||||
body.author_display.strip(), rev,
|
||||
(body.language or "").strip(), (body.specialty or "").strip(),
|
||||
created_at, now,
|
||||
(body.city or "").strip(), created_at, now,
|
||||
),
|
||||
)
|
||||
con.commit()
|
||||
@@ -227,7 +467,8 @@ def _http_post(url: str, headers: Dict[str, str], body: Any) -> Tuple[int, Any]:
|
||||
try:
|
||||
return r.status_code, r.json()
|
||||
except Exception:
|
||||
return r.status_code, {}
|
||||
raw = (r.text or "").strip()
|
||||
return r.status_code, {"detail": raw} if raw else {}
|
||||
|
||||
|
||||
def local_private_templates_to_sync_items(data: dict) -> List[Dict[str, Any]]:
|
||||
@@ -344,24 +585,111 @@ def sync_doku_prompts_with_server(app) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def fetch_public_doku_prompts(app) -> List[Dict[str, Any]]:
|
||||
def normalize_public_doku_item(raw: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Einheitliches Client-Format für öffentliche Doku-Prompts."""
|
||||
if not isinstance(raw, dict):
|
||||
return {}
|
||||
sid = str(raw.get("id") or raw.get("server_id") or "").strip()
|
||||
author_uid = str(raw.get("author_user_id") or raw.get("user_id") or "").strip()
|
||||
city = str(raw.get("city") or raw.get("location") or raw.get("region") or "").strip()
|
||||
return {
|
||||
"id": sid,
|
||||
"server_id": sid,
|
||||
"doc_type": str(raw.get("doc_type") or "").strip(),
|
||||
"title": str(raw.get("title") or raw.get("name") or "").strip(),
|
||||
"description": str(raw.get("description") or "").strip(),
|
||||
"content": str(raw.get("content") or raw.get("prompt") or "").strip(),
|
||||
"author_display": str(raw.get("author_display") or raw.get("author") or "").strip(),
|
||||
"author_user_id": author_uid,
|
||||
"specialty": str(raw.get("specialty") or "").strip(),
|
||||
"city": city,
|
||||
"location": city,
|
||||
"practice_id": str(raw.get("practice_id") or "").strip(),
|
||||
"visibility": str(raw.get("visibility") or raw.get("scope") or "public").strip(),
|
||||
"revision": int(raw.get("revision") or 1),
|
||||
"updated_at": int(raw.get("updated_at") or 0),
|
||||
"rating_average": raw.get("rating_average"),
|
||||
"rating_count": int(raw.get("rating_count") or 0),
|
||||
"my_rating": raw.get("my_rating"),
|
||||
"can_rate": bool(raw.get("can_rate")),
|
||||
}
|
||||
|
||||
|
||||
def normalize_public_doku_items(items: List[Any]) -> List[Dict[str, Any]]:
|
||||
out: List[Dict[str, Any]] = []
|
||||
for it in items or []:
|
||||
norm = normalize_public_doku_item(it if isinstance(it, dict) else {})
|
||||
if norm.get("id") or norm.get("title"):
|
||||
out.append(norm)
|
||||
return out
|
||||
|
||||
|
||||
def fetch_public_doku_prompts_result(app) -> Tuple[List[Dict[str, Any]], Optional[str]]:
|
||||
"""Lädt öffentliche Vorlagen; Fehler-Tags: __ROUTE_MISSING__, __CONN_ERROR__."""
|
||||
headers = _sync_headers(app)
|
||||
if not headers:
|
||||
return []
|
||||
return [], None
|
||||
try:
|
||||
bu = app.get_backend_url().rstrip("/")
|
||||
except Exception:
|
||||
return []
|
||||
code, data = _http_get(f"{bu}/v1/doku-prompts/public", headers)
|
||||
return [], "__CONN_ERROR__"
|
||||
try:
|
||||
code, data = _http_get(f"{bu}/v1/doku-prompts/public", headers)
|
||||
except Exception:
|
||||
return [], "__CONN_ERROR__"
|
||||
if code in (404, 405):
|
||||
return [], "__ROUTE_MISSING__"
|
||||
if code != 200 or not isinstance(data, dict) or not data.get("ok"):
|
||||
return []
|
||||
detail = ""
|
||||
if isinstance(data, dict):
|
||||
detail = str(data.get("detail") or data.get("message") or "").strip().lower()
|
||||
if detail in ("not found", "notfound"):
|
||||
return [], "__ROUTE_MISSING__"
|
||||
return [], None
|
||||
items = data.get("items")
|
||||
return items if isinstance(items, list) else []
|
||||
if not isinstance(items, list):
|
||||
return [], None
|
||||
return normalize_public_doku_items(items), None
|
||||
|
||||
|
||||
def fetch_public_doku_prompts(app) -> List[Dict[str, Any]]:
|
||||
items, _err = fetch_public_doku_prompts_result(app)
|
||||
return items
|
||||
|
||||
|
||||
def _profile_publish_extras(app) -> Dict[str, str]:
|
||||
prof = getattr(app, "_user_profile", {}) or {}
|
||||
specialty = str(prof.get("specialty") or "").strip()
|
||||
city = str(prof.get("city") or prof.get("location") or prof.get("region") or "").strip()
|
||||
return {"specialty": specialty, "city": city}
|
||||
|
||||
|
||||
def resolve_author_display(app) -> str:
|
||||
"""Verfasser-Anzeigename: bevorzugt Profil-Anzeigename, sonst Signaturname."""
|
||||
try:
|
||||
prof = getattr(app, "_user_profile", {}) or {}
|
||||
for k in ("display_name", "name"):
|
||||
v = str(prof.get(k) or "").strip()
|
||||
if v:
|
||||
return v
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
from aza_persistence import load_signature_name
|
||||
return (load_signature_name() or "").strip()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def publish_doku_prompt(
|
||||
app, doc_type: str, tpl: dict, title: str, description: str = "",
|
||||
) -> Tuple[bool, str]:
|
||||
"""Veröffentlicht eine Vorlage. Rückgabe (ok, message).
|
||||
|
||||
Sentinel-Meldungen für saubere UI-Übersetzung (kein rohes "Not Found"):
|
||||
"__ROUTE_MISSING__" → Route nicht deployt (404)
|
||||
"__CONN_ERROR__" → Server nicht erreichbar / Timeout
|
||||
"""
|
||||
headers = _sync_headers(app)
|
||||
if not headers:
|
||||
return False, "Nicht angemeldet oder keine Praxis-Zuordnung."
|
||||
@@ -369,12 +697,8 @@ def publish_doku_prompt(
|
||||
bu = app.get_backend_url().rstrip("/")
|
||||
except Exception:
|
||||
return False, "Backend nicht konfiguriert."
|
||||
author = ""
|
||||
try:
|
||||
from aza_persistence import load_signature_name
|
||||
author = load_signature_name() or ""
|
||||
except Exception:
|
||||
pass
|
||||
author = resolve_author_display(app)
|
||||
extras = _profile_publish_extras(app)
|
||||
body = {
|
||||
"doc_type": doc_type,
|
||||
"title": title,
|
||||
@@ -382,11 +706,95 @@ def publish_doku_prompt(
|
||||
"content": tpl.get("content") or "",
|
||||
"author_display": author,
|
||||
"server_id": tpl.get("server_id") or tpl.get("id") or "",
|
||||
"language": "de",
|
||||
"specialty": (tpl.get("specialty") or extras.get("specialty") or "").strip(),
|
||||
"city": (tpl.get("city") or tpl.get("location") or extras.get("city") or "").strip(),
|
||||
"visibility": "public",
|
||||
}
|
||||
code, data = _http_post(f"{bu}/v1/doku-prompts/publish", headers, body)
|
||||
try:
|
||||
code, data = _http_post(f"{bu}/v1/doku-prompts/publish", headers, body)
|
||||
except Exception:
|
||||
return False, "__CONN_ERROR__"
|
||||
if code == 200 and isinstance(data, dict) and data.get("ok"):
|
||||
return True, ""
|
||||
if code in (404, 405):
|
||||
# Route lokal vorhanden, aber auf dem Live-Server (noch) nicht deployt.
|
||||
return False, "__ROUTE_MISSING__"
|
||||
detail = ""
|
||||
if isinstance(data, dict):
|
||||
detail = str(data.get("detail") or data.get("message") or "")
|
||||
if detail.strip().lower() in ("not found", "notfound"):
|
||||
return False, "__ROUTE_MISSING__"
|
||||
return False, detail or f"Server-Fehler ({code})"
|
||||
|
||||
|
||||
def unpublish_doku_prompt(app, pub_id: str) -> Tuple[bool, str]:
|
||||
"""Entfernt eigene Veröffentlichung vom Server (falls Route live)."""
|
||||
headers = _sync_headers(app)
|
||||
if not headers:
|
||||
return False, "Nicht angemeldet oder keine Praxis-Zuordnung."
|
||||
pid = (pub_id or "").strip()
|
||||
if not pid:
|
||||
return False, "Keine Veröffentlichungs-ID."
|
||||
try:
|
||||
bu = app.get_backend_url().rstrip("/")
|
||||
except Exception:
|
||||
return False, "Backend nicht konfiguriert."
|
||||
try:
|
||||
code, data = _http_post(f"{bu}/v1/doku-prompts/unpublish/{pid}", headers, {})
|
||||
except Exception:
|
||||
return False, "__CONN_ERROR__"
|
||||
if code == 200 and isinstance(data, dict) and data.get("ok"):
|
||||
return True, ""
|
||||
if code in (404, 405):
|
||||
return False, "__ROUTE_MISSING__"
|
||||
detail = ""
|
||||
if isinstance(data, dict):
|
||||
detail = str(data.get("detail") or data.get("message") or "")
|
||||
if detail.strip().lower() in ("not found", "notfound"):
|
||||
return False, "__ROUTE_MISSING__"
|
||||
return False, detail or f"Server-Fehler ({code})"
|
||||
|
||||
|
||||
def rate_public_doku_prompt_result(
|
||||
app, pub_id: str, rating: float,
|
||||
) -> Tuple[bool, str, Dict[str, Any]]:
|
||||
"""Bewertet fremde oeffentliche Vorlage. Rueckgabe (ok, message, payload)."""
|
||||
headers = _sync_headers(app)
|
||||
if not headers:
|
||||
return False, "Nicht angemeldet oder keine Praxis-Zuordnung.", {}
|
||||
pid = (pub_id or "").strip()
|
||||
if not pid:
|
||||
return False, "Keine Veröffentlichungs-ID.", {}
|
||||
rv = normalize_rating_value(rating)
|
||||
if rv is None:
|
||||
return False, "Ungültige Bewertung (0,5–5,0 in 0,5-Schritten).", {}
|
||||
try:
|
||||
bu = app.get_backend_url().rstrip("/")
|
||||
except Exception:
|
||||
return False, "Backend nicht konfiguriert.", {}
|
||||
try:
|
||||
code, data = _http_post(
|
||||
f"{bu}/v1/doku-prompts/rate/{pid}",
|
||||
headers,
|
||||
{"rating": rv},
|
||||
)
|
||||
except Exception:
|
||||
return False, "__CONN_ERROR__", {}
|
||||
if code == 200 and isinstance(data, dict) and data.get("ok"):
|
||||
payload = {k: data.get(k) for k in (
|
||||
"rating_average", "rating_count", "my_rating", "can_rate",
|
||||
)}
|
||||
return True, "", payload
|
||||
if code in (404, 405):
|
||||
return False, "__ROUTE_MISSING__", {}
|
||||
if code == 403:
|
||||
return False, "Diese Vorlage kann nicht bewertet werden.", {}
|
||||
if code == 400:
|
||||
return False, "Ungültige Bewertung.", {}
|
||||
detail = ""
|
||||
if isinstance(data, dict):
|
||||
detail = str(data.get("detail") or data.get("message") or "")
|
||||
if detail.strip().lower() in ("not found", "notfound"):
|
||||
return False, "__ROUTE_MISSING__", {}
|
||||
return False, detail or f"Server-Fehler ({code})", {}
|
||||
|
||||
Reference in New Issue
Block a user