{% extends "var_cms/base.html" %} {% load var_cms_tags %} {% block title %}Help & Documentation — {{ var_cms_site.site_header }}{% endblock %} {% block breadcrumb %} › Help & Documentation {% endblock %} {% block extra_head %} {% endblock %} {% block content %}

pip install django-var-cms pillow whitenoise
INSTALLED_APPS = [
...
"var_cms",
]
from django.urls import path, include
from django.conf.urls.static import static
urlpatterns = [
path("var-cms/", include("var_cms.urls", namespace="var_cms")),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
LOGIN_URL = "/var-cms/login/" LOGIN_REDIRECT_URL = "/var-cms/"

Put all global CMS config in your settings.py using the VAR_CMS_ prefix. Your var_cms_admin.py files should only contain model registrations.
# settings.py VAR_CMS_SITE_HEADER = "My App" # Top-left brand name VAR_CMS_SITE_SUB = "ADMIN PANEL" # Subtitle under brand VAR_CMS_SITE_URL = "https://myapp.com" VAR_CMS_LOGO_URL = "/static/logo.png" VAR_CMS_ACCENT_COLOR = "250, 95%, 72%" # HSL e.g. purple VAR_CMS_ENABLE_OTP = False # True to require email OTP on login VAR_CMS_USERNAME_FIELD = "email" # if your user model uses email as login # Dashboard card visibility (optional) VAR_CMS_HIDDEN_DASHBOARD_CARDS = ["logentry", "demo.category"] VAR_CMS_SHOWN_DASHBOARD_CARDS = ["invoice", "customer"] # show ONLY these # Developer credits shown in Help page VAR_CMS_DEVELOPER_NAME = "Your Name" VAR_CMS_DEVELOPER_EMAIL = "you@example.com" VAR_CMS_DEVELOPER_WEBSITE = "https://example.com" VAR_CMS_DEVELOPER_GITHUB = "https://github.com/yourname" VAR_CMS_DEVELOPER_IMAGE = "https://example.com/avatar.png"


Create var_cms_admin.py in any app. It is auto-discovered on startup. Only put model-related config here.
from var_cms.registry import var_cms_site, VarCMSModelAdmin
from var_cms.permissions import RolePermission, UserPermission
from .models import Invoice, Customer
class InvoiceAdmin(VarCMSModelAdmin):
# ── List view ─────────────────────────────────────────────
list_display = ["number", "customer", "amount", "status", "created_at"]
list_filter = ["status"]
search_fields = ["number", "customer__name"]
ordering = ["-created_at"]
list_per_page = 30
icon = "file-invoice" # Lucide icon name
# ── Always read-only ──────────────────────────────────────
readonly_fields = ["created_at", "updated_at"]
# ── Hide from form entirely ───────────────────────────────
exclude_fields = ["internal_notes"]
# ── Permissions ───────────────────────────────────────────
permissions = [
RolePermission("superuser", add=True, list=True, view=True, edit=True, delete=True),
RolePermission("manager", 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),
]
# ── What each role can edit ────────────────────────────────
role_editable_fields = {
"superuser": "__all__",
"manager": ["status", "amount", "due_date"],
}
var_cms_site.register(Invoice, InvoiceAdmin)
var_cms_site.register(Customer)

Roles in django-var-cms map directly to Django Groups. A user's group name becomes their role name inside the CMS.
| Concept | Django Equivalent | How it's used |
|---|---|---|
| Role | Group | The group name string is matched against RolePermission("role_name", ...) |
| Superuser | is_superuser=True | Always resolves to the "superuser" role |
| Per-user override | Username match | UserPermission("alice", ...) matches by username exactly |


A "role" is simply a Django Group. Create them via Django Admin:
managerRolePermissionfrom var_cms.permissions import RolePermission
permissions = [
RolePermission("manager", add=True, list=True, view=True, edit=True, delete=False),
RolePermission("accountant", add=False, list=True, view=True, edit=False, delete=False),
]

Group.objects.get_or_create(name="manager")

Assign a user to a group in Django Admin:
manager) and saveThat user will now have the permissions you defined in RolePermission("manager", ...).
If you want to grant one specific user extra access independently of their group, use UserPermission:
from var_cms.permissions import UserPermission
permissions = [
RolePermission("viewer", add=False, list=True, view=True, edit=False, delete=False),
UserPermission("alice", add=True, list=True, view=True, edit=True, delete=True),
# alice gets full access even though she may be in the viewer group
]

VAR_CMS_ENABLE_OTP = True.add / list / view / edit / delete) checks permission before rendering.PermissionDenied (HTTP 403), never silently redirects.role_editable_fields restricts which fields each role may modify on the form.readonly_fields are always non-editable regardless of role.ImageField / FileField.MEDIA_ROOT — never executed as code.MEDIA_ROOT.
DEBUG = False in production and set a strong SECRET_KEY. Use HTTPS to protect sessions.

