Fix source actions and toggle regressions

This commit is contained in:
Abel Luck 2026-03-30 17:25:37 +02:00
parent 94717b1d1b
commit 2a99edeec3
5 changed files with 122 additions and 5 deletions

View file

@ -305,6 +305,15 @@ def delete_job_source(job_id: int) -> bool:
return source.delete_instance() > 0
def delete_source(slug: str) -> bool:
with database.connection_context():
with database.atomic():
source = Source.get_or_none(Source.slug == slug)
if source is None:
return False
return source.delete_instance() > 0
def load_sources() -> tuple[dict[str, object], ...]:
with database.connection_context():
sources = tuple(Source.select().order_by(Source.created_at.desc()))

View file

@ -54,6 +54,29 @@ def _checked(source: Mapping[str, object] | None, key: str, default: bool) -> bo
return bool(value)
def _action_button(
*,
label: str,
tone: str = "default",
post_path: str | None = None,
) -> Renderable:
classes = {
"default": "bg-stone-100 text-slate-700 hover:bg-stone-200",
"danger": "bg-rose-50 text-rose-700 hover:bg-rose-100",
}
attributes: dict[str, str] = {}
if post_path is not None:
attributes["data-on:pointerdown"] = f"@post('{post_path}')"
return h.button(
attributes,
type="button",
class_=(
"inline-flex items-center whitespace-nowrap rounded-full px-3 py-1.5 "
f"text-sm font-semibold transition {classes[tone]}"
),
)[label]
def _source_row(source: Mapping[str, object]) -> tuple[Node, ...]:
return (
h.div[
@ -81,6 +104,11 @@ def _source_row(source: Mapping[str, object]) -> tuple[Node, ...]:
href=f"/sources/{source['slug']}/edit", label="Edit", tone="amber"
),
inline_link(href="/runs", label="View runs"),
_action_button(
label="Delete",
tone="danger",
post_path=f"/actions/sources/{source['slug']}/delete",
),
],
)

View file

@ -26,6 +26,7 @@ from repub.model import (
Job,
create_source,
delete_job_source,
delete_source,
initialize_database,
load_source_form,
load_sources,
@ -209,6 +210,13 @@ def create_app(*, dev_mode: bool = False) -> Quart:
trigger_refresh(app)
return DatastarResponse(SSE.redirect("/sources"))
@app.post("/actions/sources/<string:slug>/delete")
async def delete_source_action(slug: str) -> Response:
delete_source(slug)
get_job_runtime(app).sync_jobs()
trigger_refresh(app)
return Response(status=204)
@app.post("/runs")
async def runs_patch() -> DatastarResponse:
return _page_patch_response(app, lambda: render_runs(app))
@ -383,7 +391,7 @@ def validate_source_form(
source_type = _read_string(signals, "sourceType")
feed_url = _read_string(signals, "feedUrl")
pangea_domain = _read_string(signals, "pangeaDomain")
pangea_category = _read_string(signals, "pangeaCategory")
pangea_category = _read_string(signals, "pangeaCategory", strip=False)
content_format = _read_string(signals, "contentFormat")
content_type = _read_string(signals, "contentType")
max_articles = _read_string(signals, "maxArticles")
@ -474,8 +482,9 @@ def validate_source_form(
return source, None
def _read_string(signals: dict[str, object], key: str) -> str:
return str(signals.get(key, "")).strip()
def _read_string(signals: dict[str, object], key: str, *, strip: bool = True) -> str:
value = str(signals.get(key, ""))
return value.strip() if strip else value
def _read_bool(signals: dict[str, object], key: str, *, default: bool = False) -> bool:

View file

@ -604,6 +604,46 @@ def test_delete_job_action_removes_source_job_and_execution_history(
asyncio.run(run())
def test_delete_source_action_removes_source_job_and_execution_history(
monkeypatch, tmp_path: Path
) -> None:
db_path = tmp_path / "delete-source.db"
monkeypatch.setenv("REPUBLISHER_DB_PATH", str(db_path))
async def run() -> None:
app = create_app()
client = app.test_client()
source = create_source(
name="Delete source row",
slug="delete-source-row",
source_type="feed",
notes="",
spider_arguments="",
enabled=True,
cron_minute="*/30",
cron_hour="*",
cron_day_of_month="*",
cron_day_of_week="*",
cron_month="*",
feed_url="https://example.com/delete-source-row.xml",
)
job = Job.get(Job.source == source)
execution = JobExecution.create(
job=job,
running_status=JobExecutionStatus.SUCCEEDED,
)
response = await client.post("/actions/sources/delete-source-row/delete")
assert response.status_code == 204
assert Source.get_or_none(Source.slug == "delete-source-row") is None
assert Job.get_or_none(id=job.id) is None
assert JobExecution.get_or_none(id=int(execution.get_id())) is None
asyncio.run(run())
def _wait_for_running_execution(
execution_id: int, *, timeout_seconds: float = 2.0
) -> JobExecution:

View file

@ -430,6 +430,37 @@ def test_render_sources_shows_table_and_create_link() -> None:
asyncio.run(run())
def test_render_sources_shows_delete_action_for_each_source(
monkeypatch, tmp_path: Path
) -> None:
db_path = tmp_path / "sources-delete-row.db"
monkeypatch.setenv("REPUBLISHER_DB_PATH", str(db_path))
app = create_app()
create_source(
name="Delete me",
slug="delete-me",
source_type="feed",
notes="",
spider_arguments="",
enabled=True,
cron_minute="0",
cron_hour="*",
cron_day_of_month="*",
cron_day_of_week="*",
cron_month="*",
feed_url="https://example.com/delete.xml",
)
async def run() -> None:
body = str(await render_sources(app))
assert "Delete" in body
assert "data-on:pointerdown" in body
assert "/actions/sources/delete-me/delete" in body
asyncio.run(run())
def test_render_create_source_shows_dedicated_form_page() -> None:
async def run() -> None:
body = str(await render_create_source())
@ -567,13 +598,13 @@ def test_create_source_action_creates_pangea_source_and_job_in_database(
assert source.name == "Kenya health desk"
assert source.source_type == "pangea"
assert pangea.category_name == " Health "
assert pangea.content_type == "breakingnews"
assert pangea.include_content is True
assert job.enabled is True
assert job.spider_arguments == "language=en\ndownload_media=true"
assert job.cron_hour == "*/6"
assert "kenya-health" in rendered_sources
assert "example.org / Health" in rendered_sources
assert "Enabled" in rendered_sources
asyncio.run(run())