{% 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 %}

🚀Installation & Setup

1. Install

pip install django-var-cms pillow whitenoise

2. Add to INSTALLED_APPS

INSTALLED_APPS = [
    ...
    "var_cms",
]

3. Add URLs

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)

4. Set login redirect

LOGIN_URL          = "/var-cms/login/"
LOGIN_REDIRECT_URL = "/var-cms/"

⚙️settings.py Configuration

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"
💡 All VAR_CMS_* settings are optional. Built-in defaults will be used for any key you omit.

📚Register Models

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)

🔒How Roles Work

Roles in django-var-cms map directly to Django Groups. A user's group name becomes their role name inside the CMS.

ConceptDjango EquivalentHow it's used
RoleGroupThe group name string is matched against RolePermission("role_name", ...)
Superuseris_superuser=TrueAlways resolves to the "superuser" role
Per-user overrideUsername matchUserPermission("alice", ...) matches by username exactly
⚠️ If a user belongs to multiple groups, the first group that matches a defined permission role is used.

➕Creating Roles (Groups)

A "role" is simply a Django Group. Create them via Django Admin:

  1. Go to /admin/auth/group/add/
  2. Enter a name, e.g. manager
  3. Save (no need to assign Django permissions — var-cms handles its own)
  4. Reference the same name in your RolePermission
from 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),
]
💡 You can also create groups programmatically in a migration or management command:
Group.objects.get_or_create(name="manager")

🤝Assigning Users to Roles

Assign a user to a group in Django Admin:

  1. Go to /admin/auth/user/ → click a user
  2. Scroll to Groups section
  3. Select your group (e.g. manager) and save

That user will now have the permissions you defined in RolePermission("manager", ...).

Per-user override (no group needed)

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
]

🔒Security

Authentication

  • All CMS views require login — unauthenticated users are redirected to the login page.
  • Optional OTP 2FA (email-based 6-digit code) via VAR_CMS_ENABLE_OTP = True.
  • Passwords are hashed using Django's standard PBKDF2 algorithm.

Authorization

  • Every view (add / list / view / edit / delete) checks permission before rendering.
  • Unauthorized access raises PermissionDenied (HTTP 403), never silently redirects.
  • Edit button is shown as disabled (not hidden) in the list view so users understand the restriction.
  • Field-level: role_editable_fields restricts which fields each role may modify on the form.
  • readonly_fields are always non-editable regardless of role.

CSRF & XSS

  • All forms include Django's CSRF token.
  • All output is auto-escaped by Django's template engine. Only explicitly marked safe values (file previews, icons) bypass escaping.

File Uploads

  • Files are validated by Django's ImageField / FileField.
  • Uploads go to your configured MEDIA_ROOT — never executed as code.
  • Image crop and convert endpoints require login and operate only on files already in MEDIA_ROOT.
⚠️ Always run with DEBUG = False in production and set a strong SECRET_KEY. Use HTTPS to protect sessions.

💻Form Column Widths

Use form_field_widths to set how wide each field is in the 12-column grid.

ValueColumnsTypical use
"full"12 / 12Textarea, rich text, addresses
"half"6 / 12Default for most fields
"one-third"4 / 12Status, category, short codes
"two-thirds"8 / 12Title, description
"one-fourth"3 / 12Compact numeric fields
"three-fourths"9 / 12Wide + companion narrow
class InvoiceAdmin(VarCMSModelAdmin):
    form_field_widths = {
        "title":  "two-thirds",
        "status": "one-third",
        "notes":  "full",
    }

💻Row Grouping

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.

✏️Placeholders & Help Text

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."),
    }

📊Dashboard Cards

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 or hide from dashboard

# 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

Globally in settings.py

# 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

Custom card buttons

class InvoiceAdmin(VarCMSModelAdmin):
    card_buttons = [
        {"label": "All Invoices", "action": "list"},
        {"label": "New Invoice",  "action": "add"},
        {"label": "Reports",      "url": "/reports/", "class": "btn-ghost"},
    ]

✨Branding

# 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%.

Live Accent Color Theme Preview

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.

Presets:
Custom: 250, 95%, 72%
VAR_CMS_ACCENT_COLOR = "250, 95%, 72%"

🔗Custom Object Actions

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:

  • name: Unique string identifier for the action.
  • label: Display text on the button.
  • class (optional): CSS class for styling, e.g., "btn-primary", "btn-green", "btn-blue", "btn-danger", or "btn-ghost".
  • icon (optional): Name of a Lucide icon (e.g. "check-circle", "mail").
  • action_fn: A callable or method name on the ModelAdmin class. Signature: 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.

Example 1: Approve Company (with business logic checking)

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

Example 2: Send Welcome Email to Subscriber

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
{{ var_cms_site.developer_name }}
{{ var_cms_site.developer_website }}  ·  {{ var_cms_site.developer_email }}
{% if var_cms_site.developer_github %} GitHub {% endif %} {% if var_cms_site.developer_linkedin %} LinkedIn {% endif %}
{% endblock %} {% block extra_js %} {% endblock %}