campaign sending prototype

This commit is contained in:
2026-06-24 16:20:49 +02:00
parent 841d0b4024
commit a147ad27ec
13 changed files with 274 additions and 198 deletions

View File

@@ -114,7 +114,7 @@ manifest = ModuleManifest(
route_factory=_campaigns_router,
role_templates=ROLE_TEMPLATES,
nav_items=(
NavItem(path="/campaigns", label="Campaigns", icon="mail", required_any=("campaigns:campaign:read",), order=20),
NavItem(path="/campaigns", label="Campaigns", icon="campaign", required_any=("campaigns:campaign:read",), order=20),
NavItem(
path="/operator",
label="Operator Queue",
@@ -134,7 +134,7 @@ manifest = ModuleManifest(
module_id="campaigns",
package_name="@govoplan/campaign-webui",
nav_items=(
NavItem(path="/campaigns", label="Campaigns", icon="mail", required_any=("campaigns:campaign:read",), order=20),
NavItem(path="/campaigns", label="Campaigns", icon="campaign", required_any=("campaigns:campaign:read",), order=20),
NavItem(
path="/operator",
label="Operator Queue",

View File

@@ -9,6 +9,8 @@ from typing import Any
from sqlalchemy.orm import Session
from govoplan_core.settings import settings as core_settings
from govoplan_campaign.backend.db.models import (
Campaign,
CampaignIssue,
@@ -89,6 +91,7 @@ def _load_delivery_info(version: CampaignVersion | None, jobs: list[CampaignJob]
"imap_config_fingerprint": None,
"estimated_remaining_send_seconds": None,
"estimated_remaining_send_human": None,
"background_workers_enabled": bool(core_settings.celery_enabled),
}
if not version or not isinstance(version.execution_snapshot, dict):
default["load_error"] = "No execution snapshot exists; rebuild the current locked version before delivery."
@@ -131,6 +134,7 @@ def _load_delivery_info(version: CampaignVersion | None, jobs: list[CampaignJob]
"imap_config_fingerprint": snapshot.imap_config_fingerprint,
"estimated_remaining_send_seconds": estimated_seconds,
"estimated_remaining_send_human": _human_duration(estimated_seconds),
"background_workers_enabled": bool(core_settings.celery_enabled),
}

View File

@@ -54,7 +54,7 @@ from govoplan_files.backend.storage.files import current_version_and_blob
from govoplan_files.backend.storage.campaign_attachments import managed_match_payloads, prepared_campaign_snapshot
from govoplan_campaign.backend.campaign.loader import load_campaign_config, load_campaign_json
from govoplan_campaign.backend.attachments.resolver import resolve_campaign_attachments
from govoplan_campaign.backend.time import utc_now
from govoplan_core.security.time import utc_now
from govoplan_campaign.backend.persistence.versions import (
LockedCampaignVersionError,
create_minimal_campaign,

View File

@@ -12,6 +12,7 @@ from uuid import uuid4
from sqlalchemy.orm import Session
from govoplan_core.settings import settings as core_settings
from govoplan_campaign.backend.db.models import (
Campaign,
CampaignJob,
@@ -197,6 +198,14 @@ def _get_current_version(session: Session, campaign: Campaign, version_id: str |
return version
def _celery_enabled() -> bool:
return bool(core_settings.celery_enabled)
def _should_enqueue_celery(enqueue_celery: bool) -> bool:
return bool(enqueue_celery and _celery_enabled())
def _celery_enqueue_send_job(job_id: str) -> None:
from govoplan_core.celery_app import celery
@@ -292,7 +301,7 @@ def queue_campaign_jobs(
session.commit()
enqueued_count = 0
if enqueue_celery and not dry_run:
if _should_enqueue_celery(enqueue_celery) and not dry_run:
for job in queued:
_celery_enqueue_send_job(job.id)
enqueued_count += 1
@@ -402,6 +411,8 @@ def send_campaign_now(
def enqueue_existing_queued_jobs(session: Session, *, tenant_id: str, campaign_id: str) -> int:
if not _celery_enabled():
return 0
campaign = _get_campaign_for_tenant(session, campaign_id=campaign_id, tenant_id=tenant_id)
jobs = (
session.query(CampaignJob)
@@ -459,7 +470,7 @@ def resume_campaign_jobs(session: Session, *, tenant_id: str, campaign_id: str,
session.commit()
enqueued_count = 0
if enqueue_celery:
if _should_enqueue_celery(enqueue_celery):
for job in jobs:
_celery_enqueue_send_job(job.id)
enqueued_count += 1
@@ -577,7 +588,7 @@ def queue_failed_jobs_for_retry(
session.commit()
enqueued = 0
if enqueue_celery and not dry_run:
if _should_enqueue_celery(enqueue_celery) and not dry_run:
for job in selected:
_celery_enqueue_send_job(job.id)
enqueued += 1
@@ -645,7 +656,7 @@ def queue_unattempted_jobs(
session.add(version)
session.commit()
enqueued = 0
if enqueue_celery and not dry_run:
if _should_enqueue_celery(enqueue_celery) and not dry_run:
for job in selected:
_celery_enqueue_send_job(job.id)
enqueued += 1
@@ -1092,7 +1103,7 @@ def send_campaign_job(
session.add(job)
_update_campaign_after_job(session, job.campaign_id, job.campaign_version_id)
session.commit()
if enqueue_imap_task and job.imap_status == JobImapStatus.PENDING.value:
if enqueue_imap_task and _celery_enabled() and job.imap_status == JobImapStatus.PENDING.value:
_celery_enqueue_append_sent_job(job.id)
return SendJobResult(
job_id=job.id,
@@ -1265,13 +1276,14 @@ def enqueue_pending_imap_appends(session: Session, *, tenant_id: str, campaign_i
.order_by(CampaignJob.entry_index.asc())
.all()
)
if not dry_run and enqueue_celery:
should_enqueue = _should_enqueue_celery(enqueue_celery) and not dry_run
if should_enqueue:
for job in jobs:
_celery_enqueue_append_sent_job(job.id)
return {
"campaign_id": campaign.id,
"pending_count": len(jobs),
"enqueued_count": 0 if dry_run or not enqueue_celery else len(jobs),
"enqueued_count": len(jobs) if should_enqueue else 0,
"dry_run": dry_run,
}

View File

@@ -1,21 +0,0 @@
from __future__ import annotations
from datetime import datetime, timezone
def utc_now() -> datetime:
return datetime.now(timezone.utc)
def ensure_aware_utc(value: datetime | None) -> datetime | None:
"""Return a timezone-aware UTC datetime.
SQLite and some DB drivers may return naive datetimes even when SQLAlchemy
columns are declared as DateTime(timezone=True). Treat naive values as UTC
so comparisons with aware `datetime.now(timezone.utc)` do not crash.
"""
if value is None:
return None
if value.tzinfo is None:
return value.replace(tzinfo=timezone.utc)
return value.astimezone(timezone.utc)