Building a Complete Feature¶
This tutorial picks up where the Getting Started guide left off. You already have a NotificationRegistry with EmailNotification and SMSNotification. Now you’ll add conditional availability, hierarchical sub-registries, a plugin, and signals.
Prerequisites¶
You should have the basic notification registry working from the Getting Started guide:
# myapp/registry.py
from django_stratagem import Registry, Interface
class NotificationRegistry(Registry):
implementations_module = "notifications"
class NotificationInterface(Interface):
registry = NotificationRegistry
def send(self, message: str, recipient: str) -> bool:
raise NotImplementedError
# myapp/notifications.py
from myapp.registry import NotificationInterface
class EmailNotification(NotificationInterface):
slug = "email"
description = "Send notifications via email"
priority = 10
def send(self, message, recipient):
# send email...
return True
class SMSNotification(NotificationInterface):
slug = "sms"
description = "Send notifications via SMS"
priority = 20
def send(self, message, recipient):
# send SMS...
return True
Step 1: Add Conditional Availability¶
Some notification channels should only be available to certain users. For example, push notifications might require a specific permission.
Create a conditional implementation¶
Add a PushNotification that requires the myapp.use_push permission:
# myapp/notifications.py
from django_stratagem import ConditionalInterface, PermissionCondition
from myapp.registry import NotificationRegistry
class PushNotification(ConditionalInterface):
registry = NotificationRegistry
slug = "push"
description = "Send push notifications"
priority = 30
condition = PermissionCondition("myapp.use_push")
def send(self, message, recipient):
# send push notification...
return True
ConditionalInterface works just like Interface, but adds a condition attribute that controls when the implementation is available.
Test with different user contexts¶
from myapp.registry import NotificationRegistry
# A regular user without the permission
context = {"user": regular_user}
available = NotificationRegistry.get_available_implementations(context)
# {"email": <class EmailNotification>, "sms": <class SMSNotification>}
# push is NOT included
# An admin user with the permission
context = {"user": admin_user}
available = NotificationRegistry.get_available_implementations(context)
# {"email": ..., "sms": ..., "push": <class PushNotification>}
You can compose conditions with &, |, and ~ for more complex rules. See How to Use Conditional Availability for all built-in conditions and composition patterns.
Step 2: Add a Hierarchical Sub-Registry¶
Suppose each notification channel has sub-options - for email, you might choose HTML or plain text format; for SMS, you might choose a provider. Hierarchical registries model these parent-child relationships.
Define the child registry¶
# myapp/registry.py
from django_stratagem import Registry, Interface, HierarchicalRegistry, HierarchicalInterface
class NotificationRegistry(Registry):
implementations_module = "notifications"
class NotificationInterface(Interface):
registry = NotificationRegistry
def send(self, message: str, recipient: str) -> bool:
raise NotImplementedError
class NotificationFormatRegistry(HierarchicalRegistry):
implementations_module = "notification_formats"
parent_registry = NotificationRegistry
Create child implementations¶
# myapp/notification_formats.py
from django_stratagem import HierarchicalInterface
from myapp.registry import NotificationFormatRegistry
class HTMLEmail(HierarchicalInterface):
registry = NotificationFormatRegistry
slug = "html_email"
description = "Rich HTML email"
parent_slug = "email" # Only valid under EmailNotification
class PlainTextEmail(HierarchicalInterface):
registry = NotificationFormatRegistry
slug = "plain_text_email"
description = "Plain text email"
parent_slug = "email"
class TwilioSMS(HierarchicalInterface):
registry = NotificationFormatRegistry
slug = "twilio_sms"
description = "SMS via Twilio"
parent_slug = "sms"
Query filtered by parent¶
from myapp.registry import NotificationFormatRegistry
# Get formats available for email
formats = NotificationFormatRegistry.get_children_for_parent("email")
# {"html_email": <class HTMLEmail>, "plain_text_email": <class PlainTextEmail>}
# Get choices for a form dropdown filtered by parent
choices = NotificationFormatRegistry.get_choices_for_parent("email")
# [("html_email", "HTML Email"), ("plain_text_email", "Plain Text Email")]
Use in a model¶
# myapp/models.py
from django.db import models
from django_stratagem import HierarchicalRegistryField
from myapp.registry import NotificationRegistry, NotificationFormatRegistry
class NotificationConfig(models.Model):
channel = NotificationRegistry.choices_field()
format = HierarchicalRegistryField(
registry=NotificationFormatRegistry,
parent_field="channel",
)
The parent_field parameter tells the field to validate that the selected format is valid for the selected channel. See How to Use Hierarchical Registries for more.
Step 3: Write a Simple Plugin¶
Plugins let third-party packages add implementations to your registry without modifying your code. We’ll create a webhook notification as a plugin.
Create the implementation¶
# django_webhook_notifications/notifications.py
from myapp.registry import NotificationInterface
class WebhookNotification(NotificationInterface):
slug = "webhook"
description = "Send notifications via webhook"
priority = 50
def send(self, message, recipient):
# POST to webhook URL...
return True
Create plugin metadata¶
# django_webhook_notifications/stratagem_plugin.py
__version__ = "1.0.0"
REGISTRY = "NotificationRegistry"
IMPLEMENTATIONS = [
"django_webhook_notifications.notifications.WebhookNotification",
]
Register the entry point¶
In the plugin’s pyproject.toml:
[project.entry-points."django_stratagem.plugins"]
webhook_notifications = "django_webhook_notifications.stratagem_plugin"
Once installed, the webhook option appears in NotificationRegistry automatically - in forms, admin, and API responses. See How to Use the Plugin System for more on plugin development.
Step 4: React to Registrations with Signals¶
django-stratagem emits signals when implementations are registered, unregistered, or when a registry is reloaded. Use them for logging, cache invalidation, or other side effects.
Log registrations¶
# myapp/signals.py
from django.dispatch import receiver
from django_stratagem.signals import implementation_registered
@receiver(implementation_registered)
def log_registration(sender, registry, implementation, **kwargs):
import logging
logger = logging.getLogger("django_stratagem")
logger.info(
"Registered %s in %s",
implementation.slug,
registry.__name__,
)
Make sure this module is imported at startup - for example, in your AppConfig.ready():
# myapp/apps.py
from django.apps import AppConfig
class MyAppConfig(AppConfig):
name = "myapp"
def ready(self):
import myapp.signals # noqa: F401
For more signal patterns (cache invalidation, audit trails) and the full list of extension hooks, see Extension Hooks and Customization Points.
Step 5: Wire It All Up¶
Now combine everything into an admin view and a form.
Admin¶
# myapp/admin.py
from django.contrib import admin
from django_stratagem.admin import HierarchicalRegistryAdmin
from myapp.models import NotificationConfig
@admin.register(NotificationConfig)
class NotificationConfigAdmin(HierarchicalRegistryAdmin):
pass
HierarchicalRegistryAdmin handles both conditional filtering (based on the logged-in admin’s permissions) and hierarchical field relationships. See How to Use Forms, Widgets, and the Admin.
Form¶
# myapp/forms.py
from django import forms
from django_stratagem import (
ContextAwareRegistryFormField,
HierarchicalRegistryFormField,
RegistryContextMixin,
HierarchicalFormMixin,
)
from myapp.registry import NotificationRegistry, NotificationFormatRegistry
class NotificationConfigForm(
RegistryContextMixin,
HierarchicalFormMixin,
forms.Form,
):
channel = ContextAwareRegistryFormField(registry=NotificationRegistry)
format = HierarchicalRegistryFormField(
registry=NotificationFormatRegistry,
parent_field="channel",
)
# Usage in a view:
form = NotificationConfigForm(
registry_context={"user": request.user, "request": request},
)
Template¶
{% load stratagem %}
<h2>Available Notification Channels</h2>
{% get_implementations notification_registry request_context as channels %}
{% for slug, impl in channels.items %}
<div class="channel">
<strong>{{ impl|display_name }}</strong>
<p>{{ impl|registry_description }}</p>
{% if impl|is_available:request_context %}
<span class="badge">Available</span>
{% endif %}
</div>
{% endfor %}
See How to Use Template Tags and Filters for the full list of tags and filters.
What’s Next¶
That covers the main intermediate features. The how-to guides go deeper on each topic:
How to Use Model Fields - all field types, lookups, factory patterns
How to Use Forms, Widgets, and the Admin - form fields, widgets, admin classes
How to Use Conditional Availability - all built-in conditions, composition, custom conditions
How to Use Hierarchical Registries - parent-child relationships
How to Use DRF Integration - serializer fields and API views
How to Use the Plugin System - writing and using plugins
Extension Hooks and Customization Points - hooks, signals, testing extensions
Architecture and Design - how auto-discovery works, design decisions