Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/rahul-baberwal/django-var-cms/llms.txt

Use this file to discover all available pages before exploring further.

VarCMSModelAdmin is the base class you subclass and pass to var_cms_site.register(). Every attribute is optional — sensible defaults apply out of the box so you can start with a minimal class and layer in customization as needed.
from var_cms.registry import var_cms_site, VarCMSModelAdmin

Constructor

__init__(model, site)

Called automatically by var_cms_site.register(). Sets self.model and self.site. If list_display is empty at construction time, it is auto-populated with the first six concrete (non-many-to-many, non-one-to-many) model fields.
ParameterTypeDescription
modelType[Model]The Django model class being registered.
siteVarCMSSiteThe site instance that owns this admin.

List View Attributes

These attributes control the paginated list view rendered for each registered model.
list_display
List[str]
default:"[]"
Column fields shown in the list view table. Each entry is a model field name or a double-underscore traversal (e.g., "category__name"). When left empty, the first six concrete model fields are used automatically (populated in __init__).
list_filter
List[str]
default:"[]"
Fields used to generate filter sidebar widgets. The widget type is inferred from the field type: BooleanField → toggle, DateField / DateTimeField → date-range, ForeignKey → select, IntegerField / FloatField → number-range, everything else → text input.
search_fields
List[str]
default:"[]"
Fields searched with a case-insensitive icontains lookup. Multiple fields are ORed together.
search_fields = ["title", "body", "author"]
ordering
List[str]
default:"[]"
Default queryset ordering passed directly to Django’s order_by(). Prefix a field with - to sort descending.
ordering = ["-created_at"]
list_per_page
int
default:"25"
Number of rows displayed per page. The built-in paginator uses this value.
When True, calls .select_related() on the base queryset, eagerly loading related objects and reducing N+1 queries for ForeignKey columns in list_display.
list_image_width
int
default:"38"
Thumbnail width in pixels for ImageField columns rendered in the list view.
list_image_height
int
default:"38"
Thumbnail height in pixels for ImageField columns rendered in the list view.

Form Attributes

These attributes shape the add/edit form — which fields appear, how they look, and how they are validated.
readonly_fields
List[str]
default:"[]"
Fields that are always shown on the form but are never editable by anyone (including superusers). Their rendered values are displayed as read-only text alongside the form.
readonly_fields = ["created_at", "updated_at", "view_count"]
exclude_fields
List[str]
default:"[]"
Fields hidden from the form entirely. Unlike readonly_fields, excluded fields are not shown at all.
html_fields
List[str]
default:"[]"
Fields rendered with the integrated Quill.js rich-text editor instead of a plain <textarea>. Only applicable to TextField fields.
html_fields = ["body", "description"]
regex_validators
Dict[str, Union[str, Tuple[str, str]]]
default:"{}"
Per-field regex validation applied both server-side (via Django’s RegexValidator) and client-side (via the HTML pattern attribute).The value for each field is either a plain regex string or a (pattern, error_message) tuple:
regex_validators = {
    "slug": (r"^[a-z0-9-]+$", "Slug must only contain lowercase letters, numbers, and hyphens."),
}
form_field_widths
Dict[str, str]
default:"{}"
Width presets for individual fields. The form uses a 12-column grid. Available values:
PresetColumnsDescription
"full"12Full form width. Default for checkboxes and textareas.
"half"6Half width. Default for most other fields.
"one-third"4One-third width.
"two-thirds"8Two-thirds width.
"one-fourth"3One-fourth width.
"three-fourths"9Three-fourths width.
form_field_widths = {
    "title":    "two-thirds",
    "status":   "one-third",
    "category": "half",
}
form_field_rows
List[List[str]]
default:"[]"
Group multiple fields into a single visual row. Fields within the same row automatically share the available width equally (e.g., three fields → 4 columns each on a 12-column grid). Row groupings override any individual form_field_widths entry for those fields.
form_field_rows = [
    ["first_name", "last_name"],
    ["mobile", "email", "date_of_birth"],
]
form_field_widgets
Dict[str, str]
default:"{}"
Override the widget rendered for a specific field. Useful for ForeignKey, CharField with choices, and ManyToManyField fields.
Widget typeRendered as
"select"Standard HTML <select> dropdown (default; no override needed).
"select_search"Searchable dropdown with a live-filter input.
"multiselect"Checkbox list for multi-selection (no search).
"multiselect_search"Checkbox list with a search input above it.
form_field_widgets = {
    "status":   "select",
    "customer": "select_search",
    "tags":     "multiselect_search",
}
multiselect and multiselect_search work best with ManyToManyField fields. Use select_search for ForeignKey fields.
form_field_classes
Dict[str, str]
default:"{}"
Extra CSS classes applied to the container <div> that wraps each field (label + input).
form_field_classes = {
    "title": "highlighted-field",
}
form_field_styles
Dict[str, str]
default:"{}"
Inline CSS applied to the container <div> that wraps each field.
form_field_styles = {
    "body": "border-top: 2px solid var(--accent);",
}
form_widget_classes
Dict[str, str]
default:"{}"
CSS classes injected directly on the <input> / <select> / <textarea> element itself (not the wrapper div).
form_widget_classes = {
    "title": "form-control-lg",
}
form_field_placeholders
Dict[str, str]
default:"{}"
Placeholder text rendered inside each input element.
form_field_placeholders = {
    "title": "Enter article title...",
    "slug":  "auto-generated-if-blank",
}
form_field_help_texts
Dict[str, str]
default:"{}"
Help text displayed below each field, overriding any help text set on the model field itself.
form_field_help_texts = {
    "body": "Write the full article content using the Quill editor.",
    "tags": "Comma-separated tags, e.g. python, django, cms.",
}

