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 a daf.views.FormView on 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 the get_default_args method on the view.

  • daf.views.ObjectFormView: A FormView that automatically passes an object argument to the action. Similar to django DetailView classes, it must be supplied an object ID via the URL. Actions that are used by this view must take an object parameter, which is the default behavior of ObjectAction and ObjectsAction classes.

  • daf.views.ObjectsFormView: A FormView that automatically passes an objects argument to the action. Similar to how ObjectFormView obtains the object ID via the URL, the daf.views.ObjectsFormView uses the URL query parameters for loading multiple objects. Actions must accept an objects argument, which is the default behavior for ObjectAction and ObjectsAction.

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:

  1. GrantStaffAccess now inherits ObjectAction, which requires defining object_arg and model. Our object action is going to use “user” as the argument, and it will be a User model.

  2. Instead of using get_default_args to pass in the granter, we instead use obtain the grant with an @arg.defaults decorator. By default, every action view makes the request argument available to every view. Using @arg.defaults(granter=arg.first('granter', arg.val('request').user)) means that the granter argument is going to be assign to either the granter argument that is used when calling the callable, or the user from the request argument that is automatically available.

  3. Since the ObjectFormView automatically makes the object argument available to the callable, and since the object argument is automatically mapped to the argument referenced by object_arg, we only need to collect the is_staff flag in our form in this view. All other arguments to our callable are automatically handled as a result of the underlying python-args and django-args libraries.

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:

  1. On the main model list page in the toolbar. Along with the default creation button, any daf.admin.FormView classes will automatically be presented here since they are not associated with a particular object or set of objects. In GrantStaffAccessModelView, we are rendering a button to grant staff access to all users at once, hence why we set the objects argument to every user in get_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.

  2. 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.ObjectsFormView actions will be rendered here.

  3. On the toolbar for the detail view of the model. Along with the default history button, any daf.admin.ObjectFormView actions 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:

_images/admin_model_page.png

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:

_images/admin_detail_page.png

When selecting all objects and clicking on the bulk update action, the page looks like:

_images/admin_bulk_change_page.png

The individual object update page looks like:

_images/admin_detail_change_page.png

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.