Integrating with your project
The engine knows nothing about your users, organisations or permission system. It asks one object, an
ApprovalsAdapter, named by the APPROVALS_ADAPTER setting. You either use
the built-in adapter, or write your own.
The built-in adapter
With APPROVALS_TENANT_MODEL, APPROVALS_ROLE_MODEL and APPROVALS_ADAPTER unset, the engine uses
AuthApprovalsAdapter, for single-tenant projects on Django’s auth
app:
Tenant: one built-in tenant,
Tenant.default(), which every active user belongs to.Roles: Django groups. A rule can name a group, and any of its members may approve.
Permissions: ordinary Django permissions in the
approvalsapp, created after everymigrate:approve_<action_type>for each registered action andmanage_approval_policiesfor administrators. Grant them to users or groups as usual; superusers have them all, but still can’t approve their own changes.Notifications: emailed with
django.core.mailto recipients with an email address. SetDEFAULT_FROM_EMAIL, and overriderequest_url()to include a link to your approval screen.Audit: each
AuditRecordis logged on thedjango_maker_checker.auditlogger, with the record in the log entry’sauditattribute.
Subclass it to change any of these, and point APPROVALS_ADAPTER at your subclass:
from django_maker_checker.auth_adapter import AuthApprovalsAdapter
class Approvals(AuthApprovalsAdapter):
def request_url(self, approval_request):
return f"https://app.example.com/approvals/{approval_request.reference}/"
def audit(self, record):
AuditLog.objects.create(action=record.action, resource=record.resource_type, data=record.after or {})
Your own adapter
For multi-tenant projects, or your own roles or permission system, set APPROVALS_TENANT_MODEL,
APPROVALS_ROLE_MODEL and APPROVALS_ADAPTER, and subclass ApprovalsAdapter. For a project where each user
has a Membership in one Organisation with one Role:
# approvals_integration/adapter.py
from django.contrib.auth import get_user_model
from django.db.models import Q
from django_maker_checker.adapter import ApprovalsAdapter
from accounts.models import Membership, Role
class Approvals(ApprovalsAdapter):
def _membership(self, user):
return Membership.objects.filter(user=user, active=True).select_related("organisation", "role").first()
def tenant_of(self, user):
membership = self._membership(user)
return membership.organisation if membership and user.is_active else None
def members(self, tenant, user_ids=None, role_ids=None):
users = get_user_model().objects.filter(
membership__organisation=tenant, membership__active=True, is_active=True
)
if user_ids is not None or role_ids is not None:
users = users.filter(Q(pk__in=user_ids or ()) | Q(membership__role_id__in=role_ids or ()))
return users.distinct()
def role_ids_of(self, user):
membership = self._membership(user)
return {membership.role_id} if membership and membership.role_id else set()
def roles(self, tenant):
return Role.objects.filter(organisation=tenant)
def role_member_count(self, role):
return Membership.objects.filter(role=role, active=True).count()
def has_permission(self, user, codename):
return user.has_perm(f"accounts.{codename}")
def role_has_permission(self, role, codename):
return role.permissions.filter(codename=codename).exists()
def notify(self, notice):
... # send notice.text to notice.recipients by email, in-app or chat
def audit(self, record):
... # write the AuditRecord to your audit log
Adapter methods
Required methods:
Method |
Answers |
|---|---|
|
The tenant the user acts for, or |
|
Active users of the tenant, optionally only those with the given ids or roles. |
|
Ids of the user’s active roles. |
|
The roles that can be picked as approvers. |
|
How many active users hold the role. |
|
Your permission check for the user. |
|
Whether the role grants the permission. |
|
Deliver a |
|
Write an |
Optional methods:
Method |
Default |
|---|---|
|
|
|
The scope key itself. |
|
|
|
The user’s full name, or username. |
|
|
Notifications
The engine decides who hears about an event; your notify() delivers it over your channels.
Notice kind |
Sent to |
|---|---|
|
The current approvers and their delegates: a request is waiting for them |
|
The maker: applied, rejected (with the reason, severity |
|
The maker: the request was sent back, with what to change |
|
The current approvers, and the maker when an administrator cancelled |
|
The current approvers: the deadline is near |
|
The backup approver, or the current approvers when there is none |
Permissions
The engine only names the permission codenames it checks:
approve_<action_type>for each action (ActionHandler.approve_permission), needed to be an approver;manage_approval_policies(django_maker_checker.permissions.MANAGE_POLICIES), for administrators who configure rules, cancel anyone’s request and retry failed ones.
The built-in adapter creates them for you. With your own adapter, your project creates and grants them. A
post_migrate handler that loops over
all_handlers() keeps them in step with the registered actions:
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
from django_maker_checker.registry import all_handlers
def sync_approval_permissions(sender, **kwargs):
content_type = ContentType.objects.get_for_model(Organisation)
for handler in all_handlers():
Permission.objects.get_or_create(
codename=handler.approve_permission,
content_type=content_type,
defaults={"name": handler.approve_permission_label},
)
Switching maker-checker on per tenant
Override is_enabled(tenant) to put the feature behind a feature flag, a plan or a setting:
class Approvals(ApprovalsAdapter):
def is_enabled(self, tenant):
return tenant.plan.includes_approvals
While it returns False the tenant is treated as having no rules: guarded actions run immediately,
requires_approval is False, submit refuses, and the SLA job skips the tenant. Rules, history and requests
are kept, so switching back on resumes where it stopped. Requests that are open at the switch stay open and can still
be cancelled; hide your approval screens and cancel them if they must not linger.
Migration order
Django resolves APPROVALS_TENANT_MODEL and APPROVALS_ROLE_MODEL to their app’s first migration. If those
models are created in a later migration, add an empty migration to your integration app that depends on it and runs
before the engine:
class Migration(migrations.Migration):
dependencies = [("accounts", "0007_role")]
run_before = [("approvals", "0001_initial")]
operations = []
As with AUTH_USER_MODEL, choose the tenant and role models before the first migration; changing them later is not
supported.
Reacting to events
Need |
Hook |
|---|---|
A request’s outcome |
|
Anything on the timeline |
|
Doing something when your action is |
Your handler’s |
approved |