Permissions Attributes

permissions
List[RolePermission]
default:"[]"
A list of RolePermission, GroupPermission, or UserPermission objects defining which actions each role or user may perform.When this list is empty the legacy allow_add / allow_edit / allow_delete flags are used as a fallback, and only authenticated superusers have unrestricted access.
from var_cms.permissions import RolePermission, UserPermission

permissions = [
    RolePermission("superuser", add=True,  list=True, view=True, edit=True,  delete=True),
    RolePermission("editor",    add=True,  list=True, view=True, edit=True,  delete=False),
    RolePermission("viewer",    add=False, list=True, view=True, edit=False, delete=False),
    # Per-user override — alice can delete despite being in the viewer group
    UserPermission("alice",     add=True,  list=True, view=True, edit=True,  delete=True),
]
role_editable_fields
Dict[str, Union[List[str], str]]
default:"{}"
Maps each role name to the subset of fields that role is allowed to edit. Use the string "__all__" to permit editing every non-readonly field.The special key "*" acts as a wildcard for any role not explicitly listed. An empty list [] denies all edits.
role_editable_fields = {
    "superuser": "__all__",
    "editor":    ["title", "slug", "body", "status"],
    "author":    ["title", "body", "status"],
    "*":         [],   # all other roles: no edits
}
allow_add
bool
default:"True"
Legacy fallback only. Permits the add action when permissions = []. Ignored when permissions is populated.
allow_edit
bool
default:"True"
Legacy fallback only. Permits the edit action when permissions = []. Ignored when permissions is populated.
allow_delete
bool
default:"True"
Legacy fallback only. Permits the delete action when permissions = []. Ignored when permissions is populated.
Once you set permissions = [...], the allow_add, allow_edit, and allow_delete attributes have no effect. Define all access rules through the permissions list.

Display Attributes

icon
str
default:"\"\""
A Lucide icon name displayed next to the model in the sidebar navigation and on dashboard cards.
icon = "file-text"   # renders the file-text Lucide icon
dashboard_card
bool
default:"False"
When True, a summary card for this model appears on the dashboard. The card shows a live object count and the buttons defined in card_buttons.
card_buttons
List[Dict[str, str]]
default:"[]"
Quick-action buttons rendered at the bottom of the dashboard card. Each button is a dict with the following keys:
KeyRequiredDescription
labelButton text.
actionBuilt-in shortcut: "list" or "add". The URL is resolved automatically.
urlExplicit URL (used when action is not set).
classCSS class: "btn-primary", "btn-ghost", "btn-danger", etc.
When card_buttons is empty and dashboard_card = True, a View List and (if permitted) Add New button are added automatically.
card_buttons = [
    {"label": "All Articles", "action": "list"},
    {"label": "Write Draft",  "action": "add"},
    {"label": "External",     "url": "https://example.com", "class": "btn-ghost"},
]
custom_object_actions
List[Dict[str, Any]]
default:"[]"
Action buttons rendered in list view row menus and on detail view pages. Each action is a dict:
KeyRequiredDescription
nameUnique identifier used in the URL (/var-cms/{app}/{model}/{pk}/action/{name}/).
labelButton text.
action_fnA callable or a string naming a method on the admin class. Receives (request, obj). Return None to redirect back, or return an HttpResponse.
classCSS class: "btn-primary", "btn-green", "btn-blue", "btn-danger", "btn-ghost".
iconLucide icon name shown on the button.
custom_object_actions = [
    {
        "name":      "toggle_active",
        "label":     "Toggle Active",
        "class":     "btn-blue",
        "icon":      "shuffle",
        "action_fn": "toggle_active_status",
    }
]

