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 format djbn_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

  1. Superusers: Full access to all templates

  2. Template Owners: Full access to their own templates

  3. 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: Returns True (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: Returns True (changelist access)

  • Superuser: Returns True

  • Owner: Returns True

  • Others: 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:

  • title

  • subtext

  • aliases

  • group

  • icon

  • content

  • user

  • show_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 PermissionDenied for 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

  1. Automatic Widget Configuration: Detects and configures BlockNote widgets

  2. User Context Injection: Passes user information to widgets

  3. 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

"" or None

[]

CSV String

"meeting, notes, agenda"

["meeting", "notes", "agenda"]

JSON String

'["meeting", "notes"]'

["meeting", "notes"]

List

["meeting", "notes"]

["meeting", "notes"]

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:

  1. Check cache backend configuration

  2. Verify DJANGO_BLOCKNOTE_CACHE_TIMEOUT setting

  3. Check logs for cache refresh errors

  4. 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:

  1. Ensure form inherits from BlockNoteFormMixin

  2. Verify aliases field is JSONField in model

  3. Check 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.