ViewFactory Internals¶
This document explains how the ViewFactory generates Django class-based views from actions, including the mechanics of plugin injection, MRO construction, and attribute binding.
The Core Problem¶
Django's class-based views are designed to be subclassed and customized through inheritance. However, this creates challenges when you need to:
- Dynamically compose views based on configuration
- Inject functionality from plugins without modifying code
- Avoid deep inheritance hierarchies that become brittle
- Generate views at runtime based on action configuration
The ViewFactory solves this by treating view generation as a composition problem rather than an inheritance problem.
Factory Architecture¶
ViewFactory
│
┌─────────────────────┼─────────────────────┐
│ │ │
1. Get Base Class 2. Collect Mixins 3. Build Attributes
│ │ │
▼ ▼ ▼
Action's mixin Plugin registries Action + Plugins
(ListView, (Pagination, (model, queryset,
CreateView) Breadcrumbs) template_names)
│ │ │
└─────────────────────┼─────────────────────┘
│
▼
type(name, bases, attrs)
│
▼
Generated Django CBV
The Four Steps¶
Step 1: Get Base Class¶
The factory asks: What kind of Django view should this be?
def _get_base_class(self, action: 'BaseAction') -> type[View]:
# Check plugins first (allows override)
for registry in pm.hook.djadmin_get_action_view_base_class(action=action):
if registry:
for action_class, base_class in registry.items():
if isinstance(action, action_class):
return base_class
# Use action's default (from view-type mixin)
return action.get_base_class()
How it works:
- Plugins can register base class overrides via
djadmin_get_action_view_base_class() - Plugin returns dict:
{ActionClass: BaseClass} - Factory checks
isinstance(action, key)for each key - First match wins, or falls back to action's
get_base_class()
Example:
# Action declares view type via mixin
class ListAction(ListActionMixin, GeneralActionMixin, BaseAction):
pass
# ListActionMixin provides:
class ListActionMixin:
base_class = ListView # Django's generic ListView
Result: Factory uses ListView as base class.
Plugin override example:
@hookimpl
def djadmin_get_action_view_base_class(action):
# Use custom list view for certain models
if action.model == Product:
return {ListAction: OptimizedListView}
return {}
Step 2: Collect Mixins¶
The factory asks: What additional functionality should this view have?
def _get_mixins(self, action: 'BaseAction') -> list[type]:
mixins = []
# Get all plugin registries
for registry in pm.hook.djadmin_get_action_view_mixins(action=action):
if registry:
# registry = {ActionClass: [Mixin1, Mixin2]}
for action_class, mixin_list in registry.items():
if isinstance(action, action_class):
if mixin_list:
mixins.extend(mixin_list)
return mixins
How it works:
- Each plugin can register mixins via
djadmin_get_action_view_mixins() - Plugin returns dict:
{ActionClass: [Mixin1, Mixin2]} - Factory checks
isinstance(action, key)for each key - All matching mixins are collected in order
Example:
# Core plugin provides essential mixins
@hookimpl
def djadmin_get_action_view_mixins(action):
from djadmin.actions.view_mixins import ListActionMixin
return {
ListActionMixin: [
BreadcrumbMixin, # Add breadcrumbs
ActionContextMixin, # Add action context
]
}
# Theme plugin adds UI mixins
@hookimpl
def djadmin_get_action_view_mixins(action):
from djadmin.actions.view_mixins import ListActionMixin
return {
ListActionMixin: [
PaginationMixin, # Enhanced pagination
SortingMixin, # Column sorting
]
}
Result: Factory collects [BreadcrumbMixin, ActionContextMixin, PaginationMixin, SortingMixin]
Step 3: Build Attributes¶
The factory asks: What properties and methods should this view have?
def _build_class_dict(self, action: 'BaseAction') -> dict:
# Start with core attributes
class_dict = {
'action': action,
'model': action.model,
'model_admin': action.model_admin,
'admin_site': action.admin_site,
}
# Bind attributes from action
for attr_name in self._get_attribute_bindings(action):
if hasattr(action, attr_name):
# Get unbound function from action class
attr_value = getattr(action.__class__, attr_name, None)
if attr_value is not None:
class_dict[attr_name] = attr_value
else:
# Fallback for instance attributes
class_dict[attr_name] = getattr(action, attr_name)
# Merge plugin-provided attributes
plugin_attributes = self._get_plugin_attributes(action)
class_dict.update(plugin_attributes)
return class_dict
How it works:
- Start with core attributes (action, model, model_admin, admin_site)
- Bind attributes from action (methods, properties)
- Add plugin-provided attributes (override earlier values)
Attribute Bindings¶
Plugins declare which action methods should be bound to the view:
@hookimpl
def djadmin_get_action_view_attribute_bindings(action):
from djadmin.actions.base import BaseAction
from djadmin.actions.view_mixins import ListActionMixin
return {
BaseAction: [
'get_template_names', # All actions
],
ListActionMixin: [
'get_queryset', # ListView actions
'get_context_data',
],
}
Result: These methods from the action become methods on the generated view.
Plugin Attributes¶
Plugins can add computed attributes or override behavior:
@hookimpl
def djadmin_get_action_view_attributes(action):
from djadmin.actions.view_mixins import ListActionMixin
if isinstance(action, ListActionMixin):
return {
ListActionMixin: {
'paginate_by': action.model_admin.paginate_by,
'ordering': action.model_admin.ordering or ['-pk'],
}
}
return {}
Step 4: Generate Class¶
The factory asks: How do we assemble everything into a Python class?
def create_view(self, action: 'BaseAction') -> type[View]:
# 1. Get components
base_class = self._get_base_class(action)
mixins = self._get_mixins(action)
class_dict = self._build_class_dict(action)
view_name = self._generate_view_name(action)
# 2. Build MRO: mixins first, then base class
bases = tuple(mixins) + (base_class,)
# 3. Create the class using type()
view_class = type(view_name, bases, class_dict)
return view_class
How it works:
- Name generation:
ProductListView(descriptive name for debugging) - MRO construction:
(Mixin1, Mixin2, ..., BaseClass) - Class creation:
type(name, bases, attrs)- Python's metaclass magic - Result: A real Python class, equivalent to a class definition
What you get:
# Equivalent to writing:
class ProductListView(BreadcrumbMixin, ActionContextMixin,
PaginationMixin, SortingMixin, ListView):
action = <action_instance>
model = Product
model_admin = ProductAdmin
admin_site = site
paginate_by = 100
ordering = ['-created']
def get_queryset(self):
# Bound from action
return self.action.get_queryset(self)
def get_template_names(self):
# Bound from action
return self.action.get_template_names()
Method Resolution Order (MRO)¶
The order of base classes matters for method resolution:
Generated View Class MRO:
├─ ProductListView (generated class)
├─ BreadcrumbMixin (first mixin)
├─ ActionContextMixin (second mixin)
├─ PaginationMixin (third mixin)
├─ SortingMixin (fourth mixin)
├─ ListView (base class)
├─ MultipleObjectTemplateResponseMixin
├─ TemplateResponseMixin
├─ BaseListView
├─ MultipleObjectMixin
├─ ContextMixin
├─ View
└─ object
Why mixins come first:
- Mixins can override base class methods
- Earlier mixins override later mixins
- Base class provides fallback implementations
- Standard Python MRO C3 linearization applies
Example: Method lookup for get_context_data():
Call: view.get_context_data()
1. Check ProductListView → No definition
2. Check BreadcrumbMixin → def get_context_data() → FOUND
→ Calls super() →
3. Check ActionContextMixin → def get_context_data() → FOUND
→ Calls super() →
4. Check PaginationMixin → No definition
5. Check SortingMixin → No definition
6. Check ListView/MultipleObjectMixin → def get_context_data() → FOUND (fallback)
Attribute Binding Mechanics¶
When the factory binds methods from actions to views, it must handle the difference between bound and unbound methods:
# Action class
class ListAction(BaseAction):
def get_queryset(self):
qs = self.model.objects.all()
return qs.filter(status='active')
# Factory binding
attr_value = getattr(action.__class__, 'get_queryset', None)
class_dict['get_queryset'] = attr_value
Why get from action.class?
# If we did this:
bound_method = action.get_queryset
class_dict['get_queryset'] = bound_method
# Then in the view:
view.get_queryset() # Calls with action as self, not view!
# Instead, we do this:
unbound_func = action.__class__.get_queryset
class_dict['get_queryset'] = unbound_func
# Then in the view:
view.get_queryset() # Calls with view as self (correct!)
Result: Methods work correctly when called on the view instance.
Plugin Registry Pattern¶
Plugins don't directly provide mixins/attributes - they provide registries (dictionaries) that map action classes to lists/dicts:
# Plugin hook signature
@hookspec
def djadmin_get_action_view_mixins(action):
"""
Return dictionary mapping action classes to mixin lists.
Factory will check isinstance(action, key) for each key.
"""
pass
# Plugin implementation
@hookimpl
def djadmin_get_action_view_mixins(action):
return {
ListAction: [PaginationMixin], # Specific action class
ListActionMixin: [BreadcrumbMixin], # View-type mixin (broader)
BaseAction: [GlobalMixin], # All actions
}
Why registries?
- Type-based dispatch: Uses
isinstance()for flexible matching - Inheritance-aware: Can target base classes (like view-type mixins)
- Composable: Multiple plugins contribute to same action type
- Explicit: Clear mapping of "what" gets "which" functionality
Matching logic:
# Given action: AddAction(CreateViewActionMixin, GeneralActionMixin, BaseAction)
for action_class, mixins in registry.items():
if isinstance(action, action_class):
# Matches: BaseAction ✓
# Matches: GeneralActionMixin ✓
# Matches: CreateViewActionMixin ✓
# Matches: ListAction ✗
collect(mixins)
View Generation Examples¶
Example 1: ListView¶
# Starting action
action = ListAction(Product, product_admin, site)
# Factory process
factory = ViewFactory()
# Step 1: Base class
base = action.get_base_class() # → ListView
# Step 2: Mixins
mixins = [BreadcrumbMixin, ActionContextMixin, PaginationMixin]
# Step 3: Attributes
attrs = {
'action': action,
'model': Product,
'model_admin': product_admin,
'paginate_by': 100,
'get_queryset': <bound from action>,
'get_template_names': <bound from action>,
}
# Step 4: Generate
view_class = type('ProductListView',
(BreadcrumbMixin, ActionContextMixin, PaginationMixin, ListView),
attrs)
# Result
ProductListView.as_view() # Ready to handle requests
Example 2: CreateView (Add Action)¶
# Starting action
action = AddAction(Product, product_admin, site)
# Factory process
base = CreateView # From CreateViewActionMixin
mixins = [BreadcrumbMixin, FormSuccessMixin]
attrs = {
'action': action,
'model': Product,
'form_class': ProductForm,
'fields': ['name', 'price', 'stock'],
'success_url': '/admin/products/',
'get_template_names': <bound from action>,
}
# Result
ProductAddActionView(BreadcrumbMixin, FormSuccessMixin, CreateView)
Example 3: Custom Record Action¶
# Custom action with form
class ChangeStatusAction(FormActionMixin, RecordActionMixin, BaseAction):
form_class = StatusForm
# Factory process
base = FormView # From FormActionMixin
mixins = [ConfirmationMixin]
attrs = {
'action': action,
'model': Product,
'form_class': StatusForm,
'get_object': <bound from action>, # Gets the record
'form_valid': <bound from action>, # Handles submission
}
# Result
ProductChangeStatusActionView(ConfirmationMixin, FormView)
Performance Considerations¶
View Class Caching¶
Views are generated at URL pattern creation time (Django startup), not per-request:
# During Django startup
def get_urls(self):
for action in model_admin.general_actions:
# Generate view class ONCE
view_class = factory.create_view(action)
# Register URL pattern
path(action.get_url_pattern(),
view_class.as_view(), # as_view() is also cached
name=action.url_name)
Runtime cost: Zero. Views are normal Python classes after startup.
Memory Usage¶
Each generated view class is ~1-2KB in memory: - Class object itself - Method references (not copies) - Attribute dictionary
For 100 models × 5 actions = 500 views ≈ 500KB-1MB total.
Debugging Generated Views¶
Inspect MRO¶
view_class = factory.create_view(action)
print(view_class.__mro__)
# Shows exact method resolution order
Inspect Attributes¶
Debug View Name¶
Access Action from View¶
def my_view_method(self):
action = self.action # Access action instance
model_admin = self.model_admin # Access admin
# Can call action methods if needed
Comparison with Alternatives¶
Alternative 1: Deep Inheritance¶
# Traditional approach
class BaseAdminView(ListView):
def get_queryset(self): ...
class PaginatedAdminView(BaseAdminView):
def get_context_data(self): ...
class SortableAdminView(PaginatedAdminView):
def get_queryset(self): ...
# Problems:
# - Fixed hierarchy
# - Can't compose features
# - Hard to override mid-chain
Alternative 2: View Subclassing¶
# Per-action subclassing
class ProductListView(AdminListView):
model = Product
paginate_by = 100
# Problems:
# - Must write class for each action
# - Can't inject from plugins
# - Lots of boilerplate
Alternative 3: Factory Pattern (Our Choice)¶
# Dynamic generation
view_class = factory.create_view(action)
# Benefits:
# - Compose mixins dynamically
# - Plugins inject functionality
# - Zero boilerplate
# - Full customization
Related Documentation¶
Source Code:
- Factory implementation: djadmin/factories/base.py
- Plugin hooks: djadmin/plugins/__init__.py
- Core plugin mixins: djadmin/plugins/core/mixins.py
Conceptual: - Action System - What gets passed to the factory - Plugin System - How plugins provide mixins - Design Decisions - Why factory pattern
Django References: - Class-Based Views - Built-in CBVs - Python type() function