Tutorial¶
This tutorial serves to show how to use the core aspects of daf. Many
of the main benefits of using daf come as a result of decorating
your functions and action wrappers with
python-args decorators.
Similarly, many of the advantages of reduced Django form and view boilerplate
comes as a result of daf using
django-args for views. Some
of the main features of django-args, which include dynamic conditional
form wizard steps, will not be covered in this tutorial. It is recommended
that the reader also read and understand how
python-args and
django-args work before
going through the daf tutorial.
Before we even go into constructing interfaces using daf, we’re first
going to cover the fundamentals of Action objects and the
corresponding ObjectAction and
ObjectsAction utilities that can be used to construct a
large amount of interfaces.
After we cover the basics, we will go over examples of defining views and wizards, using views and wizards in the Django admin, and how to construct Rest Framework actions.
Core action views¶
As shown in the quickstart, daf comes with several core views. The
primary ones are:
daf.views.FormView: For constructing adaf.views.FormViewon an action. The view can be used by any actions, but the user needs to supply the parameters to the action via the form or via theget_default_argsmethod on the view.daf.views.ObjectFormView: AFormViewthat automatically passes anobjectargument to the action. Similar to djangoDetailViewclasses, it must be supplied an object ID via the URL. Actions that are used by this view must take anobjectparameter, which is the default behavior ofObjectActionandObjectsActionclasses.daf.views.ObjectsFormView: AFormViewthat automatically passes anobjectsargument to the action. Similar to howObjectFormViewobtains the object ID via the URL, thedaf.views.ObjectsFormViewuses the URL query parameters for loading multiple objects. Actions must accept anobjectsargument, which is the default behavior forObjectActionandObjectsAction.
Similar to Django DetailView classes, the daf.views.ObjectFormView
and daf.views.ObjectsFormView both make an object and objects variable
accessible on the view class and the view context. This can be useful
for making templates that are interoperable with both types of action
views. For example, this template works with both ObjectFormView
and ObjectsFormView and changes the header accordingly:
{{ form.media }}
{{ view.display_name }}
{% if object %} - One {{ view.model_meta.verbose_name }}
{% elif objects %} - {{ objects|length|title }} {{ view.model_meta.verbose_name_plural }}
<form action=".?{{ request.GET.urlencode }}" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">
Submit
</button>
</form>
As you might have noticed, we access the model_meta attribute on the view
in the template.
The Action class has many attributes, including the model_meta
attribute, and many of these attributes are directly mirrored on the
action view. This allows us to create actions that have default attributes
across every interface while still maintaining the ability to customize
attributes per view.
We showed how to create a FormView in the quickstart. An ObjectFormView
follows a nearly-indentical structure. Below shows all of the code needed,
including some minor modifications to the action definition and callable.
import arg
from django import forms
import django.contrib.auth.models as auth_models
import daf.actions
import daf.views
def is_granter_valid(granter):
if not granter.is_staff:
raise ValueError(f'Granter {granter} is not staff')
@arg.defaults(granter=arg.first('granter', arg.val('request').user))
@arg.validators(is_granter_staff)
def grant_staff_access(granter, users, is_staff):
users.update(is_staff=staff)
return users
class GrantStaffAccess(daf.actions.ObjectAction):
callable = grant_staff_access
object_arg = 'user'
model = auth_models.User
class GrantStaffAccessForm(forms.Form):
is_staff = forms.BooleanField(required=False)
class GrantStaffAccessObjectFormView(daf.views.ObjectFormView):
form_class = GrantStaffAccessForm
template_name = 'examples/grant_staff.html'
action = GrantStaffAccess
Some notes on the main changes from the previous example in our quickstart:
GrantStaffAccessnow inheritsObjectAction, which requires definingobject_argandmodel. Our object action is going to use “user” as the argument, and it will be aUsermodel.Instead of using
get_default_argsto pass in the granter, we instead use obtain the grant with an@arg.defaultsdecorator. By default, every action view makes therequestargument available to every view. Using@arg.defaults(granter=arg.first('granter', arg.val('request').user))means that thegranterargument is going to be assign to either thegranterargument that is used when calling thecallable, or theuserfrom therequestargument that is automatically available.Since the
ObjectFormViewautomatically makes theobjectargument available to the callable, and since theobjectargument is automatically mapped to the argument referenced byobject_arg, we only need to collect theis_staffflag in our form in this view. All other arguments to our callable are automatically handled as a result of the underlyingpython-argsanddjango-argslibraries.
Now that we have an ObjectAction, this means we can automatically make
a bulk update view like so:
class GrantStaffAccessObjectsFormView(daf.views.ObjectsFormView):
form_class = GrantStaffAccessForm
template_name = 'examples/grant_staff.html'
action = GrantStaffAccess
There is no difference in the arguments. The only difference is the
type of url pattern that needs to be set up in order to create paths
to the views. This is automatically handled with the daf.urls.get_url_patterns
utility:
import daf.urls
urlpatterns = daf.urls.get_url_patterns(
[GrantStaffAccessObjectFormView, GrantStaffAccessObjectsFormView]
)
Assuming our urls are included under /example/, our generated URL
paths would be /examples/auth/user/grant-staff-access/<int:pk>/ for
the ObjectAction and /examples/auth/user/grant-staff-access/ for
the ObjectsAction. The ObjectAction parses the pk URL arguments
when determining which objects are being edited. For example,
/examples/auth/user/grant-staff-access/?pk=1&pk=2&pk=3 operates on
User 1, 2, and 3.
Note
The PK URL argument for ObjectFormView can be modified with the
pk_url_kwarg attribute. The PK query argument for ObjectsFormView can
be modified with the url_query_arg attribute.
Core action wizards¶
Any action FormView or related subclass can also take advantage of
daf wizard views. The wizard views in daf make use of
django-args, which builds on the
django-formtools
library.
daf makes little modifications or additions to these core views. Although
we will cover the necessities here, we recommend reading
django-args docs
and django-formtools docs
to better understand how to utilize the wizard classes.
Similar to daf.views.FormView, one can use daf.views.WizardView as
the base class for any form wizard. For those unfamiliar with
django-formtools,
a wizard is just a set of Django forms that are shown in order. The
WizardView manages showing the steps and collecting the data using
a storage backend. daf comes with the daf.views.SessionWizardView that
is already configured to use session storage as a backend.
Let’s take our GrantStaffAccess action and make a wizard where
the user can select the granter, the user, and the staff flag in a wizard:
from django import forms
import django.contrib.auth.models as auth_models
import daf.views
class GranterForm(forms.Form):
granter = forms.ModelChoiceField(queryset=auth_models.User.objects.all())
class UserForm(forms.Form):
user = forms.ModelChoiceField(queryset=auth_models.User.objects.all())
class StaffForm(forms.Form):
is_staff = forms.BooleanField(required=False)
class GrantStaffAccessWizard(daf.views.SessionWizardView):
form_list = [GranterForm, UserForm, StaffForm]
template_name = 'examples/grant_staff.html'
action = GrantStaffAccess
The main difference with wizard classes is the form_list variable that must
list all steps of the wizard.
Creating the template for a wizard is very similar to a form view. For example, this will render a “Next” button and also include the additional data needed by the form wizard:
{{ form.media }}
{{ view.display_name }}
<form action=".?{{ request.GET.urlencode }}" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ wizard.management_form }}
{{ form.as_p }}
{% if wizard and wizard.steps.current != wizard.steps.last %}
<button type="submit">Next</button>
{% else %}
<button type="submit">
Submit
</button>
{% endif %}
</form>
To conditionally show steps, use the condition_dict attribute
on the wizard view. More examples of doing this are shown in
the django-args docs
and the django-formtools docs.
The wizard views in daf are functionally no different than the standard
form views. Every form view we discussed in the previous section, including
ObjectFormView and ObjectsFormView have corresponding wizard views
(daf.views.ObjectWizardView and daf.views.ObjectsWizardView) and
session wizard views
(daf.views.SessionObjectWizardView and daf.views.SessionObjectsWizardView).
URLs for associated wizard views can also be created the same way with
daf.urls.get_url_patterns.
Admin actions¶
daf comes with a native integration into the Django admin, removing
the need to write template or URL paths for views. Along with this, it means
there is no boilerplate for authentication and URL protection.
We’re going to continue strong with our GrantStaffAccess action and
integrated it into the Django admin. Before we create an admin, let’s go
ahead and create three different admin views for our GrantStaffAccess
action:
import daf.admin
class GrantStaffAccessForm(forms.Form):
is_staff = forms.BooleanField(required=False)
class GrantStaffAccessModelView(daf.admin.FormView):
action = GrantStaffAccess
form_class = GrantStaffAccessForm
def get_default_args(self):
return {
**super().get_default_args(),
**{'objects': auth_models.User.objects.all()},
}
class GrantStaffAccessObjectView(daf.admin.ObjectFormView):
action = GrantStaffAccess
form_class = GrantStaffAccessForm
class GrantStaffAccessObjectsView(daf.admin.ObjectsFormView):
action = GrantStaffAccess
form_class = GrantStaffAccessForm
We are creating three views because there are three different integration points into the Django admin:
On the main model list page in the toolbar. Along with the default creation button, any
daf.admin.FormViewclasses will automatically be presented here since they are not associated with a particular object or set of objects. InGrantStaffAccessModelView, we are rendering a button to grant staff access to all users at once, hence why we set theobjectsargument to every user inget_default_args. As mentioned, these types of actions don’t need to be associated with any objects. We are just using our action as an example.On the action dropdown list from the model list page. These are where bulk actions are rendered by default, such as Django’s default deletion action. Any
daf.admin.ObjectsFormViewactions will be rendered here.On the toolbar for the detail view of the model. Along with the default history button, any
daf.admin.ObjectFormViewactions will be rendered here.
Note
The rendering locations are the same for the wizard views, which
include (but are not limited to) daf.admin.SessionWizardView,
daf.admin.SessionObjectWizardView, and daf.admin.SessionObjectsWizardView.
When all actions are defined, one must inherit daf.admin.ActionMixin
in their admin and also provide the interfaces to the daf_actions
attribute to register them:
class UserAdmin(daf.admin.ActionMixin, admin.ModelAdmin):
daf_actions = [
GrantStaffAccessModelView,
GrantStaffAccessObjectView,
GrantStaffAccessObjectsView,
]
admin.site.register(User, UserAdmin)
When going to the User change list page, the admin looks like:
The bulk action appears as an option in the dropdown, and the action that operates over every user is rendered as a button in the toolbar.
The detail page also renders a button in the toolbar for the object action:
When selecting all objects and clicking on the bulk update action, the page looks like:
The individual object update page looks like:
By default, all admin views are protected just like any other admin
page. Admin actions are also only rendered if the user has permission
to perform the action. This can be overridden on an individual view
basis or by setting the settings.DAF_ADMIN_ACTION_PERMISSIONS_REQUIRED
to False as the default.
Note
Corresponding wizard views (daf.admin.SessionObjectWizardView, etc)
are rendered the same way in the admin. The primary difference is that
a “Next” button appears until the final step.
Rest framework actions¶
Similar to the Django admin, daf allows one to integrate actions
directly into Django Rest Framework Viewsets.
Currently daf only supports object actions for viewsets using
the daf.rest_framework.DetailAction class. For example, let’s turn
our GrantStaffAccess action into an API endpoint:
class GrantStaffAccessForm(forms.Form):
is_staff = forms.BooleanField(required=False)
class GrantStaffAccessObjectDRFAction(daf.rest_framework.DetailAction):
action = GrantStaffAccess
form_class = GrantStaffAccessForm
Similar to daf.views.ObjectFormView, DetailAction automatically makes
the request and object variable available, making our
GrantStaffAccess action compatible out of the box.
You might have noticed something strange - our detail action for rest
framework is still using a form, even though we don’t need to collect
input from the user. While it is not required to provide a form_class
for DetailAction interfaces, using a form_class will automatically
clean any input before it is provided to our action. This helps in
reducing the boilerplate of having to cast numbers, parse datetimes, and
other tedious things when writing endpoints.
When we have our action defined, we still need to add it to the viewset.
Similar to how our admin integration works, viewsets must inherit
the daf.rest_framework.ActionMixin class and also provide actions
in the daf_actions class attribute. Here’s our full viewset definition.
from django.contrib.auth.models import User
from rest_framework import permissions
from rest_framework import serializers
from rest_framework import viewsets
import daf.rest_framework
class UserSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
username = serializers.CharField()
email = serializers.CharField()
is_staff = serializers.BooleanField()
class UserViewSet(daf.rest_framework.ActionMixin, viewsets.ModelViewSet):
permission_classes = [permissions.IsAuthenticated]
queryset = User.objects.all()
serializer_class = UserSerializer
daf_actions = [GrantStaffAccessObjectDRFAction]
With this configuration, our action will now appear as a detail endpoint on the viewset.
By default, all exceptions raised by a daf.rest_framework.DetailAction
will be wrapped in daf.rest_framework.raise_drf_error, which will
turn any non-rest framework exception into a daf.rest_framework.APIException.
The daf.rest_framework.APIException is just a subclass of rest framework’s
APIException class, but has a default status code of 400.
By default, the daf.rest_framework.raise_drf_error wrapper simply re-raises
non-rest framework errors with the stringified error as the message.
However, if a Django ValidationError is raised,
daf.rest_framework.raise_drf_error will fill in the code of the
APIException using the code from the ValidationError.
Atomicity of actions¶
All action classes that operate on objects (daf.actions.ObjectAction and
daf.actions.ObjectsAction) operate atomically by default, meaning that
a select_for_update is applied to the object(s) in question in a transaction
before running any validations or action code. If this behavior is
undesirable or if the user wishes to control atomicity manually, set
the select_for_update class variable on the action class to None.
Note
The select_for_update class attribute just passes the argument through
to djarg.qset. For more information about how to configure this
parameter to suit your needs (such as skipping locked objects),
see the djarg docs and
the docs for select_for_update.