Skip to content

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:

  1. Dynamically compose views based on configuration
  2. Inject functionality from plugins without modifying code
  3. Avoid deep inheritance hierarchies that become brittle
  4. 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:

  1. Plugins can register base class overrides via djadmin_get_action_view_base_class()
  2. Plugin returns dict: {ActionClass: BaseClass}
  3. Factory checks isinstance(action, key) for each key
  4. 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:

  1. Each plugin can register mixins via djadmin_get_action_view_mixins()
  2. Plugin returns dict: {ActionClass: [Mixin1, Mixin2]}
  3. Factory checks isinstance(action, key) for each key
  4. 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:

  1. Start with core attributes (action, model, model_admin, admin_site)
  2. Bind attributes from action (methods, properties)
  3. 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:

  1. Name generation: ProductListView (descriptive name for debugging)
  2. MRO construction: (Mixin1, Mixin2, ..., BaseClass)
  3. Class creation: type(name, bases, attrs) - Python's metaclass magic
  4. 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:

  1. Mixins can override base class methods
  2. Earlier mixins override later mixins
  3. Base class provides fallback implementations
  4. 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?

  1. Type-based dispatch: Uses isinstance() for flexible matching
  2. Inheritance-aware: Can target base classes (like view-type mixins)
  3. Composable: Multiple plugins contribute to same action type
  4. 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

print(view_class.__dict__)
# Shows all class attributes

Debug View Name

print(view_class.__name__)
# ProductListView (descriptive)

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

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