Moderation: Foto-Freigabe repariert (rasse_id join, delegate an wiki-API), SW by-v364
This commit is contained in:
parent
d603b7bae1
commit
034f7ef21f
3 changed files with 42 additions and 61 deletions
|
|
@ -170,22 +170,20 @@ async def mod_patch_user(uid: int, data: dict, user=Depends(require_moderator)):
|
||||||
@router.get("/fotos")
|
@router.get("/fotos")
|
||||||
async def mod_fotos(user=Depends(require_moderator)):
|
async def mod_fotos(user=Depends(require_moderator)):
|
||||||
with db() as conn:
|
with db() as conn:
|
||||||
try:
|
rows = conn.execute("""
|
||||||
rows = conn.execute("""
|
SELECT s.id, s.foto_url, s.created_at,
|
||||||
SELECT s.id, s.rasse_slug, s.foto_url, s.created_at,
|
COALESCE(s.rights_confirmed, 0) AS rights_confirmed,
|
||||||
COALESCE(s.rights_confirmed, 0) AS rights_confirmed,
|
u.name AS user_name,
|
||||||
u.name AS user_name,
|
r.name AS rasse_name, r.slug AS rasse_slug,
|
||||||
r.name AS rasse_name, r.foto_url AS aktuell_foto
|
r.foto_url AS aktuell_foto
|
||||||
FROM wiki_foto_submissions s
|
FROM wiki_foto_submissions s
|
||||||
LEFT JOIN users u ON u.id=s.user_id
|
LEFT JOIN users u ON u.id = s.user_id
|
||||||
LEFT JOIN wiki_rassen r ON r.slug=s.rasse_slug
|
LEFT JOIN wiki_rassen r ON r.id = s.rasse_id
|
||||||
WHERE s.status='pending'
|
WHERE s.status = 'pending'
|
||||||
ORDER BY s.created_at ASC
|
ORDER BY s.created_at ASC
|
||||||
LIMIT 50
|
LIMIT 50
|
||||||
""").fetchall()
|
""").fetchall()
|
||||||
return [dict(r) for r in rows]
|
return [dict(r) for r in rows]
|
||||||
except Exception:
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
@ -193,32 +191,10 @@ async def mod_fotos(user=Depends(require_moderator)):
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@router.patch("/fotos/{foto_id}")
|
@router.patch("/fotos/{foto_id}")
|
||||||
async def mod_foto_action(foto_id: int, data: dict, user=Depends(require_moderator)):
|
async def mod_foto_action(foto_id: int, data: dict, user=Depends(require_moderator)):
|
||||||
action = data.get("action")
|
"""Delegiert an die wiki-Route — dort ist die vollständige Logik (inkl. Datei-Kopie)."""
|
||||||
if action not in ("approve", "reject"):
|
from routes.wiki import review_submission, ReviewModel
|
||||||
raise HTTPException(400, "action muss 'approve' oder 'reject' sein.")
|
model = ReviewModel(
|
||||||
|
action=data.get("action", ""),
|
||||||
with db() as conn:
|
reject_reason=data.get("reject_reason", ""),
|
||||||
sub = conn.execute(
|
)
|
||||||
"SELECT id, rasse_slug, foto_url FROM wiki_foto_submissions WHERE id=?",
|
return await review_submission(foto_id, model, user)
|
||||||
(foto_id,)
|
|
||||||
).fetchone()
|
|
||||||
if not sub:
|
|
||||||
raise HTTPException(404, "Einreichung nicht gefunden.")
|
|
||||||
|
|
||||||
if action == "approve":
|
|
||||||
conn.execute(
|
|
||||||
"UPDATE wiki_foto_submissions SET status='approved' WHERE id=?",
|
|
||||||
(foto_id,)
|
|
||||||
)
|
|
||||||
conn.execute(
|
|
||||||
"UPDATE wiki_rassen SET foto_url=? WHERE slug=?",
|
|
||||||
(sub["foto_url"], sub["rasse_slug"])
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
reason = data.get("reject_reason", "Nicht geeignet.")
|
|
||||||
conn.execute(
|
|
||||||
"UPDATE wiki_foto_submissions SET status='rejected', reject_reason=? WHERE id=?",
|
|
||||||
(reason, foto_id)
|
|
||||||
)
|
|
||||||
|
|
||||||
return {"ok": True}
|
|
||||||
|
|
|
||||||
|
|
@ -164,10 +164,12 @@ window.Page_moderation = (() => {
|
||||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));
|
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));
|
||||||
gap:var(--space-4)">
|
gap:var(--space-4)">
|
||||||
${fotos.map(f => `
|
${fotos.map(f => `
|
||||||
<div class="card" style="padding:var(--space-4)">
|
<div class="card" style="padding:var(--space-4)" data-id="${f.id}">
|
||||||
<img src="${_esc(f.foto_url)}" alt=""
|
<a href="#wiki?rasse=${_esc(f.rasse_slug)}" style="display:block;text-decoration:none">
|
||||||
style="width:100%;height:140px;object-fit:cover;
|
<img src="${_esc(f.foto_url)}" alt=""
|
||||||
border-radius:var(--radius-md);margin-bottom:var(--space-3)">
|
style="width:100%;height:140px;object-fit:cover;
|
||||||
|
border-radius:var(--radius-md);margin-bottom:var(--space-3)">
|
||||||
|
</a>
|
||||||
<div style="font-weight:var(--weight-semibold);font-size:var(--text-sm)">
|
<div style="font-weight:var(--weight-semibold);font-size:var(--text-sm)">
|
||||||
${_esc(f.rasse_name || f.rasse_slug)}
|
${_esc(f.rasse_name || f.rasse_slug)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -183,18 +185,18 @@ window.Page_moderation = (() => {
|
||||||
background:#fef9c3;color:#92400e">⚠ Keine Bestätigung</span>`}
|
background:#fef9c3;color:#92400e">⚠ Keine Bestätigung</span>`}
|
||||||
</div>
|
</div>
|
||||||
${f.aktuell_foto ? `
|
${f.aktuell_foto ? `
|
||||||
|
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:4px">Aktuell:</div>
|
||||||
<img src="${_esc(f.aktuell_foto)}" alt="Aktuell"
|
<img src="${_esc(f.aktuell_foto)}" alt="Aktuell"
|
||||||
style="width:100%;height:80px;object-fit:cover;
|
style="width:100%;height:70px;object-fit:cover;
|
||||||
border-radius:var(--radius-sm);opacity:.5;
|
border-radius:var(--radius-sm);opacity:.5;
|
||||||
margin-bottom:var(--space-2)">
|
margin-bottom:var(--space-3)">
|
||||||
<div style="font-size:var(--text-xs);color:var(--c-text-muted);
|
` : `<div style="font-size:var(--text-xs);color:var(--c-warning);
|
||||||
margin-bottom:var(--space-3)">aktuelles Foto</div>
|
margin-bottom:var(--space-3)">Noch kein Foto vorhanden</div>`}
|
||||||
` : ''}
|
|
||||||
<div style="display:flex;gap:var(--space-2)">
|
<div style="display:flex;gap:var(--space-2)">
|
||||||
<button class="btn btn-sm btn-primary mod-foto-approve"
|
<button class="btn btn-sm btn-primary mod-foto-approve"
|
||||||
data-id="${f.id}" style="flex:1">Freigeben</button>
|
data-id="${f.id}" style="flex:1">✓ Freigeben</button>
|
||||||
<button class="btn btn-sm btn-ghost mod-foto-reject"
|
<button class="btn btn-sm btn-ghost mod-foto-reject"
|
||||||
data-id="${f.id}" style="color:var(--c-danger)">Ablehnen</button>
|
data-id="${f.id}" style="color:var(--c-danger)">✗ Ablehnen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`).join('')}
|
`).join('')}
|
||||||
|
|
@ -204,23 +206,26 @@ window.Page_moderation = (() => {
|
||||||
el.querySelectorAll('.mod-foto-approve').forEach(btn => {
|
el.querySelectorAll('.mod-foto-approve').forEach(btn => {
|
||||||
btn.addEventListener('click', async () => {
|
btn.addEventListener('click', async () => {
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
|
btn.textContent = '…';
|
||||||
try {
|
try {
|
||||||
await API.patch(`/moderation/fotos/${btn.dataset.id}`, { action: 'approve' });
|
await API.patch(`/moderation/fotos/${btn.dataset.id}`, { action: 'approve' });
|
||||||
UI.toast.success('Foto freigegeben.');
|
UI.toast('Foto freigegeben.', 'success');
|
||||||
await _loadFotos(el);
|
await _loadFotos(el);
|
||||||
} catch (e) { UI.toast.error(e.message); btn.disabled = false; }
|
} catch (e) { UI.toast(e.message, 'danger'); btn.disabled = false; btn.textContent = '✓ Freigeben'; }
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
el.querySelectorAll('.mod-foto-reject').forEach(btn => {
|
el.querySelectorAll('.mod-foto-reject').forEach(btn => {
|
||||||
btn.addEventListener('click', async () => {
|
btn.addEventListener('click', async () => {
|
||||||
|
const reason = prompt('Ablehnungsgrund (optional, wird dem User angezeigt):');
|
||||||
|
if (reason === null) return;
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
try {
|
try {
|
||||||
await API.patch(`/moderation/fotos/${btn.dataset.id}`, {
|
await API.patch(`/moderation/fotos/${btn.dataset.id}`, {
|
||||||
action: 'reject',
|
action: 'reject',
|
||||||
reject_reason: 'Nicht geeignet.'
|
reject_reason: reason || 'Foto entspricht nicht den Anforderungen.'
|
||||||
});
|
});
|
||||||
UI.toast.success('Foto abgelehnt.');
|
UI.toast('Einreichung abgelehnt.', 'info');
|
||||||
await _loadFotos(el);
|
await _loadFotos(el);
|
||||||
} catch (e) { UI.toast.error(e.message); btn.disabled = false; }
|
} catch (e) { UI.toast.error(e.message); btn.disabled = false; }
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Offline-Cache + Push Notifications + Tile-Cache
|
Offline-Cache + Push Notifications + Tile-Cache
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const CACHE_VERSION = 'by-v363';
|
const CACHE_VERSION = 'by-v364';
|
||||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||||
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue