How-tos

Create CRUD views for a model

Given a model in app named library (source code taken from the demo project ) in project’s repo:

# library/models.py
class Author(models.Model):
    name = models.CharField("Name", max_length=128)
    penname = models.CharField("Pen Name", max_length=128)
    age = models.SmallIntegerField("Age", null=True, blank=True)

    class Meta:
        ordering = ('name',)
        verbose_name = "Author"
        verbose_name_plural = "Authors"

    def __str__(self):
        return self.name

Declare a PopupCrudViewSet derived class in app’s views.py:

# library/views.py
from popupcrud.views import PopupCrudViewSet

class AuthorViewSet(PopupCrudViewSet):
    model = Author
    fields = ('name', 'penname', 'age')
    list_display = ('name', 'penname', 'age')
    list_url = reverse_lazy("library:authors")
    new_url = reverse_lazy("library:new-author")

    def get_edit_url(self, obj):
        return reverse_lazy("library:edit-author", kwargs={'pk': obj.pk})

    def get_delete_url(self, obj):
        return reverse_lazy("library:delete-author", kwargs={'pk': obj.pk})

Wire up the individual CRUD views generated by the viewset to the app URL namespace in urls.py:

# library/urls.py
urlpatterns= [
    url(r'^authors/$', views.AuthorCrudViewset.list(), name='authors'),
    url(r'^authors/new/$', views.AuthorCrudViewset.create(), name='new-author'),
    url(r'^authors(?P<pk>\d+)/edit/$', views.AuthorCrudViewset.update(), name='edit-author'),
    url(r'^authors(?P<pk>\d+)/delete/$', views.AuthorCrudViewset.delete(), name='delete-author'),
    ]

In the projects root urls.py:

# demo/urls.py
urlpatterns + [
    url(r'^library/', include('library.urls', namespace='library')),
]

Control access using permissions

In your CRUD ViewSet, declare the permissions required for each CRUD view as:

class AuthorViewSet(PopupCrudViewSet):
    model = Author
    ...
    list_permission_required = ('library.list_authors',)
    create_permission_required = ('library.add_author',)
    update_permission_required = ('library.change_author',)
    delete_permission_required = ('library.delete_author',)

However, if you want to determine the permission dynamically, override the get_permission_required() method and implement your custom permission logic:

class AuthorViewSet(PopupCrudViewSet):
    model = Author
    ...

    def get_permission_required(self, op):
        if op == 'create':
            # custom permission for creating new objects

        elif op == 'delete':
            # custom permission for updating existing objects
        else:
            return super(AuthorViewSet, self).get_permission_required(op)

Create a model object from its FK select box in another form

This allows user to create new instances of a model while they are working on a form which has a FK reference to the model for which PopupCrudViewSet views exist. This allows objects to be added seamlessly without the user switching context to another page to add the object and then coming back to work on the form.

To illustrate with an example:

from popupcrud.widgets import RelatedFieldPopupFormWidget

class AuthorRatingForm(forms.Form):
    author = forms.ModelChoiceField(queryset=Author.objects.all())
    rating = forms.ChoiceField(label="Rating", choices=(
        ('1', '1 Star'),
        ('2', '2 Stars'),
        ('3', '3 Stars'),
        ('4', '4 Stars')
    ))

    def __init__(self, *args, **kwargs):
        super(AuthorRatingForm, self).__init__(*args, **kwargs)
        author = self.fields['author']
        # Replace the default Select widget with PopupCrudViewSet's
        # RelatedFieldPopupFormWidget. Note the url argument to the widget.
        author.widget = RelatedFieldPopupFormWidget(
            widget=forms.Select(choices=author.choices),
            new_url=reverse_lazy("library:new-author"))


class AuthorRatingView(generic.FormView):
    form_class = AuthorRatingForm

    # rest of the View handling code as per Django norms

In the above form, the default widget for author, django.forms.widgets.Select has been replaced by RelatedFieldPopupFormWidget. Note the arguments to the widget constructor – it takes the underlying Select widget and a url to create a new instance of the model.

Use Select2 instead of native Select widget

Select2 is an advanced version the browser native Select box allowing users navigate through fairly large selection list using keystrokes. Select2 is excellently supported in Django through the thirdparty app django-select2. Replacing the native django.forms.Select control with equivalent django_select2.forms.Select2Widget widget is extremely easy:

from django_select2.forms import Select2Widget
from popupcrud.widgets import RelatedFieldPopupFormWidget