Use form_field_widths to set how wide each field is in the 12-column grid.
| Value | Columns | Typical use |
|---|---|---|
"full" | 12 / 12 | Textarea, rich text, addresses |
"half" | 6 / 12 | Default for most fields |
"one-third" | 4 / 12 | Status, category, short codes |
"two-thirds" | 8 / 12 | Title, description |
"one-fourth" | 3 / 12 | Compact numeric fields |
"three-fourths" | 9 / 12 | Wide + companion narrow |
class InvoiceAdmin(VarCMSModelAdmin):
form_field_widths = {
"title": "two-thirds",
"status": "one-third",
"notes": "full",
}

Use form_field_rows to place multiple fields side-by-side in one visual row. Fields in the same row split the width equally.
class CustomerAdmin(VarCMSModelAdmin):
form_field_rows = [
["first_name", "last_name"], # 2 cols → each takes half
["mobile", "email", "date_of_birth"], # 3 cols → each takes one-third
["city", "state", "country", "pin"], # 4 cols → each takes one-fourth
]

form_field_rows takes priority over form_field_widths for listed fields.

Use form_field_widgets to control the dropdown style per field.
| Value | Renders as | Best for |
|---|---|---|
"select" | Standard HTML select | Short choice lists (default) |
"select_search" | Searchable dropdown | ForeignKey with many options |
"multiselect" | Checkbox list | ManyToManyField |
"multiselect_search" | Checkbox list + search | ManyToManyField with many items |
class OrderAdmin(VarCMSModelAdmin):
form_field_widgets = {
"status": "select", # plain dropdown (default)
"customer": "select_search", # searchable dropdown
"tags": "multiselect_search", # checkbox list with search
}

class ArticleAdmin(VarCMSModelAdmin):
form_field_placeholders = {
"title": "Enter the article headline…",
"slug": "auto-generated-from-title",
}
form_field_help_texts = {
"slug": "URL-safe identifier. Leave blank to auto-generate.",
"body": "Supports full HTML via the Quill editor.",
}
# HTML rich text editor on specific fields:
html_fields = ["body", "description"]
# Regex pattern validation:
regex_validators = {
"slug": (r"^[a-z0-9-]+$", "Only lowercase letters, numbers, hyphens."),
}

By default, dashboard cards are hidden (dashboard_card = False). Set it to True explicitly to show them, or configure globally in settings.py.
# Per model: show on dashboard (default is False, so set True to show)
class InvoiceAdmin(VarCMSModelAdmin):
dashboard_card = True # will appear on dashboard
# Per model: hide from dashboard (default)
class LogAdmin(VarCMSModelAdmin):
dashboard_card = False # won't appear on dashboard
# settings.py # Hide specific cards: VAR_CMS_HIDDEN_DASHBOARD_CARDS = ["logentry", "demo.category"] # Show ONLY these cards: VAR_CMS_SHOWN_DASHBOARD_CARDS = ["invoice", "customer"] # show ONLY these
class InvoiceAdmin(VarCMSModelAdmin):
card_buttons = [
{"label": "All Invoices", "action": "list"},
{"label": "New Invoice", "action": "add"},
{"label": "Reports", "url": "/reports/", "class": "btn-ghost"},
]

# settings.py VAR_CMS_SITE_HEADER = "VAR CMS" VAR_CMS_SITE_SUB = "CONTROL PANEL" VAR_CMS_LOGO_URL = "/static/myapp/logo.png" VAR_CMS_ACCENT_COLOR = "142, 72%, 45%" # Emerald green
Accent color is an HSL triplet (hue, saturation%, lightness%) — e.g. purple is 250, 95%, 72%, teal is 180, 60%, 50%.
Pick a custom color or choose one of our harmonized presets to see the entire CMS theme adapt immediately. Once you find a look you love, copy the configuration snippet below for your settings.py.
VAR_CMS_ACCENT_COLOR = "250, 95%, 72%"

You can add custom action buttons (like "Approve", "Send welcome", "Toggle Active") to both the List View table rows and the Details View page.
Define custom_object_actions as a list of dictionaries on your ModelAdmin class. Each action can specify:
"btn-primary", "btn-green", "btn-blue", "btn-danger", or "btn-ghost"."check-circle", "mail").def action_fn(self, request, obj).If the action function returns None, the page will automatically redirect back to the previous page. You can use Django's messages framework inside the function to display feedback to the user.
class CompanyAdmin(VarCMSModelAdmin):
custom_object_actions = [
{
"name": "approve",
"label": "Approve",
"class": "btn-green",
"icon": "check-circle",
"action_fn": "approve_company"
}
]
def approve_company(self, request, obj):
from django.contrib import messages
if not obj.documents_uploaded:
messages.error(request, f"Cannot approve {obj.name} - documents are missing.")
return None
obj.is_approved = True
obj.save()
messages.success(request, f"Company {obj.name} has been approved.")
return None
class SubscriberAdmin(VarCMSModelAdmin):
custom_object_actions = [
{
"name": "send_welcome",
"label": "Send Welcome",
"class": "btn-blue",
"icon": "mail",
"action_fn": "send_welcome_email"
}
]
def send_welcome_email(self, request, obj):
from django.core.mail import send_mail
from django.contrib import messages
try:
send_mail("Welcome!", f"Hi {obj.first_name}!", "noreply@example.com", [obj.email])
messages.success(request, f"Welcome email sent to {obj.email}.")
except Exception as e:
messages.error(request, f"Failed: {e}")
return None