django-action-framework¶
django-action-framework
(daf
) provides the ability to generate
a number of diverse interfaces from a single action definition. What is
an action? It’s a function. By writing a function and providing a few
hints about the characteristics of your function, you can:
Generate a form view from the function with proper form validation.
Generate an update view on a model object that is passed to the function.
Generate a bulk update view on multiple objects. These objects can be parametrized over a function expecting one object, meaning your detail and bulk views share the same code when desired.
Generate wizard views to collect function arguments over multiple steps, even if the steps are conditional.
Natively integrate these views into the Django admin as model, detail, or bulk actions.
Generate Django Rest Framework actions on your viewsets.
daf
removes the boilerplate and cognitive overhead of maintaining validation
logic, view logic, and update logic spread across Django views, models, admin
interfaces, API endpoints, and other locations in a Django project. daf
allows the engineer to focus on writing one clear and easily-testable piece of
business logic while treating complex UI and APIs as an extension of the
function rather than a piece of intertwined code.
How is one able to write a function and have all validation logic and
parametrization of bulk actions work seamlessly with minimal additional
boilerplate, even if parameters are gathered through a multi-step UI like
a form wizard? The answer lies in the underlying libraries used
by daf
- python-args
and django-args. While it is
not a requirement to understand these libraries to use the action framework,
we assume some knowledge of python-args
when constructing actions.
For now, we will give a brief quickstart of daf
and then go into
more complex examples in the Tutorial.
Quickstart¶
Assuming you have read the installation instructions for installing daf
in your Django project, let’s go through an example of creating an action
to grant a user staff access in the database.
Defining the action¶
It is recommended to define or import all of your actions in the actions.py
file of the relevant Django app. daf
automatically searches for actions
in this module and makes all actions accessible via the daf
action
registry. This will be explained more later.
Here, we define the GrantStaffAccess
action in examples/actions.py
:
import daf.actions
def grant_staff_access(user, is_staff):
user.is_staff = is_staff
user.save()
return user
class GrantStaffAccess(daf.actions.Action):
callable = grant_staff_access
app_label = 'examples'
The purpose of an Action
class is to primarily serve as
a container of metadata about a function. It is this metadata that is
used to aid in constructing a wide variety of interfaces.
Creating a FormView on the action¶
A FormView
can now be created on our action with:
from django import forms
import daf.views
class GrantStaffAccessForm(forms.Form):
user = forms.ModelChoiceField(queryset=User.objects.all())
is_staff = forms.BooleanField(required=False)
class GrantStaffAccessFormView(daf.views.FormView):
form_class = GrantStaffAccessForm
template_name = 'examples/grant_staff.html'
action = GrantStaffAccess
Similar to Django’s FormView
, we can construct a template using
the form
variable like so:
{{ form.media }}
{{ view.display_name }}
<form action=".?{{ request.GET.urlencode }}" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">
Submit
</button>
</form>
Every daf
interface and view comes with the action
property on the
view and several other properties of the action mirrored by default. In
the above, we use display_name
to render the title of the action.
We will cover all action and interface attributes in the Tutorial.
Two properties, the url_name
and url_path
of the action can
automatically be used to construct a URL to the view in urls.py
:
import daf.urls import examples.actions urlpatterns = daf.urls.get_url_patterns( [examples.actions.GrantStaffAccessFormView] )
And voila, you have now written an entire form view on top of a function.
daf
is not opinionated on where views and interfaces should be defined.
Users can continue to define these in views.py
or wherever they see fit.
daf
, however, is opinionated in the sense of defining functions and
business logic completely separate from the interface.
A more advanced use case¶
The advantages of daf
are not going to be seen in defining one trivial
action with a single interface, however, we can extend this example just a bit
more to highlight where daf
starts to shine.
One common pattern in complex Django forms and views is validation. Users
typically have the option to override form and field clean
methods in
order to display nice error messages to the user. Django provides several
ways to make this process easier, some of which include using
validators on
form fields.
One pattern daf
aims to prevent is the intertwining of UI logic with
core business logic and code. Not only does this make testing code more
involved, but it can create a web of complexity in trying to perform the action
safely and understand what is going on.
For example, what if we really need to perform the validation logic on our
model only after a select_for_update
to ensure there are no race conditions?
What if we want to make sure this select_for_update
only happens during
the run of the action (after the entire form is validated)? It’s these
types of situations that can quickly make a simple form into a very complex
one.
daf
is build completely on top of
python-args
and django-args. This means
that we can decorate our main function with python-args
decorators
and have our functions work seamlessly with Django form validation.
For example, let’s extend our example and pass the person that’s granting the user staff access. Let’s also make sure the granter is also a staff member.
import arg
def is_granter_valid(granter):
if not granter.is_staff:
raise ValueError(f'Granter {granter} is not staff')
@arg.validators(is_granter_staff)
def grant_staff_access(granter, user, is_staff):
user.is_staff = is_staff
user.save()
logging.info(f'Granted staff access to {user} from {granter}')
return user
When we use python-args
@arg.validators
decorator on our function,
the validation routines will automatically be bound to our form based on
the function arguments. This means you can always keep your validation logic
close to your function and keep it away from your form. It also means that
you can more easily test individual validators
(by running is_granter_valid
) or only test the core business logic
(by running grant_staff_access.func(...)
since it’s a python-args
function now).
We still need to update our view to pass in the granter
parameter.
We do this by overridding get_default_args
since we are not going to
collect the granter from the form:
class GrantStaffAccessFormView(daf.views.FormView):
form_class = GrantStaffAccessForm
template_name = 'examples/grant_staff.html'
action = GrantStaffAccess
def get_default_args(self):
return {
**super().get_default_args(),
'granter': self.request.user
}
Submitting this form results in a form error message if the authenticated user is not a staff member. Although one could solve the specific problem of permissions and object access with different mechanisms, this serves as an example of how one can write clear and concise validation logic that is not intertwined with a UI.
Next Steps¶
As we will show in the Tutorial, the use of other utilities
like daf.actions.ObjectAction
and the combination of other python-args
decorators for action wrappers can cut down on even more of the boilerplate
of writing:
Single and multiple object actions.
Wizards that collect data across multiple steps.
Rest framework viewsets that run actions.
Admin actions that run on single or multiple objects across single or multiple steps.
Before continuing to the Tutorial, it is highly recommended to read about python-args and django-args to understand the full expressiveness of what one can do. The action framework is really just a wrapper on top of these libraries.