DjangoPlay — frontend App
frontend (AppConfig.name = "frontend", no custom ready(), no models, no views, no urls.py, no admin.py) is DjangoPlay's shared presentation layer — the single place every other app's HTML, CSS, JS,...
On this page ▾
- 1. Summary
- 2. Architecture
- 2.1 Directory layout (as extracted)
- 2.2 How Django finds these files
- 2.3 The four global context processors
- 2.4 Template composition — base/base.html as the shell
- 2.5 Template Registry — the indirection layer other apps depend on
- 3. Templatetags (the only executable logic in this app)
- 3.1 templatetags/actions.py
- 3.2 templatetags/filter_config_tags.py
- 3.3 templatetags/privacy_filters.py
- 4. Static assets & the manual build pipeline
- 4.1 No JS framework, no bundler-as-a-tool — a hand-written bash pipeline
- 4.2 Vendor assets are deliberately excluded from the bundles
- 4.3 CSS architecture
- 4.4 JS architecture (source files, pre-bundle)
- 5. Third-party vendor libraries (static/vendor/)
- 6. Integration with mailer (email templates)
- 7. Security-relevant observations
- 7.1 Client-side auth token stored in localStorage
- 7.2 Client-side permission gating is cosmetic, not enforcement
- 7.3 mask_email fully hides the domain, not just part of it
- 7.4 Admin-namespace detection for JS bundle loading is a string match on request.path
- 8. Known Gaps / Things Worth Confirming With the Team
- 9. Integration Points (cross-app)
- 10. Quick Reference — Common Tasks
Doc generated by direct inspection of every file in
webapp/frontend/(Python, HTML, CSS, and theminify.shasset pipeline — CSS/JS/image binaries were listed and spot-checked, not fully read line-by-line), plus its wiring points:paystream/app_settings/templates.py,paystream/app_settings/static_media.py,paystream/app_settings/cdn.py,paystream/services/context_processors/*,utilities/context_processors/report_bug.py,utilities/constants/template_registry.py,utilities/admin/base_admin_page.py, andmailer/engine/*.Revision note: this pass adds Cloudflare R2/CDN asset delivery (
dist/,assets/,vendor/now optionally served from R2 instead of the Django box) and supersedes the prior revision's description of the static pipeline as purely local. Seedocs/releases/djangoplay/frontend-r2-requirements.mdfor the original design doc and decision log, anddocs/system/asset-builder-cloudflare-r2.mdfor the brand-asset generator this ships alongside.
1. Summary
- It is registered as the sole entry in both
STATICFILES_DIRSandTEMPLATES[0]['DIRS'](paystream/app_settings/static_media.py,paystream/app_settings/templates.py) — the comment instatic_media.pyis explicit: "Onlyfrontend/staticshould be used now". - Every other app's views (
users,apidocs,helpdesk's issue-tracker integration,teamcentral,industries,locations, etc.) render templates that physically live underfrontend/templates/, resolved indirectly throughutilities.constants.template_registry.TemplateRegistryrather than hard-coded template paths scattered per-app. - It carries three small template-tag modules (
actions.py,filter_config_tags.py,privacy_filters.py) that are the only executable Python in the app — everything else is markup, CSS, and hand-written vanilla JS (no frontend build framework — no React/Vue/webpack; a bespoke bash script (packaging/minify.sh) concatenates and minifies plain<script>-tag JS withterser/cleancss). - It provides the
BaseAdminPage-driven custom Django-admin skin (templates/admin/...) used across nearly every app'sModelAdmin, replacing Django's default admin templates wholesale. - It hosts the HTML/text email templates consumed by
mailerviaTemplateRegistry's email-prefix constants, and the issue-tracker UI templates (templates/integrations/issuetracker/) consumed byhelpdesk'sgenericissuetrackerintegration.
In short: frontend is DjangoPlay's "static/templates app" in the Django sense (comparable to a project-level templates/ + static/ directory, but formalized as its own installed app) plus a thin templatetag layer and a manual JS/CSS bundler — it owns presentation, not business logic, and is depended on by essentially every other app that renders HTML.
2. Architecture
2.1 Directory layout (as extracted)
frontend/
├── __init__.py
├── apps.py # trivial AppConfig — no models, no ready()
├── build/
│ └── minify.sh # manual CSS/JS bundling (cleancss + terser)
├── static/
│ ├── assets/ # source CSS + JS (pre-bundle)
│ │ ├── css/{admin,public}/ # numbered ITCSS-style partials (01_tokens.css … 09_utilities.css)
│ │ └── js/{core,admin,api,components,pages,shared}/
│ ├── dist/ # minify.sh OUTPUT — bundle.min.{css,js}, bundle.admin.min.js, bundle.api.min.js, favicon/logo images
│ └── vendor/ # third-party libs checked into the repo (see §5)
├── templates/
│ ├── account/ # allauth-style auth pages + HTML/text email templates
│ ├── admin/ # full custom Django-admin template override tree
│ ├── apidocs/ # Swagger / ReDoc / API-stats pages
│ ├── base/ # base.html shell + header/footer/scripts/styles partials
│ ├── components/ # small standalone includes (bug report modal)
│ ├── errors/ # single generic error page reused for 401/403/404
│ ├── integrations/issuetracker/ # full UI for the genericissuetracker integration
│ └── support/ # empty directory — see §8 gaps
└── templatetags/
├── actions.py
├── filter_config_tags.py
└── privacy_filters.py2.2 How Django finds these files
paystream/app_settings/templates.py:
TEMPLATES = [{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'frontend' / 'templates'],
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'utilities.context_processors.report_bug.report_bug_context',
'paystream.services.context_processors.site_context.site_context',
'paystream.services.context_processors.site_context.global_urls',
'paystream.services.context_processors.site_context.admin_urls',
'paystream.services.context_processors.app_version.app_version',
],
'loaders': [
('django.template.loaders.cached.Loader', [
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
]),
],
},
}]Because APP_DIRS isn't set and a filesystem.Loader pointed at frontend/templates is listed ahead of app_directories.Loader (wrapped in the caching loader), frontend/templates is checked first for every template lookup in the project, including Django's own admin/*.html names — this is how the app is able to fully replace Django admin's stock templates (admin/base_site.html, admin/change_form.html, admin/submit_line.html) just by shipping files at the matching relative paths.
paystream/app_settings/static_media.py does the equivalent for static assets:
STATICFILES_DIRS = [BASE_DIR / 'frontend' / 'static', admin_static.__path__[0]]
STATICFILES_FINDERS = ['django.contrib.staticfiles.finders.FileSystemFinder']
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'FileSystemFinder-only (no AppDirectoriesFinder) means no other app can ship its own static/ directory and have it picked up — frontend/static plus Django's own admin static path are the only two static roots in the project. ManifestStaticFilesStorage means production collectstatic produces hashed filenames for cache-busting.
2.3 The four global context processors
Every template rendered anywhere in the project (not just frontend's own) gets these variables for free, which is why templates across many apps can reference things like SITE_URL, HOME_URL, or schema_url without each view passing them explicitly:
| Processor | Source | Injects |
|---|---|---|
report_bug_context |
utilities.context_processors.report_bug |
show_bug_report (bool) — gates the bug-report widget (see §2.4.4) based on REPORT_BUG_ENABLED / REPORT_BUG_ENABLED_ON_URLS settings and whether the current view is in the Django-admin namespace |
site_context |
paystream.services.context_processors.site_context |
HOME_URL (resolved from the request's subdomain via a hardcoded SUBDOMAIN_HOME_MAP = {"issues": "issues_ui:list", "docs": "docs", "console": "console_dashboard"}), CURRENT_SUBDOMAIN, SITE_URL, SITE_NAME, WEBSITE_URL, AI_ENABLED, and one <NAME>_URL variable per subdomain (via utilities.admin.url_utils.get_all_subdomains()) |
global_urls |
same module | schema_url (reverse of apidocs:schema, empty string if unresolvable), TURNSTILE_SITE_KEY (Cloudflare Turnstile site key, used in base/scripts.html) |
admin_urls |
same module | console_dashboard_url |
app_version |
paystream.services.context_processors.app_version |
APP_VERSION (from Django settings) |
2.4 Template composition — base/base.html as the shell
Nearly every page-rendering template in the project ultimately extends base/base.html (directly, or via admin/base_site.html → base/base.html). Its structure:
<head>: favicon links,{% include "base/styles.html" %}(vendor CSS with CDNonerrorfallbacks + the builtbundle.min.css), the HTMX<script>tag (also with a CDN fallback), then a{% block extra_head %}.<body>carriesdata-authenticated,data-site-url, and conditionallydata-schema-urlattributes — these are read by the vanilla-JS layer (core/app.js,core/api.js) rather than passed through Django template logic at runtime.- Body content:
base/header.html→{% block messages %}(Django messages viabase/django_messages.html) →{% block content_wrapper %}(wraps{% block content %}in a.page-centerdiv) →base/footer.html. - Conditionally authenticated-only includes:
base/support_drawer.html,components/bug_report_modal.html, and (only ifAI_ENABLED)base/chat_widget.html. {% include "base/scripts.html" %}at the very end (vendor JS with CDN fallbacks, the app/admin/api bundles conditionally loaded by path, Cloudflare Turnstile) followed by{% block extra_js %}andbase/partials/offline.html(the offline-monitor UI).
2.4.1 Conditional script/style loading is path-based, not template-based
base/scripts.html and base/styles.html decide whether to load Swagger/ReDoc vendor bundles by inspecting request.resolver_match.url_name (== 'swagger-ui' / == 'redoc'), and whether to load the admin JS bundle by checking '/admin/' in request.path or '/console/' in request.path as a raw string match — not Django's app namespace or a {% block %} override. This is a global include shared by literally every page, so any future URL containing the literal substring /admin/ or /console/ (even outside the real Django-admin namespace) would also trigger the admin JS bundle to load.
2.4.2 Admin skin: admin/base_site.html → admin/layouts/*
admin/base_site.html extends base/base.html, adds the admin-page body class, and renders admin/components/header.html (app label/title breadcrumb) inside a {% block content %}. Three layout templates then extend admin/base_site.html for the three page archetypes used throughout the admin:
admin/layouts/changelist.html— extendsadmin/common/changelist.html; renders the Select2-styled filter bar (see §2.4.3), search box, and the changelist table viaadmin/components/table.html, with adata-features="select2"body attribute that the JS engine (admin/engine/*, §4) reads to know which enhancements to bootstrap.admin/layouts/form.html— the add/change form: tabbed layout driven entirely by aform_layoutcontext variable (a list of{id, label, groups: [{fields: [...]}]}dicts) supplied byBaseAdminPage.get_form_fields()/the per-appform_layoutsconfig (owned byutilities.admin, notfrontend); conditionally addsAuditandHistorytabs (show_audit/show_history) renderingadmin/components/audit_section.htmlandadmin/components/history_section.html.admin/layouts/page.html— a generic "single app landing page" layout (title + optionalapp_icon+page_actions/page_toolbar/page_contentblocks) used for non-model admin pages (e.g.admin/pages/single_app.html).
Several apps override the changelist further under admin/custom/<app>/changelist.html (apidocs, audit, entities, helpdesk, industry, locations (+ a global_region sub-variant), teamcentral, users) — these are app-specific tweaks layered on top of the shared admin/layouts/changelist.html.
2.4.3 AJAX-driven filter bar (admin/common/filters.html)
Documented in-file: any ModelAdmin (in practice, any subclass of utilities.admin.base_admin_page.BaseAdminPage) can set a filter_config dict keyed by parameter_name, each entry declaring "type": "ajax"|"static", "ajax_url", and optionally "parent_param" for cascading filters (country → region → subregion → city, etc.). BaseAdminPage.get_filter_config() / changelist_view() inject this into the template context automatically. frontend/templatetags/filter_config_tags.get_item is the only templatetag this relies on — a null-safe dict lookup so a missing key degrades to {} rather than raising in the template. Django's native filter widgets are still rendered (inside a visually hidden .django-filter-data div) purely so their state/links remain functional; the visible control is a Select2 dropdown populated by JS.
2.4.4 Support/report-bug widgets are global but permission-gated
base/support_drawer.html and components/bug_report_modal.html are included on every authenticated page render (from base.html, unconditional on auth) but report_bug_context further gates visibility of the bug-report entry point specifically (show_bug_report) based on an allow-list of URL names or being inside the admin app namespace — i.e., the drawer markup always ships to authenticated users, but the bug-report trigger is conditionally rendered inside it depending on where they are.
2.5 Template Registry — the indirection layer other apps depend on
utilities/constants/template_registry.py (TemplateRegistry, not part of frontend itself, but the single consumer-facing map to nearly all of frontend/templates/) is how other apps resolve which frontend template to render, instead of hardcoding paths:
CONSOLE_DASHBOARD = "account/site_pages/dashboard.html"
CONSOLE_SIGNIN_FORM = "account/site_pages/signin.html"
APIDOCS_SWAGGER = "apidocs/swagger.html"
ISSUES_LIST = "integrations/issuetracker/issues/list.html"
CHANGE_FORM_TEMPLATE = "admin/layouts/form.html"
CHANGELIST_TEMPLATE = "admin/pages/changelist.html"
ACCOUNT_401 = ACCOUNT_403 = ACCOUNT_404 = "errors/base.html" # same file, three error types
# ...and email-template prefixes, see §6One dynamic exception: TemplateRegistry.get_api_stats_template(user) picks between APIDOCS_STATS_PUBLIC and APIDOCS_STATS_PERSONAL based on user.has_perm("policyengine.view_apidocs_stats") — this is the one spot template selection logic (not just naming) leaks into the registry, and it's a direct policyengine dependency living in utilities.
The 401/403/404 case is notable: all three error types render the exact same errors/base.html file, differentiated purely by context variables (error_title, error_message, error_icon, error_image, error_actions, custom_err_page, app_display_name) passed in by the calling view (e.g. users.views.ui.errors.custom_403, used by BaseAdminPage.add_view() to reject unauthorized add attempts — see the audit app doc §7 for another caller of the same helper).
3. Templatetags (the only executable logic in this app)
3.1 templatetags/actions.py
| Tag/filter | Purpose |
|---|---|
get_action_name(cl, action) |
Wraps Django admin's ChangeList.get_action_name, with a fallback to short_description/str() on failure — used in admin/actions.html (not shown above but referenced from admin/layouts/changelist.html). |
dict_get(d, key) |
Null-safe dict lookup, empty string default. |
region_country_id(region_id) |
Cross-app query from a templatetag: imports locations.models.CustomRegion directly and does a select_related('country') DB lookup to resolve a region's parent country ID. This is a live database call triggered from template rendering — a locations dependency baked directly into frontend. |
field_groups() (simple_tag) |
Returns a hardcoded FIELD_GROUPS dict — basic, organization, employment, personal, metadata — whose field names (sso_provider, sso_id, employment_status, employee_type, hire_date, approval_limit, bank_details, is_verified, …) are unmistakably the users app's User model fields. This is used by admin/components/form_fields.html to group fields into tabs for what is presumably the Users admin form — a hardcoded, users-specific constant living inside a generic app's templatetag module (see §8 gaps). |
get_item(dictionary, key) |
Duplicate-purpose of filter_config_tags.get_item but returns [] (list default) instead of {} — used for a different context shape than the filter config one. |
get_field(form, name) |
form[name] accessor, used heavily in admin/layouts/form.html to dynamically pull a form field by name inside nested {% for %} loops over form_layout. |
get_attr(obj, attr) |
getattr(obj, attr, ""), used to render readonly-field values in the admin form layout. |
3.2 templatetags/filter_config_tags.py
Single filter, get_item, purpose-built (per its own header comment) for admin/common/filters.html's filter_config|get_item:param lookup (see §2.4.3). Deliberately returns {} (not None) on a miss so chained attribute access (cfg.type) degrades gracefully with Django's default filter rather than throwing.
3.3 templatetags/privacy_filters.py
| Filter | Behavior |
|---|---|
mask_email(email) |
j***@*** style masking — keeps first local-part character, masks the rest, and always replaces the domain with *** (i.e. the domain is never shown, not even partially). |
display_name(email) |
Returns the local-part of an email, or "Anonymous" if none. |
avatar_initial(email) |
First character, uppercased, or "?". |
Used in exactly one place found in this pass: templates/integrations/issuetracker/issues/partials/timeline_item.html — i.e. this is specifically for masking user identity in the issue-tracker UI, which (per the helpdesk/genericissuetracker integration) can plausibly be exposed to a wider or less-trusted audience than the internal admin.
4. Static assets & the manual build pipeline
4.1 No JS framework, no bundler-as-a-tool — a hand-written bash pipeline
frontend/packaging/minify.sh is a plain shell script (not a Django management command, not npm run build) that:
- Validates vendor assets exist (
jquery,select2,bootstrap,chartjs,redoc,swagger— see §5) — warns (doesn't fail) if missing. - CSS: concatenates
static/assets/css/admin/admin.css(mandatory —exit 1if missing) andstatic/assets/css/public/public.css(optional) viacleancss --inline=allintostatic/dist/css/bundle.min.css. - Frontend JS bundle (
bundle.min.js): a fixed, explicit ordered list of ~15 files undercore/,shared/,api/,components/,pages/— run throughterser --compress --mangle. Missing files are skipped with a warning rather than failing the build. - Admin JS bundle (
bundle.admin.min.js): a strictly ordered list, with the ordering constraint spelled out in comments —admin/engine/context.jsandadmin/engine/registry.jsmust load first (they definewindow.AdminRegistry), feature files load in the middle (window.initAllSelect2,window.initAllFilters, etc.), andadmin/engine/admin-engine.jsmust load last (it fires the registry onDOMContentLoaded). The comment explicitly warns against double-loading these engine files as standalone<script>tags in templates, since that re-triggers Select2 initialization and produces a"destroy is not a function"crash. - API JS bundle (
bundle.api.min.js): Swagger/ReDoc helpers, session handling, API-stats charting. - A large commented-out earlier version of the same script is left at the bottom of the file (dead code — mirrors a pattern also seen in
audit/apps.pyper theauditdoc).
4.2 Vendor assets are deliberately excluded from the bundles
Per minify.sh's own header comment, vendor libraries (jQuery, Select2, Bootstrap, Chart.js, ReDoc, Swagger UI) live under static/vendor/ and are loaded as separate <script>/<link> tags directly from templates (base/scripts.html, base/styles.html), each with an onerror fallback to a public CDN (jsDelivr / cdnjs / unpkg). Stated rationale: avoids re-bundling third-party code, simplifies CDN fallback, improves browser caching, eases offline dev, and avoids minifier corruption of vendor code.
4.3 CSS architecture
static/assets/css/admin/ follows a numbered ITCSS-ish convention: 01_tokens.css → 02_base.css → 03_layout.css → 04_components/* (alerts, badges, buttons, cards, header, loader, toolbar, and two changelist/changeform "select" variants) → 05_forms/* → 06_tables/* → 07_pages/* → 08_space_themes/* (a themed variant of the changeform/changelist styles) → 09_utilities.css, rolled up by admin.css. static/assets/css/public/ mirrors a similar but flatter structure for non-admin pages, including a dedicated 07_pages/api_login.css / api_stats.css.
4.4 JS architecture (source files, pre-bundle)
core/— foundational, framework-free JS:app.js,api.js(awindow.apifetch wrapper — see §7.1 for a security note),auth.js(login form handler),permissions.js(client-side permission-based DOM hiding — see §7.2),toast.js,timezone.js.admin/— the custom admin-page JS engine:engine/{context.js, registry.js, admin-engine.js}(the bootstrap trio described in §4.1),core.js,form.js,changelist-ui.js,form-options.js,filter-options.js,table-truncate.js, plus per-app feature files:admin/industry/form-options.js,admin/teamcentral/form-options.js,admin/audit/filter-options.js, andadmin/invoices/{invoice-form-options.js, invoice-gst.js, invoice-line-items.js, invoice-totals.js}.api/—session.js(564 lines — the largest single JS file in the app; builds/injects a session-expired modal dynamically, per comments inapidocs/swagger.html/redoc.html),swagger.js,redoc.js,apistats_chart.js.components/—space.js(+ a stray duplicatespace copy.js, see §8),banners.js,django_messages.js,file_upload.js,support.js,support_drawer.js(455 lines),chat_widget.js(233 lines, AI chat UI),reportbug.js.pages/—login.js,password_reset.js,dashboard.js,modal_signin.js.shared/—offline-monitor.js.
5. Third-party vendor libraries (static/vendor/)
Checked directly into the repo (not npm/pip-managed): bootstrap, chartjs, fontawesome, fonts, htmx, jquery, redoc, select2, swagger. Every vendor <script>/<link> tag in base/scripts.html / base/styles.html has an inline onerror CDN fallback (jsDelivr for Bootstrap/Select2/ReDoc, cdnjs for Font Awesome, unpkg for HTMX). This means the app can degrade gracefully to public CDNs if the vendored copy is missing or corrupted, but it also means a production deployment with network egress restrictions to those CDNs would only notice vendor-asset breakage as a silent fallback failure, not a hard error.
6. Integration with mailer (email templates)
account/email/*.html (HTML bodies) and account/fallback/*.txt (plaintext bodies + *_subject.txt subject fallbacks) are the templates mailer resolves through TemplateRegistry's prefix constants (EMAIL_SIGNUP_SUCCESS, EMAIL_VERIFICATION_MANUAL, EMAIL_SSO_ONBOARDING, REQUEST_TO_SUPPORT_EMAIL, CONFIRMATION_TO_USER_EMAIL, PASSWORD_RESET_EMAIL). Confirmed via direct references in mailer/engine/engine.py, mailer/engine/templates.py, mailer/flows/bug.py, mailer/flows/password_reset.py, and mailer/flows/support.py, all of which import TemplateRegistry as T from utilities.constants.template_registry. mailer/engine/base.py calls adapter.send_mail(template_prefix, to_email, context) — the "adapter" pattern reads as django-allauth-compatible (the code comment in paystream/app_settings/templates.py notes PASSWORD_RESET_EMAIL's subject-template path is dictated by allauth's own resolution rules, not TemplateRegistry's general convention).
Shared HTML-email partials (account/email/partials/: attachments.html, button.html, info_box.html, info_box_block.html, link_box.html, message_box.html, unsubscribe_footer.html) are composed into the top-level email templates and driven by base/base_email.html — a separate, self-contained shell from base/base.html (email HTML can't rely on the app's CSS bundle or JS at all, so it's inline-styled independently).
7. Security-relevant observations
7.1 Client-side auth token stored in localStorage
core/auth.js's login handler and core/api.js's window.api.headers() store and read a JWT access token via localStorage.getItem('token') / setItem('token', ...), sent as Authorization: Bearer <token>, in addition to cookie-based session auth (credentials: 'include' on every fetch, plus X-CSRFToken). core/permissions.js's loadUser() explicitly tries the JWT path first (/api/v1/users/auth/me/jwt/) and falls back to the session endpoint (/api/v1/users/auth/me/) if no token is present or the JWT call fails. Storing bearer tokens in localStorage (vs. an httpOnly cookie) is a well-known XSS-exposure pattern — any script-injection vulnerability anywhere on the same origin can read localStorage and exfiltrate the token. This is a frontend/users-boundary concern worth flagging to whoever owns the users JWT issuance flow.
7.2 Client-side permission gating is cosmetic, not enforcement
core/permissions.js hides any element carrying a data-perm="<code>" attribute if the loaded user's permissions array (from /api/v1/users/auth/me/) doesn't include that code. This is the concrete implementation of the "for UI, utilities handles it" access model referenced by the project's access-control split (policyengine for webapp/API authorization; this data-perm DOM-hiding mechanism for UI). It is presentation-only — removing a DOM node client-side has no bearing on whether the underlying API call the element would have triggered is actually authorized; that enforcement has to happen server-side (in policyengine or the view itself) regardless of whether the UI shows the control. This isn't a bug — it's a standard "hide, don't rely on hiding" UI pattern — but it's worth stating explicitly in any AI-training-grade doc so the two layers aren't conflated.
7.3 mask_email fully hides the domain, not just part of it
Unlike typical email-masking (j***@e***.com), privacy_filters.mask_email replaces the entire domain with a fixed *** — a stronger masking choice appropriate for a public-facing surface (the issue tracker), consistent with audit's SENSITIVE_KEYS redaction philosophy.
7.4 Admin-namespace detection for JS bundle loading is a string match on request.path
As noted in §2.4.1, '/admin/' in request.path or '/console/' in request.path is a substring check, not a namespace check — low risk in practice (it only controls whether extra admin JS loads, not an authorization decision), but notably looser than the rest of the codebase's pattern of resolving via request.resolver_match.
8. Known Gaps / Things Worth Confirming With the Team
static/dist/js/bundle.support.min.jsexists on disk but is produced by nothing.minify.shonly ever writesbundle.min.css,bundle.min.js,bundle.admin.min.js,bundle.api.min.js— there is noDIST_SUPPORT_JSvariable or build step for it anywhere in the current or the commented-out legacy version of the script. A repo-wide search found zero template references tobundle.support.min.jseither. It's orphaned build output — either a leftover from a build run against an older version ofminify.sh, or a manually-created file that was never wired in.templates/support/is an empty directory. No files, tracked only as an empty path. Either a placeholder for planned content or a leftover from a refactor (thesupportbundle above and this empty dir are plausibly related — e.g. a "support ticket UI" that was pulled out).components/space copy.js— an evident accidental duplicate ofcomponents/space.js, committed with a space in the filename. Not referenced byminify.sh's explicit file lists (which namespace.js, not the copy) — dead weight, not a functional bug, but worth deleting.invoicesadmin JS files are never bundled.static/assets/js/admin/invoices/{invoice-form-options.js, invoice-gst.js, invoice-line-items.js, invoice-totals.js}exist on disk butminify.sh'sADMIN_JSarray does not include them (it listsindustry,teamcentral, andauditfeature files, but notinvoices) — meaning, unless something else loads these files independently (no such reference was found in the templates read for this pass), the invoices admin UI's JS enhancements are not shipped inbundle.admin.min.jsat all. Worth confirming with whoever ownsinvoices' admin whether these files are legacy/superseded or a build-script gap that's silently breaking invoice admin form behavior.FIELD_GROUPSintemplatetags/actions.pyis a hardcoded, app-specific (users) constant living in a generic, shared templatetag module, with no apparent per-app override mechanism — if any other app needed the same tabbed-field-grouping pattern for its own admin form, there's no indication from this pass of how it would supply its own groups (field_groups()takes no arguments and returns the one fixed dict). Confirm with whoever ownsusers' admin form (and check theusersapp doc) whether this is meant to move into a per-model config eventually.templatetags/actions.py:region_country_idperforms a live synchronous DB query (CustomRegion.objects.select_related('country').get(...)) from within template rendering, and importslocations.modelsdirectly at call time (not module level, presumably to dodge an app-loading-order circular import). This is a working pattern, not a bug, but it does mean alocationsapp change toCustomRegion's schema could silently breakfrontendtemplate rendering wherever this filter is used, and it's a real per-row query cost if used inside a loop over many rows.- Two DB round-trip risks in filter tags are not caching-aware —
region_country_idhas no caching layer; if used per-row in a large changelist it would generate N queries. Not confirmed either way whether it's actually used in a loop anywhere in the currently-read template set (no direct usage site was found in this pass — it may be dead code; flagged as a gap in coverage rather than a confirmed bug). minify.shhas a large fully-commented-out legacy copy of itself appended at the bottom of the file (mirrors the same dead-code pattern noted inaudit/apps.pyin theauditdoc) — harmless, but worth a cleanup pass.- Admin-bundle-loading detection via raw path substring matching (§7.4/§2.4.1) rather than namespace/URL-name resolution — low risk, but inconsistent with the rest of the app's pattern (e.g. Swagger/ReDoc detection correctly uses
request.resolver_match.url_name). - No
frontend/tests.pyor test directory was found in this app during this pass — for an app carrying non-trivial cross-app template-tag logic (a live DB query, a hardcoded field-grouping contract other apps' forms depend on), this is a coverage gap worth flagging, similar to the one noted in theauditapp doc.
9. Integration Points (cross-app)
| App | How it touches frontend |
|---|---|
paystream |
Owns the settings wiring that makes frontend/templates and frontend/static the project's only template/static roots (app_settings/templates.py, app_settings/static_media.py), and owns three of the five global context processors (site_context, global_urls, admin_urls, app_version) that every frontend template can use. |
utilities |
Owns TemplateRegistry (the indirection layer nearly every other app uses to reference a frontend template by name), BaseAdminPage (drives admin/layouts/* and the filter-config context), the report_bug_context processor, and get_all_subdomains() (feeds site_context's per-subdomain URL variables). |
users |
account/* templates (signin, dashboard, password reset, invite, deletion, license, logout, support form) are the entire user-facing auth/account surface; admin/custom/users/changelist.html customizes the Users admin; FIELD_GROUPS in templatetags/actions.py is written specifically for the Users admin form; users.views.ui.errors.custom_403 is the handler that renders errors/base.html. |
mailer |
Consumes account/email/* and account/fallback/* templates exclusively via TemplateRegistry's email-prefix constants — see §6. |
apidocs |
apidocs/{swagger,redoc,stats_base,stats_public,stats_private,stats_chart_embed}.html are apidocs's entire UI; note apidocs/stats_base.html reverses URLs under a users_api:users_ui: namespace rather than an apidocs-prefixed one — a naming quirk worth confirming with whoever owns URL routing for API stats. |
helpdesk (via genericissuetracker) |
templates/integrations/issuetracker/** is the full issue-tracker UI (list/detail/create/timeline/comments/attachments/status tabs), including the one usage site of the privacy_filters email-masking tags. |
locations |
Queried directly (not via a service layer) from templatetags/actions.py:region_country_id. |
policyengine |
Reached indirectly through utilities.constants.template_registry.get_api_stats_template()'s user.has_perm("policyengine.view_apidocs_stats") check — the one place frontend's template-selection logic depends on a policyengine permission code. |
industries, teamcentral, invoices, audit |
Each owns one or more per-app admin JS feature files under static/assets/js/admin/<app>/ (see §4.4), bundled (except invoices — see §8 gap #4) into bundle.admin.min.js. |
devtools |
Not directly referenced in this app's own code, but the shared admin/CI conventions this app's build script encodes (asset validation warnings, ordered bundling) are the kind of thing a devtools build/lint command would plausibly wrap — flagged for confirmation when that app's doc is cross-checked. |
| Django admin (framework) | Wholesale template override — admin/base_site.html, admin/change_form.html, admin/submit_line.html replace Django's stock admin templates for the entire project, since frontend/templates is checked first in the loader chain. |
10. Quick Reference — Common Tasks
Add a new page that uses the standard site chrome:
- Create the template under an appropriate subdirectory of
frontend/templates/(e.g.account/site_pages/for a public page). {% extends "base/base.html" %}and fill in{% block content %}(plus{% block title %}/{% block extra_head %}/{% block extra_js %}as needed).- Add a constant to
utilities.constants.template_registry.TemplateRegistryrather than hardcoding the path string in the view, and have the view referenceTemplateRegistry.YOUR_CONSTANT.
Add a new admin page/model using the shared admin skin:
- Subclass
utilities.admin.base_admin_page.BaseAdminPage(not plainModelAdmin) — this alone gets youadmin/layouts/changelist.html/admin/layouts/form.html, pagination, audit/history tabs, and (if you setfilter_config) AJAX cascading filters. - If the model needs a bespoke changelist tweak beyond what
BaseAdminPagegives for free, addadmin/custom/<your_app>/changelist.htmlextendingadmin/layouts/changelist.html. - If your admin form needs per-app JS (Select2 dependent-dropdowns, computed totals, etc.), add a file under
static/assets/js/admin/<your_app>/and remember to add it to theADMIN_JSarray infrontend/packaging/minify.sh— being on disk is not sufficient (see §8 gap #4 for what happens when this step is skipped). - Re-run
frontend/packaging/minify.shto regeneratestatic/dist/js/bundle.admin.min.jsbefore the change will show up in the browser.
Add a new outbound email:
- Add
account/email/<prefix>.html,account/fallback/<prefix>.txt, andaccount/fallback/<prefix>_subject.txt(oraccount/email/<prefix>_subject.txtif it's in the allauth-controlled password-reset flow — see the note in §6). - Add the prefix constant to
TemplateRegistry's email-templates section. - Wire it up on the
mailerside (see themailerapp doc for the adapter/flow pattern) —frontendonly supplies the templates, not the sending logic.