Document Template¶
This reference documentation covers the core models, admin interfaces, and form handling for Django BlockNote templates and caching systems.
Caution
This system is working but “held together by toothpicks” its a solid foundation for further development.
Unfold Admin Support
Django BlockNote provides basic compatibility with django-unfold admin theme. The admin interface will work with Unfold’s styling, though some BlockNote-specific customizations may not be fully optimized for the Unfold design system.
DocumentTemplate Model¶
The DocumentTemplate model manages user-specific BlockNote templates with intelligent caching and ownership controls.
Model Fields¶
Core Template Fields¶
title = models.CharField(max_length=200)
The display title shown in the slash menu.
subtext = models.CharField(max_length=20, blank=True)
Brief description displayed under the title in the slash menu (maximum 20 characters).
aliases = models.JSONField(default=list, blank=True)
Search aliases stored as a JSON list. Accepts CSV input through forms, automatically converted to list format.
group = models.CharField(max_length=100, blank=True)
Organizational group name for categorizing templates in the slash menu.
icon = models.CharField(max_length=50, choices=ICON_CHOICES, default="template")
Icon identifier displayed in the slash menu. See ICON_CHOICES for available options.
content = BlockNoteField(menu_type="template", ...)
The actual template content as BlockNote blocks (JSON structure).
Metadata Fields¶
user = models.ForeignKey(User, on_delete=models.CASCADE)
Template owner. Controls access permissions and cache organization.
show_in_menu = models.BooleanField(default=True)
Whether this template appears in the BlockNote slash menu.
created_at = models.DateTimeField(auto_now_add=True)
Timestamp when the template was created.
Cache Management Methods¶
get_cache_key(user_id)¶
@staticmethod
def get_cache_key(user_id):
"""Generate cache key for user templates"""
return f"djbn_templates_user_{user_id}"
Parameters:
user_id(int): User ID for cache key generation
Returns:
str: Cache key in formatdjbn_templates_user_{user_id}
get_cache_timeout()¶
@classmethod
def get_cache_timeout(cls):
"""Get cache timeout from settings with sensible default"""
return getattr(settings, 'DJANGO_BLOCKNOTE_CACHE_TIMEOUT', 3600)
Returns:
int: Cache timeout in seconds (default: 3600 = 1 hour)
Configuration:
# settings.py
DJANGO_BLOCKNOTE_CACHE_TIMEOUT = 1800 # 30 minutes
get_cached_templates(user)¶
@classmethod
def get_cached_templates(cls, user):
"""Get user templates from cache, fallback to DB"""
Parameters:
user(User): Django user instance
Returns:
list: List of template dictionaries ready for frontend consumption
Behavior:
Attempts cache retrieval first
Falls back to database query and cache refresh on cache miss
Logs cache hit/miss events for monitoring
# Usage example
templates = DocumentTemplate.get_cached_templates(request.user)
refresh_user_cache(user)¶
@classmethod
def refresh_user_cache(cls, user):
"""Refresh cache for a specific user's templates"""
Parameters:
user(User): Django user instance
Returns:
list: Refreshed template data
Template Data Structure:
{
"id": "123",
"title": "Meeting Notes",
"subtext": "Agenda & actions",
"aliases": ["meeting", "notes", "agenda"],
"group": "Business",
"icon": "meeting",
"content": [
{
"id": "block-1",
"type": "heading",
"props": {"level": 1},
"content": [{"type": "text", "text": "Meeting Title"}]
}
# ... more blocks
]
}
invalidate_user_cache(user)¶
@classmethod
def invalidate_user_cache(cls, user):
"""Invalidate cache for a specific user"""
Parameters:
user(User): Django user instance
Purpose:
Removes user’s template cache
Called automatically on user logout
Useful for manual cache management
Model Lifecycle Methods¶
save()¶
def save(self, *args, **kwargs):
"""Override save to refresh cache"""
Behavior:
Saves the model instance
Automatically refreshes the user’s template cache
Logs save events with structured logging
Handles errors gracefully with exception logging
Note: Aliases conversion from CSV to JSON list is handled by form layer, not in save method.
delete()¶
def delete(self, *args, **kwargs):
"""Override delete to refresh cache after template removal"""
Behavior:
Captures user and title before deletion
Performs deletion
Refreshes user’s template cache
Logs deletion events with context
Cache Flow Diagram¶
graph TD
A[Widget Render] --> B{Cache Hit?}
B -->|Yes| C[Return Cached Templates]
B -->|No| D[Query Database]
D --> E[Build Template List]
E --> F[Store in Cache]
F --> G[Return Templates]
H[Template Save] --> I[Refresh Cache]
J[Template Delete] --> I
K[User Logout] --> L[Invalidate Cache]
style A fill:#e1f5fe
style C fill:#c8e6c9
style G fill:#c8e6c9
style I fill:#fff3e0
style L fill:#ffebee
DocumentTemplateAdmin¶
Django admin interface for managing DocumentTemplate instances with ownership-based permissions and support-friendly access.
Permission Model¶
Access Levels¶
Superusers: Full access to all templates
Template Owners: Full access to their own templates
Admin Staff: Read-only access to all templates (for support)
Permission Methods¶
has_change_permission(request, obj=None)¶
def has_change_permission(self, request, obj=None):
"""Users can edit their own templates, all admin can view"""
Logic:
obj is None: ReturnsTrue(changelist access)Superuser: Returns
True(can edit anything)Others: Returns
True(view access, editing controlled by readonly fields)
has_delete_permission(request, obj=None)¶
def has_delete_permission(self, request, obj=None):
"""Only superusers and template owners can delete"""
Logic:
obj is None: ReturnsTrue(changelist access)Superuser: Returns
TrueOwner: Returns
TrueOthers: Returns
False
get_readonly_fields(request, obj=None)¶
def get_readonly_fields(self, request, obj=None):
"""Make everything readonly for non-superusers viewing others' templates"""
Readonly Fields for Non-Superusers Viewing Others’ Templates:
titlesubtextaliasesgroupiconcontentusershow_in_menu
Admin Configuration¶
List Display¶
list_display = ["title", "user", "group", "show_in_menu", "created_at"]
list_filter = ["group", "show_in_menu", "created_at"]
search_fields = ["title", "user__username", "subtext"]
readonly_fields = ["created_at"]
Query Optimization¶
def get_queryset(self, request):
"""All admin users can see all templates (for support)"""
return super().get_queryset(request).select_related("user")
Security Features¶
Ownership Validation¶
def save_model(self, request, obj, form, change):
"""Handle template saves with ownership validation"""
New Template Creation:
Non-superusers automatically become the owner
Superusers can set any owner
Template Updates:
Non-superusers can only edit their own templates
Ownership transfers require superuser privileges
Raises
PermissionDeniedfor unauthorized attempts
Bulk Delete Protection¶
def delete_queryset(self, request, queryset):
"""Handle bulk delete with permission checks and cache refresh"""
Security Checks:
Non-superusers can only bulk delete their own templates
Permission validation before deletion
Automatic cache refresh for affected users
Error Handling:
Logs cache refresh failures
Graceful handling of missing users
Structured error logging
Admin Permission Flow¶
graph TD
A[Admin User Access] --> B{Is Superuser?}
B -->|Yes| C[Full Access to All Templates]
B -->|No| D{Owns Template?}
D -->|Yes| E[Full Edit/Delete Access]
D -->|No| F[Read-Only Access]
G[Template Save] --> H{Ownership Change?}
H -->|Yes| I{Is Superuser?}
H -->|No| J[Normal Save + Cache Refresh]
I -->|Yes| J
I -->|No| K[PermissionDenied]
L[Bulk Delete] --> M{Superuser or Own Templates?}
M -->|Yes| N[Execute + Cache Refresh]
M -->|No| O[PermissionDenied]
style C fill:#c8e6c9
style E fill:#c8e6c9
style F fill:#fff3e0
style J fill:#c8e6c9
style K fill:#ffebee
style N fill:#c8e6c9
style O fill:#ffebee
Form Mixins¶
BlockNoteFormMixin¶
Core form mixin that provides automatic BlockNote widget configuration and aliases field processing.
Features¶
Automatic Widget Configuration: Detects and configures BlockNote widgets
User Context Injection: Passes user information to widgets
Aliases Processing: Converts CSV input to JSON list format
Usage¶
from django_blocknote.forms.mixins import BlockNoteFormMixin
class MyTemplateForm(BlockNoteFormMixin, forms.ModelForm):
class Meta:
model = DocumentTemplate
fields = ['title', 'aliases', 'content']
widgets = {
'content': BlockNoteWidget()
}
# In view
form = MyTemplateForm(user=request.user)
Widget Configuration Method¶
def _configure_blocknote_widgets(self):
"""Find and configure all BlockNote widgets with user context via attrs"""
Behavior:
Automatically detects BlockNote widgets in form fields
Injects user context through widget attributes
Provides debug logging when enabled
Debug Mode:
class MyForm(BlockNoteFormMixin, forms.ModelForm):
_debug_widget_config = True # Enable debug output
Aliases Processing: clean_aliases()¶
def clean_aliases(self):
"""Convert CSV string input to JSON list for aliases field"""
Input Handling:
Input Type |
Example |
Output |
|---|---|---|
Empty |
|
|
CSV String |
|
|
JSON String |
|
|
List |
|
|
Implementation Details:
Strips whitespace from individual aliases
Removes empty aliases
Handles both form input (CSV) and programmatic input (JSON/list)
Graceful fallback for unexpected input types
Error Handling¶
try:
# Try parsing as JSON first (for API/programmatic input)
import json
return json.loads(aliases)
except (json.JSONDecodeError, ValueError):
# Treat as CSV string (normal form input)
return [alias.strip() for alias in aliases.split(',') if alias.strip()]
Form Mixin Hierarchy¶
# Base mixin with core functionality
class BlockNoteFormMixin:
# Core widget configuration and aliases processing
# Convenience mixins
class BlockNoteFormMixin(BlockNoteFormMixin, forms.Form):
# Ready-to-use form base class
class BlockNoteModelFormMixin(BlockNoteFormMixin, forms.ModelForm):
# Ready-to-use model form base class
# Formset support
class BlockNoteUserFormsetMixin:
# Handles user context for formsets
Form Processing Flow¶
graph TD
A[Form Initialization] --> B[Extract User from kwargs]
B --> C[Call super init]
C --> D[Configure BlockNote Widgets]
D --> E[Form Ready for Use]
F[Form Validation] --> G[Field Validation]
G --> H[clean_aliases Called]
H --> I{Input Type?}
I -->|CSV String| J[Split and Clean]
I -->|JSON String| K[Parse JSON]
I -->|List| L[Return As-Is]
I -->|Empty| M[Return Empty List]
J --> N[Return Cleaned List]
K --> N
L --> N
M --> N
style E fill:#c8e6c9
style N fill:#c8e6c9
Integration Examples¶
Basic Template Management¶
# views.py
from django.views.generic import CreateView, UpdateView
from django_blocknote.views.mixins import BlockNoteViewMixin
from django_blocknote.forms.mixins BlockNoteModelFormMixin
class DocumentTemplateForm(BlockNoteModelFormMixin):
class Meta:
model = DocumentTemplate
fields = ['title', 'subtext', 'aliases', 'group', 'icon', 'content']
widgets = {
'content': BlockNoteWidget()
}
class TemplateCreateView(BlockNoteUserViewMixin, CreateView):
model = DocumentTemplate
form_class = DocumentTemplateForm
class TemplateUpdateView(BlockNoteUserViewMixin, UpdateView):
model = DocumentTemplate
form_class = DocumentTemplateForm
def get_queryset(self):
# Users can only edit their own templates
return DocumentTemplate.objects.filter(user=self.request.user)
Advanced Usage with Custom Validation¶
class AdvancedTemplateForm(BlockNoteModelFormMixin):
class Meta:
model = DocumentTemplate
fields = ['title', 'aliases', 'content', 'group']
def clean_title(self):
title = self.cleaned_data['title']
# Check for duplicate titles for this user
if DocumentTemplate.objects.filter(
user=self.user,
title=title
).exclude(pk=self.instance.pk if self.instance else None).exists():
raise forms.ValidationError("You already have a template with this title.")
return title
def clean(self):
cleaned_data = super().clean()
# Custom cross-field validation
if not cleaned_data.get('aliases') and not cleaned_data.get('group'):
raise forms.ValidationError(
"Templates must have either aliases or be assigned to a group."
)
return cleaned_data
Performance Monitoring¶
# Custom cache monitoring
class MonitoredDocumentTemplate(DocumentTemplate):
class Meta:
proxy = True
@classmethod
def get_cache_stats(cls):
"""Get cache statistics for monitoring"""
from django.core.cache import cache
from django.contrib.auth import get_user_model
User = get_user_model()
stats = {
'total_users': User.objects.count(),
'cached_users': 0,
'total_cache_size': 0
}
for user in User.objects.all():
cache_key = cls.get_cache_key(user.id)
cached_data = cache.get(cache_key)
if cached_data:
stats['cached_users'] += 1
stats['total_cache_size'] += len(str(cached_data))
return stats
Configuration Reference¶
Settings¶
# settings.py
# Cache timeout for template data (seconds)
DJANGO_BLOCKNOTE_CACHE_TIMEOUT = 3600 # Default: 1 hour
# Example configurations for different use cases:
# High-frequency changes
DJANGO_BLOCKNOTE_CACHE_TIMEOUT = 300 # 5 minutes
# Stable production environment
DJANGO_BLOCKNOTE_CACHE_TIMEOUT = 86400 # 24 hours
# Development/testing
DJANGO_BLOCKNOTE_CACHE_TIMEOUT = 60 # 1 minute
# Disable caching (always fetch from DB)
DJANGO_BLOCKNOTE_CACHE_TIMEOUT = 0
Logging Configuration¶
# settings.py
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'file': {
'level': 'INFO',
'class': 'logging.FileHandler',
'filename': 'django_blocknote.log',
},
},
'loggers': {
'django_blocknote.models': {
'handlers': ['file'],
'level': 'INFO',
'propagate': True,
},
'django_blocknote.signals': {
'handlers': ['file'],
'level': 'INFO',
'propagate': True,
},
},
}
Troubleshooting¶
Common Issues¶
Cache Not Refreshing¶
Symptoms: Templates don’t appear in slash menu after saving
Solutions:
Check cache backend configuration
Verify
DJANGO_BLOCKNOTE_CACHE_TIMEOUTsettingCheck logs for cache refresh errors
Manually invalidate cache:
DocumentTemplate.invalidate_user_cache(user)
Permission Denied in Admin¶
Symptoms: Admin users can’t edit templates
Cause: Non-superuser trying to edit another user’s template
Solution: Either:
Make user a superuser
Transfer template ownership
User should create their own template
Aliases Not Converting¶
Symptoms: Form validation errors with aliases field
Solutions:
Ensure form inherits from
BlockNoteFormMixinVerify aliases field is
JSONFieldin modelCheck for custom
clean_aliases()method conflicts
Debug Mode¶
Enable debug logging for detailed troubleshooting:
# In your form
class MyForm(BlockNoteModelFormMixin):
_debug_widget_config = True
def clean_aliases(self):
print(f"Processing aliases: {self.data.get('aliases')}")
return super().clean_aliases()
This will provide detailed output about widget configuration and aliases processing.