def toggle_active_status(self, request, obj):
    obj.is_active = not obj.is_active
    obj.save()
    return None   # redirects to referer

Methods

get_queryset(request) → QuerySet

Returns the base queryset used for all list and detail views. Override to filter records, annotate fields, or apply custom ordering.
def get_queryset(self, request):
    qs = super().get_queryset(request)
    # Only show published articles to non-superusers
    if not request.user.is_superuser:
        qs = qs.filter(status="published")
    return qs
The default implementation calls self.model._default_manager.all(), optionally calls .select_related() when list_select_related = True, and applies ordering if set.

apply_search(qs, query: str) → QuerySet

Filters the queryset by applying a case-insensitive icontains lookup across all search_fields, OR-joined into a single Q object. Returns qs unchanged when query is empty or search_fields is empty.
def apply_search(self, qs, query: str):
    if not query or not self.search_fields:
        return qs
    q = Q()
    for f in self.search_fields:
        q |= Q(**{f"{f}__icontains": query})
    return qs.filter(q)

apply_filters(qs, params: dict) → QuerySet

Applies filter parameters (typically from request.GET) to the queryset. Keys ending in __gte or __lte are applied directly as range filters. Any remaining key that matches a concrete model field name is applied as an exact match. The reserved keys q, page, o, and ot are skipped.
def apply_filters(self, qs, params: dict):
    for key, value in params.items():
        if not value or key in ("q", "page", "o", "ot"):
            continue
        if key.endswith("__gte") or key.endswith("__lte"):
            qs = qs.filter(**{key: value})
        elif key in [f.name for f in self.model._meta.get_fields()]:
            qs = qs.filter(**{key: value})
    return qs

has_permission(request, action: str) → bool

Returns True if the current user is allowed to perform action on this model. action must be one of: "add", "list", "view", "edit", "delete". Resolution order:
  1. If permissions is empty → falls back to allow_add / allow_edit / allow_delete flags (list and view always return True in fallback mode).
  2. If permissions is populated → delegates to resolve_permission() with the user’s role and username.
Unauthenticated users always receive False regardless of the permissions list.

get_editable_fields(request) → Union[List[str], str]

Returns the fields the current user is allowed to edit: either a list of field names, or "__all__" (meaning every non-readonly field). The method determines the user’s role via _get_user_role(request) and delegates to resolve_editable_fields(). When role_editable_fields is empty, "__all__" is returned for every user.
def save_model(self, request, obj, form, change: bool):
    editable = self.get_editable_fields(request)
    if editable != "__all__" and "status" not in editable:
        # Prevent status tampering outside the hook
        obj.status = obj.__class__.objects.get(pk=obj.pk).status
    super().save_model(request, obj, form, change)

get_form(request, instance=None) → ModelForm

Builds and returns a bound or unbound ModelForm for the model. Called by both the add and edit views.
  • Excludes exclude_fields and readonly_fields from the form fields.
  • Applies regex_validators as server-side RegexValidator instances and adds the HTML pattern / title attributes to the corresponding widgets.
  • Injects form_widget_classes, form_field_placeholders, and form_field_help_texts into widget attributes.
  • For POST requests, binds request.POST and request.FILES to the form.
  • Disables fields that fall outside the current user’s get_editable_fields() result.

save_model(request, obj, form, change: bool)

