This page explains how DjangoLux fits into a Django project and how to think about its moving parts before you start extending it.
DjangoLux is a Django app that combines seven layers:
- runtime configuration
- generic discovery and generation
- global patches for translation and scope behavior
- reusable templates, views, and JavaScript for internal system workflows
- audit and governance infrastructure
- data-movement and productivity utilities
- generated-Compose deployment and verified package activation
If you keep those layers in mind, the package becomes much easier to extend without fighting it.
The runtime configuration comes from get_system_config() and is merged in this order:
- package defaults
settings.DLUX_CONFIG- the database-backed
SystemSettingssingleton
Practical implications:
- use
DLUX_CONFIGfor project-owned defaults checked into source control - use
SystemSettingsfor live runtime edits from the UI - expect the final resolved configuration to be the merged view, not one single source
The main system-level models are:
-
SystemSettingsStores branding plus grouped runtime policy for language, themes, typography, authentication, email, registration, notifications, navigation, logging, and the profile/onboarding surface. -
ScopeandScopeSettingsRepresent the optional scope-isolation system and whether scoping is globally enabled.ScopeSettings.auto_create_user_scopeenables automatic creation of a dedicatedScopefor every newly registered user, providing automatic user isolation without manual assignment. -
ScopedModelGives inheriting models audit fields, actor tracking, soft-delete behavior, and automatic scope support. -
ProfileExtends the user model with phone, profile picture, preferences, and 2FA state. Profiles are created automatically. -
ActivityLog,DluxNotification*, and backup/restore run models Persist the audit, user-delivery, report, and recovery control planes. -
DluxUpdateStateandDluxUpdateRunPersist the active/baked/previous release state and serialized check/apply/ rollback history for generated Compose deployments. They do not make Dlux an out-of-process Django app; the active release is still imported in-process.
Inheriting from ScopedModel is the main way to make a model feel native inside DjangoLux.
from django.db import models
from dlux.models import ScopedModel
class Department(ScopedModel):
name = models.CharField(max_length=100)What you get automatically:
scopecreated_atupdated_atcreated_byupdated_bydeleted_atdeleted_by
Behavior to remember:
save()auto-assigns actor fields from the current request usersave()can also inherit scope from the current user's profiledelete()becomes a soft-deleteobjectsis scope-aware and hides soft-deleted rowsall_objectsis the raw escape hatch- When
ScopeSettings.auto_create_user_scopeis enabled, new users automatically receive their own dedicated scope
DluxConfig.ready() applies the package's global behavior at startup. That includes:
- permission-label translation
- automatic scope handling for forms, filters, and tables
- automatic translation patches for forms, filters, tables, and some context-menu labels
That is why many dlux features feel "automatic" even when your model or form code looks ordinary.
When DluxConfig.ready() runs at startup, it applies several global monkey-patches to Django's core components to enable the "Zero-Boilerplate" experience.
Through dlux.patches.apply_scoped_patches(), the system intercepts the __init__ methods of ModelForm, FilterSet, and Table (from django-tables2).
- Discovery: It checks if the underlying model inherits from
ScopedModel. - Injection: If so, it automatically adds a
scopefield/filter/column if one hasn't been explicitly defined or excluded. - Access Control: It locks the field for non-superusers, ensuring they cannot change their assigned scope.
Through apply_global_translation_patches(), the system:
- Monkey-patches
gettext: Every call to Django's translation utilities (including_()) first checks theDLUX_STRINGSdictionary and runtime overrides before falling back to local PO files. - Translates Model Meta: Automatically wraps
verbose_nameandverbose_name_pluralof ALL models in the project with lazy translators that look up keys indlux(model_<name>/models_<name>). Each falls back to the other key before the raw name, so a model translated with only one key still resolves on every surface (sidebar, nav, section manager). The canonical helper isdlux.translations.resolve_model_label(model)— the single entry point components share so they always agree on the label (order: plural → singular → raw). - Translates Permissions: Dynamic labels in the user management UI are generated by translating the "Can add/view/delete" strings in real-time.
If LOGIN_REDIRECT_URL or LOGOUT_REDIRECT_URL are missing from your settings.py, DjangoLux injects its own defaults to ensure a smooth out-of-the-box flow.
A few practical consequences:
- you usually do not add
scopemanually to every form and filter - translated labels often come from verbose names or translation keys without manual wiring
- opting out is explicit, such as excluding
scopein form metadata when needed
DjangoLux leans heavily on naming conventions and runtime discovery.
The generic class resolver looks for model-adjacent classes in this order:
- convention-based imports such as
DepartmentForm,DepartmentTable, andDepartmentFilter - explicit model methods that return classes
- explicit dotted-path model methods
- runtime auto-generation
That same discovery model powers both sections and dynamic modal flows.
It also connects to the surrounding UI systems:
- sidebar discovery for runtime navigation
- sidebar permission inference (see below)
- context-menu actions attached to generated tables
- autofill metadata attached to generated or patched form fields
- generic modal/list/detail views that expect convention-friendly classes
Sidebar items are only visible to users who have the required view permission. The permission for each item is inferred in this order:
- Explicit decorator:
sidebar_permissionsorpermission_requiredon the view callback — used as-is. - System route meta: items in
SYSTEM_ROUTE_META(e.g.,manage_users,options_view) use their declared__dlux_*permission tokens. - Model-based inference: for class-based views with a model, the permission is
app_label.view_model_name. - URL pattern inference: for function-based views without a model, the app label comes from the URL namespace (or callback module) and the model name from the URL name prefix (e.g.,
documents:outgoing_list→documents.view_outgoing). - No inference: if none of the above produce a permission, the item is hidden from non-superusers.
This means staff users will only see sidebar items for models they have explicit app.view_model permissions for. Ensure users are granted the appropriate permissions.
Use sections when:
- the model is a simple auxiliary or lookup dataset
- you want a list + filter + modal CRUD flow with minimal code
- the model belongs in the system navigation automatically
Use dynamic modals when:
- the CRUD flow should be embedded inside another screen
- you need form-only, list-only, or read-only modal behavior
- the model should be managed from a custom trigger instead of the sections page
In both cases, the discovery system is the same. What changes is the entry point and the surrounding UI.
The dynamic modal keeps its header and footer fixed while only the body scrolls. Built-in action bars (auto-form buttons, the System Settings wizard bar, and _build_submit_actions bars) are detected and moved into the pinned footer automatically.
For a custom modal template (a view that sets template_name, or a custom options screen), mark your own button container with data-dlux-modal-footer to have it pinned the same way:
<form method="post" action="{{ request.path }}">
{% csrf_token %}
{% crispy form %}
<!-- This bar is lifted into the sticky modal footer automatically -->
<div class="d-flex justify-content-end gap-2" data-dlux-modal-footer>
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</form>Notes:
- The marked container takes priority over the built-in bars.
- Submit buttons inside it are auto-associated to the modal's form via the
form=attribute, so the modal's AJAX submit interception still fires even though the button now lives in the footer. - The element is physically moved out of the modal body, so any custom JS for those buttons should use document-level event delegation rather than querying the modal body after load.
- A container that includes multi-step wizard navigation (
.dlux-btn-next/.dlux-btn-prev) is intentionally left in the body so the wizard controller can manage it.
The user side of DjangoLux has a few important defaults:
- every user gets a
Profile - preferences and 2FA state live on that profile
- the user-management flow uses the interactive modal wizard
- permission labels are translated dynamically instead of staying in raw Django English
If you extend user-facing workflows, treat Profile as part of the normal user contract rather than an optional extra.
Two recent themes in DjangoLux are worth treating as first-class features, not side effects:
- translations are resolved with layered fallbacks, including user preferences, session state, config defaults, and runtime overrides
- scope behavior is auto-injected when enabled and removed when disabled
That means you should usually extend the system by leaning into those mechanisms rather than rebuilding them locally in each form, table, or view.
DjangoLux also includes a few subsystems that make it feel larger than a normal "app with some templates":
- Activity logging: the system captures login/logout, CRUD, merged User/Profile changes, diffs, and download/export events through signals and shared logging helpers.
- Context menus:
generated or custom tables can emit URL actions or decoupled
dlux:record:*events, which lets the UI layer stay interactive without hard-wiring custom JavaScript everywhere. - Fetch/export helpers:
fetch_file()andfetch_excel()provide package-level download/export behavior instead of every project reimplementing file handling and Excel generation. - Productivity helpers: autofill, sticky forms, filter setup, and reusable list templates push common back-office UX patterns into shared infrastructure.
That cluster of subsystems is a big part of why DjangoLux should be treated like an internal platform layer, not just a widget library.
If you are wiring a normal list screen today, the supported path is:
- extend
dlux/list_base.html - call
setup_filter_helper()oradvanced_filter_helper()in the view - render the table with
{% render_table table %}and do not wrap it in another.table-responsive - prefer
dlux.tables.DluxTablefor handwritten tables
The current table contract is:
- stock
django_tables2templates and no-template tables are auto-adopted into the Dlux renderer - explicit non-stock custom templates are left alone unless you point them at the Dlux template yourself
- density precedence is
Table.Meta.dlux_density->Profile.preferences["table_density"]->SystemSettings.default_table_density->balanced - page-size precedence is
Table.Meta.dlux_per_page-> requestper_page->Profile.preferences["table_page_size"]->20 - built-in per-page controls and the density switcher live in the framework-owned footer
- tables with forced
dlux_densityintentionally hide the footer density switcher - default row actions are
dlux:record:view,dlux:record:edit, anddlux:record:delete - disable default row actions with
Meta.dlux_actions = False - customize the action list with
get_dlux_row_actions(self, record, base_actions)
Typical handwritten table:
from dlux.tables import DluxTable
class InvoiceTable(DluxTable):
class Meta(DluxTable.Meta):
model = Invoice
fields = ("number", "customer", "status", "created_at")
dlux_density = "balanced"
dlux_per_page = 50Filter pages also have a clearer contract now:
setup_filter_helper()andadvanced_filter_helper()default to inline placeholder labels for filter bars- if you want normal external labels instead, pass
inline_labels=False - if a page cannot extend
dlux/list_base.html, includedlux/forms/filter_assets_head.html
For the full contract and more examples, use:
- Use the Customization Guide when you are ready to wire your own translations, sections, modals, or template overrides.
- Use the Reference when you need commands, endpoints, template tags, or helper names quickly.