Flash Messages
Display sensitive data — API keys, passwords, tokens — exactly once in the Django Admin change view, then erase it from the session automatically.
The Problem
When you generate a secret on save (e.g. a plain-text API key), you need to show it to the admin user immediately. But you cannot store it in the database as plain text, and there is no built-in Django mechanism to show data “once and never again” in the admin.
Flash messages solve this by writing the sensitive content to the Django session on save, injecting a virtual readonly field into the change page, rendering the content on first view, and deleting the session key immediately — so a page reload shows nothing.
Truly One-Time The session key is consumed (deleted) the moment the field renders. Refreshing the page, closing and reopening the tab, or navigating away and back — the content is gone every time.
Quick Example
from django.contrib import admin
from django_cfg.modules.django_admin.base import PydanticAdmin
from django_cfg.modules.django_admin.base.pydantic_admin.flash import FlashFieldConfig
api_key_config = AdminConfig(
model=ApiKey,
readonly_fields=["created_at"],
fieldsets=[
FieldsetConfig(title="API Key", fields=["name", "prefix", "created_at"]),
],
)
@admin.register(ApiKey)
class ApiKeyAdmin(PydanticAdmin):
config = api_key_config
one_time_flash_fields = {
'plain_key_display': FlashFieldConfig(
source='_generated_plain_key',
style='code_warning',
title='Plain API Key (One-Time Display)',
message='SAVE THIS KEY NOW — You will not see it again!',
)
}Usage Patterns
There are two ways to trigger a flash message: declarative (via one_time_flash_fields) and imperative (via self.flash_once()).
Declarative — one_time_flash_fields
Define one_time_flash_fields as a class attribute mapping a field name to a FlashFieldConfig. The system automatically calls flash_once() after every create (not edit).
from django.contrib import admin
from django_cfg.modules.django_admin.base import PydanticAdmin
from django_cfg.modules.django_admin.base.pydantic_admin.flash import FlashFieldConfig
@admin.register(ApiKey)
class ApiKeyAdmin(PydanticAdmin):
config = api_key_config
one_time_flash_fields = {
'plain_key_display': FlashFieldConfig(
source='_generated_plain_key', # Transient attribute on the saved obj
style='code_warning',
title='Plain API Key (One-Time Display)',
message='SAVE THIS KEY NOW — You will not see it again!',
)
}source is the name of a transient attribute that must be set on obj before obj.save() is called. Set it in your model’s save() method:
class ApiKey(models.Model):
name = models.CharField(max_length=100)
prefix = models.CharField(max_length=8, editable=False)
hashed_key = models.CharField(max_length=128, editable=False)
def save(self, *args, **kwargs):
if not self.pk:
import secrets
plain_key = secrets.token_urlsafe(32)
self._generated_plain_key = plain_key # Transient attribute
self.prefix = plain_key[:8]
self.hashed_key = make_password(plain_key)
super().save(*args, **kwargs)Create Only
The declarative style fires only on object creation (change=False). It does not trigger when editing an existing record. Use the imperative style if you need to flash on updates.
Imperative — self.flash_once()
Call flash_once() manually inside save_model() when you need full control — conditional logic, edit-time flashing, or multiple fields at once.
@admin.register(ApiKey)
class ApiKeyAdmin(PydanticAdmin):
config = api_key_config
def save_model(self, request, obj, form, change):
super().save_model(request, obj, form, change)
if not change and hasattr(obj, '_generated_plain_key'):
self.flash_once(
request, obj,
field_name='plain_key_display',
content=obj._generated_plain_key,
title='Plain API Key (One-Time Display)',
message='SAVE THIS KEY NOW — You will not see it again!',
style='code_warning',
)Signature:
def flash_once(
self,
request,
obj,
field_name: str,
content: str,
title: str = '',
message: str = '',
style: FlashStyle = 'code_warning',
) -> None: ...| Parameter | Type | Description |
|---|---|---|
request | HttpRequest | Current admin request (provides the session) |
obj | Model instance | Saved model object (provides pk, app_label, model_name) |
field_name | str | Virtual field name that will appear in readonly_fields |
content | str | The sensitive content to display |
title | str | Bold heading shown above the content |
message | str | Subtitle or instruction shown below the title |
style | FlashStyle | Visual style — see Style Reference |
Style Reference
Eight styles are available via the FlashStyle literal type.
| Style | Appearance | Use Case |
|---|---|---|
code_warning | Yellow background, dark code block, green monospace text | Default. API keys, tokens — “save this now” |
code_error | Red background, dark code block, red monospace text | Destructive secrets, credentials that can cause damage |
code_success | Green background, dark code block, green monospace text | Successfully generated keys, confirmation codes |
info | Blue box, plain text | Informational one-time messages |
warning | Yellow box, plain text | Warnings without sensitive code |
error | Red box, plain text | Error notices, failed generation messages |
success | Green box, plain text | Success confirmations without code |
raw | Renders content as raw HTML | Custom HTML layouts — use with care |
raw Style and XSS
The raw style skips HTML escaping and injects content directly into the page. Only use it with trusted, application-generated content — never with user-supplied strings.
All other styles pass content through Django’s format_html(), making them safe against XSS by default.
FlashFieldConfig Reference
FlashFieldConfig is a Pydantic v2 model used with the declarative one_time_flash_fields dict.
from django_cfg.modules.django_admin.base.pydantic_admin.flash import FlashFieldConfig
FlashFieldConfig(
source='_generated_plain_key', # Required
style='code_warning', # Optional, default: 'code_warning'
title='Plain API Key', # Optional
message='Save this now!', # Optional
)| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
source | str | Yes | — | Name of the transient attribute on obj to read from |
style | FlashStyle | No | 'code_warning' | Visual style for the rendered field |
title | str | No | '' | Bold heading displayed above the content |
message | str | No | '' | Subtitle or instruction shown below the title |
How It Works
The full lifecycle of a flash message, step by step:
-
Save —
save_model()runs. For the declarative style, the mixin checksone_time_flash_fieldsand callsflash_once()automatically. For the imperative style, you callflash_once()directly. -
Session write —
flash_once()constructs aFlashPayloadPydantic model from the provided data, validates it, serialises it to JSON, and stores it inrequest.sessionunder the key:_pydantic_admin_flash_{app_label}_{model_name}_{pk}_{field_name} -
Redirect — Django redirects to the change page for the newly created object (standard admin behaviour).
-
get_readonly_fields()— When the change page loads, the mixin scans the session for pending flash keys matching this object. For each found key, the correspondingfield_nameis appended toreadonly_fields. -
get_fieldsets()— Flash field names are prepended to the first fieldset so they appear at the top of the change form. -
Render and consume — The display method renders the styled HTML block and immediately deletes the session key. The field will not appear on any subsequent page load.
No Manual Configuration
You do not need to add the flash field name to AdminConfig.readonly_fields or fieldsets. Steps 4 and 5 handle injection automatically.
Complete Example — API Key Admin
A full working example showing model, admin config, and flash setup:
# models.py
import secrets
from django.contrib.auth.hashers import make_password
from django.db import models
class ApiKey(models.Model):
name = models.CharField(max_length=100)
prefix = models.CharField(max_length=8, editable=False)
hashed_key = models.CharField(max_length=128, editable=False)
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
def save(self, *args, **kwargs):
if not self.pk:
plain_key = secrets.token_urlsafe(32)
self._generated_plain_key = plain_key # Transient — not saved to DB
self.prefix = plain_key[:8]
self.hashed_key = make_password(plain_key)
super().save(*args, **kwargs)
def __str__(self):
return f"{self.name} ({self.prefix}...)"# admin.py
from django.contrib import admin
from django_cfg.modules.django_admin import AdminConfig, BadgeField, DateTimeField, FieldsetConfig, Icons
from django_cfg.modules.django_admin.base import PydanticAdmin
from django_cfg.modules.django_admin.base.pydantic_admin.flash import FlashFieldConfig
from .models import ApiKey
api_key_config = AdminConfig(
model=ApiKey,
list_display=["name", "prefix", "is_active", "created_at"],
display_fields=[
BadgeField(name="prefix", title="Key Prefix", variant="info", icon=Icons.KEY),
DateTimeField(name="created_at", title="Created", ordering="created_at"),
],
readonly_fields=["prefix", "created_at"],
fieldsets=[
FieldsetConfig(
title="API Key",
fields=["name", "prefix", "is_active", "created_at"],
),
],
search_fields=["name", "prefix"],
)
@admin.register(ApiKey)
class ApiKeyAdmin(PydanticAdmin):
config = api_key_config
# Declarative: auto-fires on create
one_time_flash_fields = {
'plain_key_display': FlashFieldConfig(
source='_generated_plain_key',
style='code_warning',
title='Plain API Key (One-Time Display)',
message='SAVE THIS KEY NOW — You will not see it again!',
)
}After creating an API key, the change page shows:
┌──────────────────────────────────────────────────────────┐
│ Plain API Key (One-Time Display) │
│ SAVE THIS KEY NOW — You will not see it again! │
│ ┌────────────────────────────────────────────────────┐ │
│ │ xK3mQ8zR2pL9nW5vY1tA6bC4dF0gH7jI... │ │
│ └────────────────────────────────────────────────────┘ │
├──────────────────────────────────────────────────────────┤
│ API Key │
│ Name: My Service Key │
│ Prefix: xK3mQ8zR │
│ Active: ✓ │
│ Created: 2026-03-09 14:22:01 │
└──────────────────────────────────────────────────────────┘On the next page load, the flash field is gone.
Imperative Pattern — Conditional Flash
Use flash_once() directly when you need conditional logic:
@admin.register(RotatingCredential)
class RotatingCredentialAdmin(PydanticAdmin):
config = credential_config
def save_model(self, request, obj, form, change):
super().save_model(request, obj, form, change)
# Flash on both create and rotation (edit)
if hasattr(obj, '_new_plain_secret'):
action = 'Rotated' if change else 'Generated'
self.flash_once(
request, obj,
field_name='secret_display',
content=obj._new_plain_secret,
title=f'Secret {action} (One-Time Display)',
message='Store this in your secrets manager immediately.',
style='code_error' if change else 'code_warning',
)Security Notes
XSS Safety
All flash content (except raw style) is escaped through Django’s format_html() before being inserted into the page. This prevents cross-site scripting even if content contains HTML characters.
Only the raw style bypasses escaping. It is intended for pre-built, application-controlled HTML — never pass user input to a raw-style flash field.
Session Storage
Flash data is stored in Django’s standard session backend. Key properties:
- The session key is unique per object and field name — multiple flash fields on the same object are independent.
- The key is deleted on first render — not on first page load, but at the moment the display method runs.
- If the admin user closes the tab before the change page loads, the data remains in the session until the session expires (governed by
SESSION_COOKIE_AGE). - Flash data is never written to the database model; only the hashed/derived value is persisted.
Session Expiry If the session expires before the admin user visits the change page, the flash data is lost silently. The field simply will not appear. This is by design — the alternative is persisting the plain-text secret, which is worse.
Transient Attributes
The source attribute in FlashFieldConfig must exist on the model instance at the time flash_once() is called. Set it in the model’s save() method or in save_model() before calling super().save_model().
# Correct: set the transient attribute in model.save()
def save(self, *args, **kwargs):
if not self.pk:
plain = secrets.token_urlsafe(32)
self._generated_plain_key = plain # Set BEFORE super().save()
self.hashed_key = make_password(plain)
super().save(*args, **kwargs)# Also correct: set it in save_model() before calling super()
def save_model(self, request, obj, form, change):
if not change:
plain = secrets.token_urlsafe(32)
obj._generated_plain_key = plain # Set BEFORE super().save_model()
obj.hashed_key = make_password(plain)
super().save_model(request, obj, form, change)
if not change:
self.flash_once(request, obj, field_name='plain_key_display',
content=obj._generated_plain_key, style='code_warning')Next Steps
- Configuration — AdminConfig reference, fieldsets, readonly fields
- Field Types — Display field types for list views
- Examples — Real-world admin examples
TAGS: flash-messages, one-time-display, api-keys, session, sensitive-data