Called after form.is_valid() on both the add and edit views. Use this hook to run custom pre- or post-save logic.
ParameterTypeDescription
requestHttpRequestThe current request.
objModel instanceThe unsaved model instance (form data already applied).
formModelFormThe validated form instance.
changeboolFalse on add, True on edit.
The default implementation auto-slugifies the slug field from title, name, or headline (in that priority) if slug is blank, then calls obj.save().
def save_model(self, request, obj, form, change: bool):
    obj.author = request.user.get_username()
    super().save_model(request, obj, form, change)

delete_model(request, obj)

Called when a delete is confirmed. Override to add cascading cleanup, audit logging, or soft-delete logic. The default implementation calls obj.delete().
def delete_model(self, request, obj):
    # Archive instead of hard-delete
    obj.status = "archived"
    obj.save()

get_card_buttons(request) → List[Dict[str, Any]]

Returns the list of button dicts rendered on the dashboard card. When card_buttons is empty, it auto-generates a View List button (if the user has list permission) and an Add New button (if the user has add permission). When card_buttons is set, each button with an action key of "add" or "list" has its URL resolved automatically — other button dicts are passed through unchanged.

get_column_header(field_name: str) → str

Returns the display label for a list view column. For a real model field, returns field.verbose_name.title(). For a traversal like "category__name", the lookup is done on the first segment. For unmapped names (computed properties, etc.), the field name is formatted by replacing underscores and double-underscores with spaces/arrows and title-casing.

verbose_name (property)

Returns self.model._meta.verbose_name. Read-only.

verbose_name_plural (property)

Returns self.model._meta.verbose_name_plural. Read-only.

Full Example

# myapp/var_cms_admin.py
from var_cms.registry import var_cms_site, VarCMSModelAdmin
from var_cms.permissions import RolePermission, UserPermission
from .models import Article


class ArticleAdmin(VarCMSModelAdmin):
    # ── List view ──────────────────────────────────────────────────
    list_display     = ["title", "category", "author", "status", "created_at"]
    list_filter      = ["status", "is_featured", "category"]
    search_fields    = ["title", "body", "author"]
    ordering         = ["-created_at"]
    list_per_page    = 20
    list_select_related = True

    # ── Form ───────────────────────────────────────────────────────
    readonly_fields  = ["created_at", "updated_at", "view_count"]
    html_fields      = ["body"]
    regex_validators = {
        "slug": (r"^[a-z0-9-]+$", "Slug must only contain lowercase letters, numbers, and hyphens."),
    }
    form_field_widths = {
        "title":    "two-thirds",
        "status":   "one-third",
        "category": "half",
        "author":   "half",
    }
    form_field_rows = [
        ["first_name", "last_name"],
    ]
    form_field_placeholders = {
        "title": "Enter article title...",
    }
    form_field_help_texts = {
        "body": "Full article content — Quill editor is active.",
    }

    # ── Permissions ────────────────────────────────────────────────
    permissions = [
        RolePermission("superuser", add=True,  list=True, view=True, edit=True,  delete=True),
        RolePermission("editor",    add=True,  list=True, view=True, edit=True,  delete=False),
        RolePermission("author",    add=True,  list=True, view=True, edit=True,  delete=False),
        RolePermission("viewer",    add=False, list=True, view=True, edit=False, delete=False),
        UserPermission("alice",     add=True,  list=True, view=True, edit=True,  delete=True),
    ]
    role_editable_fields = {
        "superuser": "__all__",
        "editor":    ["title", "slug", "body", "status", "category", "is_featured"],
        "author":    ["title", "body", "status"],
        "*":         [],
    }

    # ── Display ────────────────────────────────────────────────────
    icon            = "file-text"
    dashboard_card  = True
    card_buttons    = [
        {"label": "All Articles", "action": "list"},
        {"label": "Write Draft",  "action": "add"},
    ]
    custom_object_actions = [
        {
            "name":      "publish",
            "label":     "Publish",
            "class":     "btn-green",
            "icon":      "check-circle",
            "action_fn": "publish_article",
        }
    ]

    def publish_article(self, request, obj):
        obj.status = "published"
        obj.save()
        return None   # redirect back to referer


var_cms_site.register(Article, ArticleAdmin)

Build docs developers (and LLMs) love