republisher/repub/web/admin/actions.py
Abel Luck e4a5246ab3
All checks were successful
buildbot/nix-eval Build done.
buildbot/nix-build Build done.
buildbot/nix-effects Build done.
Add publisher dashboard routes
2026-06-02 10:18:59 +02:00

181 lines
6.6 KiB
Python

from __future__ import annotations
from collections.abc import Awaitable, Callable
from typing import Any, cast
from datastar_py import ServerSentEventGenerator as SSE
from datastar_py.quart import DatastarResponse, read_signals
from peewee import IntegrityError
from quart import Quart, Response
from repub.jobs import clear_completed_executions
from repub.model import (
create_source,
delete_job_source,
delete_source,
load_job_enabled,
save_setting,
source_slug_exists,
update_source,
)
from repub.web.app import (
RUNS_TAB_STATE_KEY,
_read_optional_signals,
_read_tab_id,
get_job_runtime,
get_tab_state_store,
run_job_now_response,
trigger_refresh,
validate_settings_form,
validate_source_form,
)
RouteGuard = Callable[[Callable[..., Awaitable[Any]]], Callable[..., Awaitable[Any]]]
def register_admin_actions(app: Quart, *, admin_required: RouteGuard) -> None:
@app.post("/admin/actions/sources/create")
@admin_required
async def admin_create_source_action() -> DatastarResponse:
signals = cast(dict[str, object], await read_signals())
source, error = validate_source_form(
signals,
slug_exists=source_slug_exists,
)
if error is not None:
return DatastarResponse(
SSE.patch_signals({"_formError": error, "_formSuccess": ""})
)
assert source is not None
try:
create_source(**source)
except IntegrityError:
return DatastarResponse(
SSE.patch_signals(
{"_formError": "Slug must be unique.", "_formSuccess": ""}
)
)
get_job_runtime(app).sync_jobs()
trigger_refresh(app)
return DatastarResponse(SSE.redirect("/admin/sources"))
@app.post("/admin/actions/sources/<string:slug>/edit")
@admin_required
async def admin_edit_source_action(slug: str) -> DatastarResponse:
signals = cast(dict[str, object], await read_signals())
source, error = validate_source_form(
signals,
slug_exists=lambda candidate: candidate != slug
and source_slug_exists(candidate),
immutable_slug=slug,
)
if error is not None:
return DatastarResponse(
SSE.patch_signals({"_formError": error, "_formSuccess": ""})
)
assert source is not None
if update_source(slug, **source) is None:
return DatastarResponse(
SSE.patch_signals(
{"_formError": "Source does not exist.", "_formSuccess": ""}
)
)
get_job_runtime(app).sync_jobs()
trigger_refresh(app)
return DatastarResponse(SSE.redirect("/admin/sources"))
@app.post("/admin/actions/sources/<string:slug>/delete")
@admin_required
async def admin_delete_source_action(slug: str) -> Response:
delete_source(slug)
get_job_runtime(app).sync_jobs()
trigger_refresh(app)
return Response(status=204)
@app.post("/admin/actions/settings")
@admin_required
async def admin_update_settings_action() -> DatastarResponse:
signals = cast(dict[str, object], await read_signals())
settings, error = validate_settings_form(signals)
if error is not None:
return DatastarResponse(
SSE.patch_signals({"_formError": error, "_formSuccess": ""})
)
assert settings is not None
save_setting("max_concurrent_jobs", settings["max_concurrent_jobs"])
save_setting("feed_url", settings["feed_url"])
trigger_refresh(app)
return DatastarResponse(SSE.redirect("/admin/settings"))
@app.post("/admin/actions/runs/completed-page/<int:page>")
@admin_required
async def admin_set_completed_runs_page_action(page: int) -> Response:
signals = await _read_optional_signals()
tab_id = _read_tab_id(signals)
if tab_id is None:
return Response(status=400)
get_tab_state_store(app).update_page_state(
tab_id,
RUNS_TAB_STATE_KEY,
lambda state: {**state, "completed_page": max(1, page)},
)
trigger_refresh(app, tab_id=tab_id)
return Response(status=204)
@app.post("/admin/actions/jobs/<int:job_id>/run-now")
@admin_required
async def admin_run_job_now_action(job_id: int) -> Response:
return run_job_now_response(app, job_id)
@app.post("/admin/actions/jobs/<int:job_id>/toggle-enabled")
@admin_required
async def admin_toggle_job_enabled_action(job_id: int) -> Response:
enabled = load_job_enabled(job_id)
if enabled is not None:
get_job_runtime(app).set_job_enabled(job_id, enabled=not enabled)
trigger_refresh(app)
return Response(status=204)
@app.post("/admin/actions/jobs/<int:job_id>/delete")
@admin_required
async def admin_delete_job_action(job_id: int) -> Response:
delete_job_source(job_id)
get_job_runtime(app).sync_jobs()
trigger_refresh(app)
return Response(status=204)
@app.post("/admin/actions/executions/<int:execution_id>/cancel")
@admin_required
async def admin_cancel_execution_action(execution_id: int) -> Response:
get_job_runtime(app).request_execution_cancel(execution_id)
trigger_refresh(app)
return Response(status=204)
@app.post("/admin/actions/queued-executions/<int:execution_id>/cancel")
@admin_required
async def admin_cancel_queued_execution_action(execution_id: int) -> Response:
get_job_runtime(app).cancel_queued_execution(execution_id)
trigger_refresh(app)
return Response(status=204)
@app.post("/admin/actions/queued-executions/<int:execution_id>/move-up")
@admin_required
async def admin_move_queued_execution_up_action(execution_id: int) -> Response:
get_job_runtime(app).move_queued_execution(execution_id, direction="up")
return Response(status=204)
@app.post("/admin/actions/queued-executions/<int:execution_id>/move-down")
@admin_required
async def admin_move_queued_execution_down_action(execution_id: int) -> Response:
get_job_runtime(app).move_queued_execution(execution_id, direction="down")
return Response(status=204)
@app.post("/admin/actions/completed-executions/clear")
@admin_required
async def admin_clear_completed_executions_action() -> Response:
clear_completed_executions(log_dir=app.config["REPUB_LOG_DIR"])
trigger_refresh(app)
return Response(status=204)