campaign sending prototype
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user