Skip to content

Async dashboard add-on

Turn lifecycle events into visible status. Whether you use the bundled ModelTrackingAsyncManager or build your own, this chapter covers how to persist task metadata, expose it in a dashboard, and customise formats.


1. Use the reusable dashboard manager

from powercrud.async_dashboard import ModelTrackingAsyncManager

class AppAsyncManager(ModelTrackingAsyncManager):
    record_model_path = "myapp.AsyncTaskRecord"

    def format_user(self, user):
        if not user:
            return "Anonymous"
        if hasattr(user, "get_username"):
            return user.get_username()
        return str(user)

    def format_affected(self, affected):
        if isinstance(affected, (list, tuple, set)):
            return ", ".join(map(str, affected))
        return str(affected) if affected else ""

Then configure PowerCRUD (and other launch sites) to use it:

POWERCRUD_SETTINGS = {
    "ASYNC_MANAGER_DEFAULT": {
        "manager_class": "myapp.async_manager.AppAsyncManager",
    }
}

If your dashboard model uses different field names, supply a field_map (either as a manager class attribute or inside the config) so lifecycle payload keys map cleanly to your columns.

The built-in manager handles:

  • Creating/updating a dashboard record per task.
  • Persisting progress messages, results, timestamps.
  • Marking records cleaned up once locks are removed.

2. Dashboard model (example)

Reuse the sample model or tailor fields to your needs. If your field names differ, provide a field_map in the manager config so lifecycle payload keys line up with your model columns.

class AsyncTaskRecord(models.Model):
    task_name = models.CharField(max_length=64, unique=True)
    status = models.CharField(max_length=20, choices=STATUS.choices, default=STATUS.PENDING)
    message = models.TextField(blank=True)
    progress_payload = models.TextField(blank=True)
    user_label = models.CharField(max_length=255, blank=True)
    affected_objects = models.TextField(blank=True)
    result_payload = models.JSONField(blank=True, null=True)
    cleaned_up = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    completed_at = models.DateTimeField(blank=True, null=True)
    failed_at = models.DateTimeField(blank=True, null=True)

Adjust types (e.g., JSON vs TextField) to suit the data you store.


3. Surfacing the dashboard

The sample app includes a CRUD view and detail page at /sample/async-dashboard/. You can copy that approach:

class AsyncTaskRecordView(PowerCRUDMixin, CRUDView):
    model = AsyncTaskRecord
    namespace = "async"
    use_htmx = True
    use_modal = True
    fields = ["task_name", "status", "user_label", "updated_at"]
    column_value_formats = {"updated_at": "time"}
    detail_fields = ["task_name", "status", "message", "progress_payload", "result_payload", "completed_at", "failed_at"]

The list uses Django's temporal format settings. PowerCRUD defaults datetime list columns to date for legacy compatibility, while the named override shows updated_at as time-only. Set DATE_FORMAT, TIME_FORMAT, and DATETIME_FORMAT in Django settings to control the resulting text. The detail view can poll the same progress endpoint to show live updates.


4. Custom lifecycle logic

If you need additional side-effects:

class NotifyingAsyncManager(ModelTrackingAsyncManager):
    record_model_path = "myapp.AsyncTaskRecord"

    def async_task_lifecycle(self, event, task_name, **payload):
        super().async_task_lifecycle(event, task_name, **payload)
        if event == "fail":
            notify_ops(f"Task {task_name} failed: {payload.get('result')}")
        elif event == "complete":
            log_success(task_name, payload.get("result"))

Events you can handle:

  • create – task queued
  • progressupdate_progress called
  • complete – worker succeeded (may include result)
  • fail – worker returned/raised failure
  • cleanup – locks cleared (always fired after complete/fail)

5. Clean-up utilities

cleanup_completed_tasks() (called via pcrud_cleanup_async) sets cleaned_up = True on dashboard rows once locks and progress are removed. You can override cleanup_dashboard_data if you store additional related objects.

class AppAsyncManager(ModelTrackingAsyncManager):
    def cleanup_dashboard_data(self, task_name):
        # return count of records deleted if you remove anything extra
        return 0

6. Sample app recap

Reference implementation lives in the sample project:

  • sample/models.AsyncTaskRecord
  • sample/async_manager.SampleAsyncManager
  • sample/views.AsyncTaskRecordCRUDView
  • sample/tests.SampleAsyncDashboardTests

Use it as a starting point or lift pieces directly into your project.

Key options

Setting / Hook Default Purpose
async_manager_class_path / ASYNC_MANAGER_DEFAULT resolves to AsyncManager Choose the manager that persists lifecycle events.
record_model_path required Tell ModelTrackingAsyncManager which model to write to.
format_user, format_affected, format_payload basic formatters Override formatting of stored metadata.
async_task_lifecycle no-op Hook into task events (create, progress, complete, fail, cleanup) for custom behaviour.
cleanup_dashboard_data returns 0 Remove dashboard artefacts during cleanup.

See the sample app and reference for full implementations.


Next steps

If you want to tweak the look and feel—modals, tables, Tailwind safelist—continue with Styling & Tailwind. Otherwise, consult the reference for clean-up commands and settings.