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.