Panels
Reference for Panel — the configuration object that declares how a named region in a Layout is populated.
What is a Panel?
A Panel is a class attribute on a Layout. It describes the source of the HTML for that region. The Layout engine resolves the source at request time and makes the result available in the layout template via {% panel "name" %}.
from dj_layouts import Layout, Panel
class DefaultLayout(Layout):
template = "myapp/layout.html"
sidebar = Panel("myapp:sidebar") # sidebar region → calls the "myapp:sidebar" view
footer = Panel("myapp:footer") # footer region → calls the "myapp:footer" view
Source types
Panel accepts exactly one source specification: either a positional argument or one of the url_name= / literal= keyword arguments.
URL name (namespaced)
Any string containing : is treated as a namespaced URL name and reversed via django.urls.reverse().
Panel("myapp:sidebar") # → reverse("myapp:sidebar") → call that view
Panel("myapp:item_detail") # → reverse("myapp:item_detail") → call that view
NoReverseMatch propagates — there is no silent fallback. If the URL name doesn't exist you get an error at request time.
URL name (non-namespaced) — url_name= kwarg
Non-namespaced URL names don't contain :, so the auto-detection heuristic would treat them as literals. Use the url_name= keyword argument to force URL reversal:
Panel(url_name="home") # → reverse("home") → call that view
Panel(url_name="about") # → reverse("about") → call that view
url_name= always calls reverse(), regardless of whether : is present.
Literal HTML — positional string without :
A string with no : is returned as-is without any URL resolution:
Panel("<p>Static content</p>") # returned verbatim
Panel("") # empty string — template fallback is used
Literal HTML — literal= kwarg
Use literal= to force literal treatment even when the string contains ::
Panel(literal="See: https://example.com for more") # never reversed
Panel(literal="<a href='http://x.com'>Link</a>") # returned verbatim
Callable
Pass any callable that accepts (request, **context) and returns an HttpResponse or str:
def my_panel_fn(request, **kwargs):
return f"<p>Hello {request.user}</p>"
class DefaultLayout(Layout):
template = "myapp/layout.html"
greeting = Panel(my_panel_fn)
The callable receives the cloned panel request plus any context kwargs from the Panel definition.
List of sources
Pass a list to combine multiple sources into one panel. Each item in the list is resolved independently and results are joined with the join separator:
Panel(["myapp:widget_a", "myapp:widget_b"], join="\n")
Panel(["myapp:widget_a", "<hr>", "myapp:widget_b"]) # mixed URL + literal
None — empty panel
Panel(None) (or just Panel()) produces empty output. The layout template's fallback content is rendered instead:
class DefaultLayout(Layout):
template = "myapp/layout.html"
ads = Panel(None) # always empty — no ad panel rendered
In the template:
{% panel "ads" %}<p>No ads configured.</p>{% endpanel %}
All source type behaviours at a glance
| Source | Contains : |
How it resolves |
|---|---|---|
Panel("app:name") |
Yes | reverse("app:name") → call view |
Panel("plain string") |
No | Return as literal HTML |
Panel(url_name="home") |
N/A | Always reverse("home") → call view |
Panel(literal="x:y") |
Yes | Return as literal HTML (never reversed) |
Panel(callable) |
N/A | Call callable(request, **context) |
Panel([...]) |
N/A | Resolve each item, join results |
Panel(None) or Panel() |
N/A | Empty — template fallback renders |
Keyword arguments
context=
Extra keyword arguments forwarded to the panel's source view or callable.
Panel("myapp:item_detail", context={"pk": 42})
Important: Panel context kwargs are merged on top of URL-captured route parameters. Panel context wins:
# URL: /items/<pk>/
# Panel: Panel("myapp:item_detail", context={"pk": 99})
# The view receives pk=99, not whatever the layout URL captured
This is intentional — it lets you pin a panel to a specific object without creating a dedicated URL.
!!! warning "Never put untrusted values in Panel.context"
Panel.context is configuration-time data — it's fixed when your layouts.py module loads. Do not use it to pass user-supplied or request-time data. For request-time data, use get_layout_context() or have the panel view fetch it from the database itself.
Putting `request.GET["user_id"]` in a `Panel.context` is not possible (there's no request at class-definition time) and is not the intended pattern. The panel view should read `request.GET` directly.
join=
Type: str | Default: ""
Separator string used when source is a list. Items are joined with this string after resolution:
Panel(["myapp:widget_a", "myapp:widget_b"], join="<hr>")
Ignored when source is not a list.
url_name=
Force URL name resolution. See URL name (non-namespaced) above.
literal=
Force literal string treatment. See Literal HTML — literal= kwarg above.
ConditionalPanel
ConditionalPanel is a subclass of Panel that conditionally renders its contents based on the evaluation of a condition against the active layout context.
from dj_layouts import Layout, ConditionalPanel
class NewsLayout(Layout):
template = "layouts/news.html"
# Only renders if 'object.allow_replies' evaluates to True in the layout context
replies_panel = ConditionalPanel(
template_name="engagement/thread_panel.html",
condition=lambda context: getattr(context.get('object'), 'allow_replies', False)
)
If the condition evaluates to False, the layout rendering engine completely bypasses the panel, returning an empty string "" without attempting to resolve the source or triggering cache lookups/writes. This triggers the template fallback inside the layout.
Specifying the condition
The condition parameter can be one of:
-
A Callable or Lambda: A function that receives
context(the activeLayoutContextobject) as its sole argument and returns a boolean value.python # Evaluates if 'user.is_staff' is True in context ConditionalPanel("admin_tools.html", condition=lambda ctx: ctx.get("user").is_staff) -
A String (Context Variable): A string representing a variable in the layout context to evaluate. It supports dotted notation for nested lookups (resolved via Django's template variable resolver). ```python # Evaluates 'allow_replies' in context ConditionalPanel("engagement.html", condition="allow_replies")
# Evaluates 'object.allow_replies' in context (nested lookup) ConditionalPanel("engagement.html", condition="object.allow_replies") ```
- A Boolean or any Truthy/Falsy value: A raw value resolved at configuration time.
python ConditionalPanel("feature.html", condition=settings.FEATURE_FLAG)
Specification details
ConditionalPanelacceptstemplate_nameas a keyword argument alias tosourcefor readability. You cannot specify bothsourceandtemplate_name.- Any exception raised during condition callable evaluation or variable lookup is caught gracefully and treated as
False(bypassing rendering).
Panel priority order
When a request is processed, the effective panel for each name is determined in this order (last wins):
- Class-level definition —
Panel(...)as a class attribute on the Layout - Per-view override —
panels={"name": Panel(...)}passed to@layoutorrender_with_layout() - Template fallback — the content between
{% panel "name" %}and{% endpanel %}
The template fallback is not really an "override" — it only applies when the resolved panel produces empty output (empty string).
Per-view panel overrides
You can override individual panels per view without subclassing:
@layout("myapp.DefaultLayout", panels={"sidebar": Panel("myapp:user_sidebar")})
def profile_page(request):
return HttpResponse(...)
To suppress a panel for a specific view, pass None:
@layout("myapp.DefaultLayout", panels={"sidebar": None})
def landing_page(request):
return HttpResponse(...) # no sidebar on this page
Panel resolution: sync vs async
Under @layout, panels are resolved sequentially (one after another).
Under @async_layout, all panels are resolved concurrently via asyncio.gather. Sync views are automatically wrapped with sync_to_async. See Async for details.
Panel requests
Panels always receive a cloned version of the original request:
methodis alwaysGETPOSTandFILESare clearedrequest.user,request.session,request.cookies,request.resolver_matchare preservedrequest.layout_roleis set to"panel"request.is_layout_partialisFalserequest.layout_contextis aFrozenLayoutContext(read-only copy)
Panel views do not go through middleware again. Auth and session data are available (from the original request), but middleware side effects (e.g. security headers, cookie updates) do not fire.
See Security for implications of this.
Panel caching
Panels can be cached so the view is only called on the first request. On subsequent requests the cached HTML is served directly, and any render queue items (scripts, styles) that the panel would have enqueued are replayed from cache — so the page's scripts and styles remain correct even when the panel itself is not re-rendered.
Quick setup
from dj_layouts import Layout
from dj_layouts import cache
class DefaultLayout(Layout):
template = "myapp/layout.html"
nav = Panel("myapp:nav", cache=cache.sitewide(timeout=3600))
Cache variation strategies
| Function | Cache key varies by | Use case |
|---|---|---|
cache.sitewide(timeout=...) |
Nothing — one entry for all users and paths | Global nav, site footer |
cache.per_user(timeout=...) |
User identity (user.pk, or "anonymous") |
User-specific sidebars |
cache.per_path(timeout=...) |
Request path | Path-sensitive widgets |
cache.per_user_per_path(timeout=...) |
User + path | User-specific per-page content |
cache.per_session(timeout=...) |
Session key | Session-scoped panels |
cache.custom(key_func=..., timeout=...) |
Return value of key_func(request) |
Any other variation |
All functions accept an optional backend= argument to specify which Django cache backend to use (default: "default").
Anonymous users
For per_user and per_user_per_path, anonymous users (those where request.user.is_authenticated is False, or where request.user doesn't exist) are all treated as the same identity using the string key "anonymous".
!!! warning "All anonymous users share one cache entry"
With cache.per_user(), every anonymous visitor to your site serves from the same cache entry. If your panel output varies by any request attribute beyond the user identity (e.g. a cookie, a session value, a query parameter), use cache.custom() with an appropriate key_func instead.
Render queues and caching
When a panel is cached, its render queue contributions (scripts, styles, arbitrary queue items) are cached alongside the HTML. On a cache hit the queue items are replayed into the current request's queues, so {% renderscripts %} and {% renderstyles %} still output the expected tags.
Deduplication still applies on replay — if two panels enqueue the same script, it only appears once in the output regardless of which panels were cached.
class DefaultLayout(Layout):
template = "myapp/layout.html"
scripts = ScriptQueue() # declared so panels can enqueue into it
nav = Panel("myapp:nav", cache=cache.sitewide(timeout=3600))
# nav calls add_script(request, "/js/nav.js") — this is cached and replayed on hit
Global cache toggle
Set CACHE_ENABLED: False in your DJ_LAYOUTS settings dict to disable all panel caching globally. Useful in development or testing:
# settings/local.py
DJ_LAYOUTS = {"CACHE_ENABLED": False}
See Settings for the full reference.
stale_ttl and refresh_func
These arguments are accepted on all cache config functions for API stability, but are not yet implemented. They are no-ops in the current version:
# Accepted but has no effect yet:
cache.sitewide(timeout=3600, stale_ttl=7200, refresh_func=my_refresh)
Do not rely on stale-while-revalidate behaviour in the current release.