horilla.contrib.activity uses one Activity model (task, meeting, log_call, event) attached via GenericForeignKey. Detail tabs load through HorillaActivitySectionView.
This post is Part 13 of 28 in the Horilla CRM Technical Blog series.
What is the activity module?
The activity module enables you to:
- Polymorphic activity linked to leads, contacts, etc.
- Task completion and filtering via HTMX
- Calendar sync for meetings (Part 25)
- Notes and attachments on detail pages
Activity model and record ownership
The activity subsystem is located in horilla.contrib.activity. It uses one Activity model rather than separate database models for calls, meetings, tasks, and events. The activity type is selected from ACTIVITY_TYPES: event, meeting, task, and log_call. This common representation gives every activity the same lifecycle, related-record integration, permission path, and timeline behavior while allowing UI fields and labels to vary by type.
Do not introduce Call, Meeting, or Task models merely to add type-specific screens. That fragments filtering, activity counts, and ownership logic. Put shared scheduling, status, description, participants, and relation information on Activity; add narrowly scoped conditional behavior in forms or views only when a type genuinely requires it. When extending the model, verify the new field is meaningful for multiple types or is safely optional for the others.
| Type | Typical use | Implementation note |
| event | General calendar or CRM event | Link to the related record and participants |
| meeting | Customer or internal meeting | Use a consistent start/end schedule |
| task | Follow-up work item | Ensure completion state is filterable |
| log_call | Completed or planned call record | Store outcome in the shared activity record |
Activity declares OWNER_FIELDS = [“owner”, “assigned_to”]. These fields drive row-level permission behavior, including own-record visibility rules. Code that creates activities must assign them intentionally: owner represents responsibility for the activity and assigned_to identify the person expected to act. Do not silently assign an arbitrary superuser when a request lacks a user; that turns a validation failure into an inaccessible work item.
Related objects and attachment storage
Activities are meant to be reusable across CRM modules. Their relationship to business objects should therefore use the established generic relation pattern rather than adding one nullable foreign key for each possible domain model. A lead, contact, opportunity, or account can expose activity history through the same content-type/object-ID query shape. This also lets a single activity section remain independent of the module that hosts it.
Attachments use a GenericForeignKey relationship. The attachment is not restricted to one activity subtype, and attachment handling should always validate both the target content type and object ID before rendering or downloading a file. Authorization must be checked against the associated record, not inferred solely from an opaque attachment URL. Treat uploaded files as user-controlled input: validate content type and size according to deployment policy and use storage names that cannot collide.
activity = Activity.objects.create(
activity_type="task",
subject="Send implementation notes",
owner=request.user,
assigned_to=request.user,
content_object=opportunity,
)
The example illustrates the intended association, but project forms and views should remain the normal creation route so field-level permissions and form validation are applied. Bulk creation code needs equivalent company, ownership, and generic-target validation.
Rendering the activity workspace
HorillaActivitySectionView is the activity-focused section view used to surface the model in a related record context. It should receive enough identity information to determine the parent object, constrain the query to that object, and apply standard access controls before rendering. Query only the required columns for large timelines, order predictable event streams consistently, and paginate or lazy-load older activity items when a record has extensive history.
An activity section is a context-specific view, not a permission bypass. First validate access to the lead or opportunity that owns the tab, then apply Activity permissions and OWNER_FIELDS restrictions. Filtering only by generic object ID is unsafe because IDs overlap among content types. Always filter with both the content type and object identifier.
activities = Activity.objects.filter(
content_type=ContentType.objects.get_for_model(lead),
object_id=lead.pk,
).order_by("-created_at")
The production implementation should use the project’s access-aware query utilities rather than treating this compact query as a complete authorization layer.
Mail entries in the activity tab
Email is displayed in an activity-oriented record view through HorillaMail from horilla.contrib.mail; it is not represented as a second activity model or a special email activity type. This distinction prevents duplicated timelines and keeps email delivery metadata—recipients, opens, message status, and attachments—owned by the mail application. A tab can combine Activity entries and related HorillaMail records into a chronological display, but preserve their original types so templates show the appropriate controls and status.
When composing a combined feed, normalize timestamps in the database timezone, use stable tie-breaking, and avoid per-row generic relation queries. Prefetch or batch-resolve related objects where the rendering layer needs labels. Mail visibility must follow both mail permissions and access to its generic target.
Practical extension guidance
Register related activity behavior through the activity_related feature rather than hard-coding an activity tab into each CRM app. This lets supported models participate consistently in activity rendering and feature permissions. Before enabling an activity relation for a new model, decide which users may create, reassign, complete, and view activities, then test those decisions with owner and non-owner accounts.
For scheduled reminders, store the due or start time on the activity and use background jobs only as delivery mechanisms. The Activity record remains the source of truth. Make reminder delivery idempotent, mark the delivery state only after success, and ensure a reassignment changes the recipient used by future notifications. These details avoid duplicate reminders and stale ownership after routine CRM edits.
Benefits of Activity in Horilla CRM
- Single activity UX across all CRM entities
- Deferred tab loading for performance
- Feeds cadences and reporting
Register your model for activity features and the detail tab appears automatically — no custom timeline code per module.
Continue the series
Previous: Part 12 — How to Set Up Email Templates, SMTP, and Mail-to-Lead in Horilla CRM
Next: Part 14 — Everything You Need to Know About Global Search in Horilla CRM
More posts are at Horilla Blogs; share feedback on GitHub.