horilla.contrib.generics is Horilla CRM’s CBV framework. You subclass list, kanban, detail, and form views and configure columns, filters, and permissions instead of writing repetitive template logic.
This post is Part 3 of 28 in the Horilla CRM Technical Blog series.
What is horilla.contrib.generics?
horilla.contrib.generics enables you to:
- HorillaListView — filterable tables with bulk actions
- HorillaKanbanView — pipeline boards by stage field
- HorillaDetailView — tabbed record pages
- HorillaModelForm / HorillaMultiStepForm — permission-aware forms
- HorillaFilterSet — shared filter panels across view modes
The generics toolkit
Import from horilla.contrib.generics.views:
| Class | UI |
|---|---|
| HorillaView | Module shell/layout entry |
| HorillaNavView | Top navigation bar |
| HorillaListView | Filterable table + bulk actions |
| HorillaCardView | Card grid |
| HorillaKanbanView | Pipeline board |
| HorillaGroupByView | Grouped list |
| HorillaChartView | ECharts analytics |
| HorillaSplitView | Master/detail split pane |
| HorillaTimelineView | Chronological feed |
| HorillaDetailView | Record detail page |
| HorillaDetailTabView | Tabbed sections |
| HorillaSingleFormView / HorillaMultiFormView | Create/edit |
All user-facing views combine with LoginRequiredMixin and permission decorators.
HorillaListView — declarative tables
# horilla_crm/leads/views/core.py (excerpt)
from django.contrib.auth.mixins import LoginRequiredMixin
from horilla.contrib.generics.views import HorillaListView
from horilla.utils.decorators import htmx_required, permission_required_or_denied
@method_decorator(htmx_required, name="dispatch")
@method_decorator(
permission_required_or_denied(["leads.view_lead", "leads.view_own_lead"]),
name="dispatch",
)
class LeadListView(LoginRequiredMixin, HorillaListView):
model = Lead
view_id = "leads-list"
filterset_class = LeadFilter
search_url = reverse_lazy("leads:leads_list")
main_url = reverse_lazy("leads:leads_view")
columns = [
"title", "first_name", "last_name", "email",
"lead_status", "lead_source", "industry", "annual_revenue",
]
bulk_update_fields = [
"annual_revenue", "lead_owner", "lead_status", "industry",
]
enable_quick_filters = True
max_visible_actions = 5
What you configure
| Attribute | Effect |
|---|---|
| columns | Visible columns (auto labels from model) |
| filterset_class | HorillaFilterSet integration |
| actions | Row actions (Edit, Delete, …) with permission keys |
| col_attrs | Per-column HTMX attributes (links to detail) |
| bulk_update_fields | Inline bulk edit |
| export_exclude | Fields omitted from CSV export |
| view_id | Stable ID for pinned views / saved filters |
Column-level HTMX and permissions
@cached_property
def col_attrs(self):
return [{
"title": {
"hx-get": "{get_detail_url}",
"hx-target": "#mainContent",
"hx-swap": "outerHTML",
"hx-push-url": "true",
"permission": "leads.view_lead",
"own_permission": "leads.view_own_lead",
"owner_field": "lead_owner",
}
}]
Clicking a lead title swaps the main content region to the detail view — no full page reload. Permission keys on columns hide links for unauthorized users (Part 4).
Row actions pattern
lead_permission = {
"permission": "leads.change_lead",
"own_permission": "leads.change_own_lead",
"owner_field": "lead_owner",
}
actions = [
{**lead_permission, "action": "Edit", "icon": "edit", "attrs": "..."},
{**lead_permission, "action": "Delete", "icon": "delete", ...},
]
Reuse a permission dict across actions to stay DRY.
HorillaKanbanView — pipeline boards
class LeadKanbanView(LoginRequiredMixin, HorillaKanbanView):
model = Lead
group_by_field = "lead_status"
# columns, filters, drag-drop stage changes — configured similarly to list
Kanban groups records by a FK or choice field (lead_status). Stage changes POST via HTMX; generic mixins handle optimistic UI updates and permission checks.
HorillaDetailView — record pages with tabs
class LeadDetailView(RecentlyViewedMixin, LoginRequiredMixin, HorillaDetailView):
model = Lead
template_name = "leads/lead_detail.html"
# sections, related lists, activity tab — composed from section view classes
Detail pages compose section views:
- HorillaDetailSectionView — field groups
- HorillaNotesAttachementSectionView — notes/files
- HorillaActivitySectionView — calls, meetings, tasks
- HorillaHistorySectionView — audit trail
- HorillaRelatedListSectionView — child records
Each section is its own CBV endpoint — HTMX loads tabs on demand.
Forms: HorillaModelForm and HorillaMultiStepForm
# horilla_crm/leads/forms.py (excerpt)
class LeadFormClass(OwnerQuerysetMixin, HorillaMultiStepForm):
class Meta:
model = Lead
fields = "__all__"
exclude = ["lead_score"]
step_fields = {
1: ["lead_owner", "first_name", "last_name", "email", ...],
2: ["lead_company", "industry", "annual_revenue", ...],
3: ["lead_status", "lead_source", ...],
}
HorillaFormMixin enforces field-level permissions on widgets (readonly/hidden per role — Part 4). OwnerQuerysetMixin limits FK dropdowns to records the user may assign.
PhoneWidget and PhoneField (v1.12+)
International phone numbers use a country dial-code Select2 plus number input, stored in the existing CharField column (no migration):
from horilla.contrib.generics.forms import PhoneField
class ContactForm(HorillaModelForm):
phone = PhoneField(label="Phone", required=False)
HorillaFormMixin._apply_phone_fields() also auto-replaces common field names (phone, mobile, contact_number, …) on model forms.
Edit-mode Select2 FK dropdowns load parent instances via all_objects and temporarily scope request.active_company to the record’s tenant so cross-company FK labels resolve correctly.
Filters: HorillaFilterSet
# horilla_crm/leads/filters.py
class LeadFilter(HorillaFilterSet):
class Meta:
model = Lead
fields = [...]
List/kanban/card views share the same filterset_class. Filters render in a slide-over panel; state syncs with URL query params for shareable views.
Built-in list features (free with HorillaListView)
- Infinite scroll pagination (paginate_by = 100)
- Global search within module
- Pinned views and saved filter lists
- Recently viewed/created/modified shortcuts
- Bulk delete, bulk update, bulk export
- Quick filters on common fields
- Sorting with default_sort_field
You opt in with boolean flags — no custom JavaScript per module.
URL map for Leads views
# horilla_crm/leads/urls.py (excerpt)
urlpatterns = [
path("leads-view/", views.LeadView.as_view(), name="leads_view"),
path("leads-list/", views.LeadListView.as_view(), name="leads_list"),
path("leads-kanban/", views.LeadKanbanView.as_view(), name="leads_kanban"),
path("leads-detail/<int:pk>/", views.LeadDetailView.as_view(), name="leads_detail"),
path("leads-create/", views.LeadFormView.as_view(), name="leads_create"),
]
The shell view (LeadView) loads layout chrome; list/kanban/detail endpoints return HTMX fragments targeting #mainContent or modals.
Architecture diagram
flowchart LR
subgraph URLs
A[leads-view] --> B[Shell HorillaView]
C[leads-list] --> D[HorillaListView]
E[leads-kanban] --> F[HorillaKanbanView]
G[leads-detail] --> H[HorillaDetailView]
end
D --> I[(Lead.objects)]
F --> I
H --> I
I --> J[CompanyFilteredManager]
When to subclass vs configure
| Situation | Approach |
|---|---|
| Standard CRM CRUD | Subclass generics, set model + columns |
| Custom column renderer | Override get_columns or use model methods |
| Extra context on detail | Override get_context_data sparingly |
| Completely custom UI | Still use permissions/HTMX decorators; avoid bypassing managers |
Benefits of Generic Views in Horilla CRM
- Ship list, kanban, and detail UIs in tens of lines
- Built-in search, export, pinned views, and pagination
- Column-level HTMX links to detail pages
- Permission keys on actions and columns
- One filterset shared across view modes
Generic views turn a HorillaCoreModel into a production CRM UI. Configure model, columns, and filterset_class — then focus on business logic instead of boilerplate.
Continue the series
Previous: Part 2 — A Complete Guide to HorillaCoreModel and Multi-Tenant CRM Architecture
Next: Part 4 — How Horilla CRM’s Four-Layer Permission System Enhances Data Security
More posts are at Horilla Blogs; share feedback on GitHub.