View layer
Introduction
If you are using django in a fullstack way, meaning with django views and templates, you surely want to use this extra requirement.
Just install the package like this:
pip install ai-django-core[view-layer]
If you are using pipenv
, you can add the following line to your Pipfile
:
ai-django-core = {extras = ["view-layer"],version = "*"}
Forms
CrispyLayoutFormMixin
This neat mixin provides a basic setup to turn a regular Django form into a nice bootstrapy crispy form.
class MyForm(CrispyLayoutFormMixin, forms.Form):
pass
The form will have the following properties:
Attribute |
Default configuration |
---|---|
Form tag |
Yes |
Form class |
form-horizontal form-bordered form-row-stripped |
Method |
POST |
Submit button |
Set, called “Save” |
Label class |
col-md-3 |
Field class |
col-md-9 |
Label size |
col-md-offset-3 |
Formsets
CountChildrenFormsetMixin
This mixin provides a method which returns the current number of children of this formset. It automatically takes care of deleted or to-be-deleted children. Have a look at the example use-case below:
class DataFieldFormset(CountChildrenFormsetMixin, BaseInlineFormSet):
def clean(self):
cleaned_data = super().clean()
# Count children
number_of_children = self.get_number_of_children()
# do some magic
return cleaned_data
Note that the form needs to be validated before you can use this method.
Formset Views
This package provides two mixins supporting class-based views combined with formsets. The FormsetCreateViewMixin
is to
be used with a generic.CreateView
, the FormsetUpdateViewMixin
together with generic.UpdateView
.
The idea behind these mixins to make handling formset less pain and provide the comfy feeling you are used from regular forms.
Here is an example for a create view:
class MyModelCreateView(FormsetCreateViewMixin, generic.CreateView):
model = MyModel
template_name = 'my_app/my_model_edit.html'
form_class = MyModelEditForm
formset_class = inlineformset_factory(MyModel, MyModelChild,
form=MyModelChildForm,
formset=MyModelChildFormset)
def get_formset_kwargs(self):
# this is optional!
kwargs = super().get_formset_kwargs()
kwargs['request'] = self.request
return kwargs
You just define - similar to the regular form - a formset_class
as a class attribute. All the required handling like
validation will happen magically. If you need to pass additional values to your formset, just extend the method
get_formset_kwargs()
as you would for djangos get_form_kwargs()
.
If you want to update a model and its children, here is an example for the edit-case:
class MyModelEditView(FormsetUpdateViewMixin, generic.UpdateView):
model = MyModel
template_name = 'my_app/my_model_edit.html'
form_class = MyModelEditForm
formset_class = inlineformset_factory(MyModel, MyModelChild,
form=MyModelChildForm,
formset=MyModelChildFormset)
def get_formset_kwargs(self):
# this is optional!
kwargs = super().get_formset_kwargs()
kwargs['request'] = self.request
return kwargs
View mixins
CustomPermissionMixin
If you want to handle some custom permission check for which you cannot use the regular django permissions, just derive
your class-based view from CustomPermissionMixin
and add the method validate_permissions()
with your custom logic. Notice that it has to return a boolean value.
For example, you want to allow only the “owner” of an object to view its detail page:
from django.views import generic
class MyModelDetailView(CustomPermissionMixin, generics.DetailView):
...
def validate_permissions(self):
object = super().get_object()
if self.request.user.employee:
return False
return True
If the method returns False
, the regular dispatch will just simply render your 403
page and skip all the other
things within the view.
RequestInFormKwargsMixin
The RequestInFormKwargsMixin
is a handy helper for passing the request automated form the view to the form. If you
need for example the current request user within the form, you need to have the current request available.
Just add the mixin to the parents of your class-based view class:
from django.views import generic
class MyModelEditView(RequestInFormKwargsMixin, generic.CreateView):
model = MyModel
template_name = 'myapp/mymodel_edit.html'
form_class = MyModelForm
...
class MyModelEditView(RequestInFormKwargsMixin, generic.UpdateView):
model = MyModel
template_name = 'myapp/mymodel_edit.html'
form_class = MyModelForm
...
Make sure that you overwrite the __init__\ ``method of your form and move the `request` to the class. Otherwise
the
super()`` will detect an unrecognised parameter within the kwargs and raise an error:
from django.forms.models import ModelForm
class MyModelForm(ModelForm):
def __init__(self, *args, **kwargs):
# Get request from kwargs
self.request = kwargs.pop('request', None)
# Call the parent method
super().__init__(*args, **kwargs)
That’s it.
DjangoPermissionRequiredMixin
General setup
In most cases your Django views will require a login and some permissions to access it. Ensuring this manually is tedious and error-prone. Therefore, we implemented a neat mixin which ensures by default (“security by design” pattern) that the user needs to be logged in and has the defined permissions.
from ai_django_core.view_layer.mixins import DjangoPermissionRequiredMixin
from django.views import generic
class MyModelListView(DjangoPermissionRequiredMixin, generic.ListView):
model = MyModel
permission_list = ('account.view_my_model',)
...
When you extend from this mixin, you can set the class attribute permission_list
which has to be a list or tuple.
Note, that you have to define the url to your login view, so the mixin can redirect your unauthenticated users
correctly. It defaults to redirect('login-view')
.
from ai_django_core.view_layer.mixins import DjangoPermissionRequiredMixin
from django.views import generic
class MyModelListView(DjangoPermissionRequiredMixin, generic.ListView):
model = MyModel
permission_list = ('account.view_my_model',)
def get_login_url(self):
return reverse('my-login-view')
Public endpoints
If you require an open view endpoint, just set the class flag login_required
to False.
from ai_django_core.view_layer.mixins import DjangoPermissionRequiredMixin
from django.views import generic
class PublicLandingPageView(DjangoPermissionRequiredMixin, generic.TemplateView):
login_required = False
permission_list = ()
...
Testing
Setup
As stated above, ensuring that every single view is protected is tedious and error-prone. Therefore, this package provides a test mixin to ensure the following things:
Your view is inheriting from the
DjangoPermissionRequiredMixin
mixinYou didn’t accidentally break the logic by overwriting important parts, so it ensures that the login barrier and the permission test methods are called
The permissions are set correctly
The following test case will create a bunch of default test cases covering the points 1-3.
from ai_django_core.view_layer.tests.mixins import BaseViewPermissionTestMixin
from django.test import TestCase
from my_project.my_app import views
class MyModelListViewTest(BaseViewPermissionTestMixin, TestCase):
view_class = views.MyModelListView
permission_list = ['account.view_my_model']
Note, that the permissions are defined redundantly. This approach reduces the risk that you make a copy/paste error when adding the permissions.
If you have implemented more custom logic, feel free to just add more test cases of your own.
Limitations
If you are using the caching decorator directly in a class-based view, the tests will fail. You have to switch to a class decorator like this:
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
from django.views import generic
@method_decorator(cache_page(60 * 10), name='dispatch')
class MyModelListView(generic.ListView):
...
Generic views
ToggleView
Django provides a neat way of enabling the update of a given object through the generic.UpdateView
. This method will
validate the user data using a given form. But sometimes an update is required which doesn’t need any user data being
sent, like toggling a flag or updating a timestamp. For these cases, just use the ToggleView
- it works basically the
same as the UpdateView
- except that “POST” is required and that no form has to be defined.
from ai_django_core.view_layer.views import ToggleView
class ToggleActiveStateView(ToggleView):
model = MyModel
template_name = 'myapp/my_model_edit.html'
def post(self, request, *args, **kwargs):
# Update object
obj = self.get_object()
obj.is_active = not obj.is_active
obj.save()
return render(self.request, self.template_name, {'object': obj})