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.