class AuthorRatingForm(forms.Form):
    author = forms.ModelChoiceField(queryset=Author.objects.all())
    rating = forms.ChoiceField(label="Rating", choices=(
        ('1', '1 Star'),
        ('2', '2 Stars'),
        ('3', '3 Stars'),
        ('4', '4 Stars')
    ))

    def __init__(self, *args, **kwargs):
        super(AuthorRatingForm, self).__init__(*args, **kwargs)
        author = self.fields['author']
        # Replace the default Select widget with PopupCrudViewSet's
        # RelatedFieldPopupFormWidget. Note the url argument to the widget.
        author.widget = RelatedFieldPopupFormWidget(
            widget=forms.Select2Widget(choices=author.choices),
            new_url=reverse_lazy("library:new-author"))

Note how Select2Widget is essentially a drop in replacement for the native django.forms.Select widget. Consult django-select2 docs for instructions on integrating it with your project.

Providing your own templates

Out of the box, popupcrud comes with its own templates for rendering all the CRUD views. For most use cases this ought to suffice. For the detail view, the default template just renders the object name in the popup. Typically, you might want to include additional information about an object in its detail view. To do this, implement <model>_detail.html in your app’s template folder and this template will be used to display details about an object.

One point to highlight about templates is that since popupcrud can work in both legacy(like Django admin) and the more modern Web 2.0 modal dialog based modes, it needs two templates to render the content for the two modes. This is necessary as contents of a modal popup window should only contain details of the object without site-wide common elements such as headers and menu that is usually provided through a base template whereas the dedicated legacy crud page requires all the site-wide common artifacts. This problem exists for all CRUD views - create, update, delete and detail. Therefore, for consistency across different CRUD views, popupcurd uses a standard file naming convention to determine the template name to use for the given CRUD view mode.

This convention gives first priority to Django generic CRUD views’ default template file name. If it’s present it will be used for the CRUD view. However, if the view is to be rendered in a modal popup window, which should not have site-wide common artifacts, popupcrud appends _inner to the base template filename (the part before .html). So if you want to display details of a object of class Book in a modal popup, you have to implement the template file book_detail_inner.html. However, if you disable popups for the detail view, you have to implement book_detail.html. The difference between the two being that *_inner.html only renders the object’s details whereas book_detail.html renders the object’s details along with site-wide page common artifacts such as header, footers and/or sidebars.

One strategy is to provide both templates and organize them using the {% include %} tag. With this pattern, book_detail.html would look like this:

{% extends "base.html" %}
{% block content %}
{% include "book_detail_inner.html" %}
{% endblock content %}

The same pattern is applicable to other CRUD views as well where template files such as book_form.html, confirm_book_delete.html are looked for first before using popupcrud’s own internal templates.

Use the formset feature

To add a formset to edit objects of a child model, override the PopupCrudViewSet.get_formset_class() method in your derived class returning the BaseModelFormSet class which will be used to render the formset along with the model form. Formsets are always rendered at the bottom of the model form.

To illustrate with an example, assume that we have a Book table with the following definition:

class Book(models.Model):
    title = models.CharField('Title', max_length=128)
    isbn = models.CharField('ISBN', max_length=12)
    author = models.ForeignKey(Author)

    class Meta:
        ordering = ('title',)
        verbose_name = "Book"
        verbose_name_plural = "Books"

    def __str__(self):
        return self.title

To allow the user to edit one or more Book objects while creating or editing a Author object, you just need to extend the AuthorCrudViewset in the previous example to:

from django import forms
from popupcrud.views import PopupCrudViewSet

class AuthorViewSet(PopupCrudViewSet):
    model = Author
    ...

    def get_formset_class(self):
        return forms.models.inlineformset_factory(
            Author,
            Book,
            fields=('title', 'isbn'),
            can_delete=True,
            extra=1)

Now when the modal for create or edit views will show a formset at the bottom with two fields – Book.title and Book.isbn. A button at the bottom of the formset allows additional formset rows to be added. Each formset row will also have a button at the right to delete the row.

The sample above uses the django formset factory function to dynamically build a formset class based on models parent-child relationship. You may also return a custom formset class that is derived from BaseModelFormSet with appropriate specializations to suit your requirements.

BaseModelFormSet base class requirement is due to PopupCrudViewSet invoking the save() method of the class to save formset data if all of them pass the field validation rules.

A note about formset feature. Since formset forms are rendered in a tabular format, and since the modal dialogs are not resizable, there is a limit to the number of formset form fields that can be specified before it becomes unusable for the user. To cater for this, PopupCrudViewSet now allows the modal sizes to be adjusted through the modal_sizes class attribute. This allows you to specify the appropriate modal size based on your form and formset field count & sizes. See modal sizes.