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:

ClassUI
HorillaViewModule shell/layout entry
HorillaNavViewTop navigation bar
HorillaListViewFilterable table + bulk actions
HorillaCardViewCard grid
HorillaKanbanViewPipeline board
HorillaGroupByViewGrouped list
HorillaChartViewECharts analytics
HorillaSplitViewMaster/detail split pane
HorillaTimelineViewChronological feed
HorillaDetailViewRecord detail page
HorillaDetailTabViewTabbed sections
HorillaSingleFormView / HorillaMultiFormViewCreate/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

AttributeEffect
columnsVisible columns (auto labels from model)
filterset_classHorillaFilterSet integration
actionsRow actions (Edit, Delete, …) with permission keys
col_attrsPer-column HTMX attributes (links to detail)
bulk_update_fieldsInline bulk edit
export_excludeFields omitted from CSV export
view_idStable 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

SituationApproach
Standard CRM CRUDSubclass generics, set model + columns
Custom column rendererOverride get_columns or use model methods
Extra context on detailOverride get_context_data sparingly
Completely custom UIStill 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.

Share this article