base¶
BaseMixin¶
https://gitlab.com/kb/base/blob/master/base/view_utils.py
The BaseMixin
class adds the following to the template context:
path
:self.request.path
orhome
if the path is/
today
: todays date (datetime.today()
)request_path
:self.request.path
Tip
To add extra context we can use the BASE_MIXIN_CONTEXT_PLUGIN
plugin system.
For an example of this, see Views
(from the apps
app).
Bullet (Hide)¶
If you create a form with a RadioSelect
widget e.g:
send_email = forms.ChoiceField(widget=forms.RadioSelect, choices=CHOICES)
Then you can hide the bullets on the page by using the kb-hide-bullet
style e.g:
{% include '_form.html' with form_class='kb-hide-bullet' %}
Checkbox¶
We have (very nice) inline checkboxes:

To make checkbox field and label appear on a single line add:
{% include '_form.html' with inline_checkbox=True ... %}
Tip
Don’t miss the nice formatting for Forms
For more information, take a look at the documentation in the templates:
Date Picker¶
Tip
The code for the Zebra Datepicker is included in our base.html
template.
Using RequiredFieldForm
will automatically set date fields to use the zebra
datepicker control e.g:
# forms.py
from base.form_utils import RequiredFieldForm
class EventForm(RequiredFieldForm):
Warning
If your date control isn’t working as a date picker, then check
your form code to see if you call
self.fields[name].widget.attrs.update({'class'...
on the field. This will overwrite the update done by the
__init__
method on RequiredFieldForm
.
File and Image Upload¶
If you include the _form.html
template and you want to upload files, the
add the multipart
option:
{% include '_form.html' with multipart=True %}
FileDropInput
Widget¶
To display a drag and drop file upload, set the widget for that field to
FileDropInput
. If your form inherits from RequiredFieldForm all
FileField
and ImageField
fields will automatically use the
FileDropInput
widget. The zone_id for the first field will be
filedrop-zone
and filedrop-1-zone
for the second, filedrop-2-zone
for the third etc.
For example, assuming the following model is defined in your models.py
:
class Document(TimedCreateModifyDeleteModel):
file = models.FileField(upload_to='document')
preview = models.FileField(upload_to='image')
description = models.CharField(max_length=256)
class Meta:
verbose_name = 'Document'
verbose_name_plural = 'Documents'
def __str__(self):
return '{}: {}'.format(self.file, self.description)
You can define a model form called DocumentForm
as follows:
from django import forms
from base.form_utils import FileDropInput
from .models import Document
class DocumentForm(models.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for name in ('file', 'description'):
self.fields[name].widget.attrs.update(
{'class': 'pure-input-2-3'}
)
class Meta:
model = Document
fields = (
'file',
'preview',
'description',
)
widgets = {
'file': FileDropInput()
'preview': FileDropInput(
zone_id="filedrop-1-zone",
default_text="Optional text to replace 'Drop a file ...'"
click_text="Optional text to replace 'or click here...'"
)
}
or using RequiredFieldForm
(which configures the each widget with the
appropriate zone_id
) as follows:
from base.form_utils import RequiredFieldForm
from .models import Document
class DocumentForm(RequiredFieldForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for name in ('file', 'preview', 'description'):
self.fields[name].widget.attrs.update(
{'class': 'pure-input-2-3'}
)
class Meta:
model = Document
fields = (
'file',
'preview',
'description',
)
When creating a FileDropWidget you can optionally pass three parameters:
If you’re inheriting from RequiredFieldForm you can change the values for
zone_id, default_text and click_text in the form’s __init__
using either
of the following:
Update the attrs member of the widget as follows:
self.fields['preview'].widget.attrs.update({
'default_text': "Drop a preview image file...",
'click_text': "or click here to choose one"
})
Or specify a new widget:
self.fields['preview'].widget = FileDropInput(
zone_id="filedrop-1-zone",
default_text="Drop a preview image file...",
click_text="or click here to choose one"
)
See the example_base
app File Drop Demo for an example form with two
FileFields. See the code in example_base/forms.py
Using FileDropWidget on your page¶
filedrop.css
defines a pure-css style appearance of a FileDropWidget for the
standard zone_ids (these are filedrop-zone
, filedrop-1-zone
,
filedrop-2-zone
and filedrop-3-zone
). If your page inherits from the
base app’s base.html then this is already included. Otherwise include it on
your page with the code:
<link rel="stylesheet" type="text/css" href="{% static 'base/css/filedrop.css' %}">
It probably makes sense to include this above your project css file as this allows the styles to be overidden.
filedrop.js
initialises the filedrop zones created when a FileDropWidget is
rendered for the standard zone_ids.
If your template does not inherit from the base app’s base.html, include this on your page with the code:
<script src="{% static 'base/js/filedrop.js' %}"></script>
Including the script at the end of the page ensures that DOM is loaded.
Advanced control of FileDropWidget¶
If you want to change the appearance of your FileDropWidget you will need to create css rules for some or all of the following selectors:
#filedrop-zone
#filedrop-zone .filedrop-file-name
#filedrop-zone .filedrop-click-here
// please note if you have more than one FileDropWidget on your page you
// will need to define additional selectors you can of course group your
// selectors to get a similar appearance for each widget.
If you want to use a different zone_id in your form or add more than 4
FileDropWidgets to your page then in addition to creating a style as above
you must also call dropZoneManager
with the zone_id
of each non standard
zone_id
as follows:
<!-- insert after filedrop.js on you page -->
<script>
dropZoneManager("non-standard-zone-id");
</script>
This should appear below filezone.js
on your page.
Flash of Unstyled Content (FOUC)¶
From An Accessible Way to Stop Your Content From Flashing (FOUC)
Include our _fouc.html
template:
{% block content %}
{% include 'base/_fouc.html' %}
Add the stuffIDontWantToFlash
style to any elements you want to hide on the
initial render:
<div class="pure-u-1 stuffIDontWantToFlash">
<div id="tree"></div>
</div>
Before working with the element, remove the style (to make it visible):
function toggleFolderTree(event) {
$(".stuffIDontWantToFlash").removeClass("stuffIDontWantToFlash");
$("#folderTree").slideToggle();
Forms¶
Label¶
To hide the :
character on a form, set the first parameter on the model
field to " "
e.g:
address_two = models.CharField(" ", max_length=100, blank=True)
This sets the verbose_name
for the field (Django, Verbose field names).
The _form_field.html template checks to see if the verbose_name
is a
space (field.label != " "
) and hides the :
character if it is.
Aligned¶
To create an aligned form that displays well on both desktop and mobile use this markup:
<div class="pure-g">
<div class="pure-u-1 pure-u-lg-2-3">
{% include '_form.html' with legend='Aligned Form' multipart=True inline_checkbox=True aligned=True %}
</div>
</div>
Note
In forms.py
set the class for the fields to pure-input-1
e.g.
self.fields[name].widget.attrs.update({"class": "pure-input-1"})
The aligned
parameter is set to true so the label and the field will be
displayed on the same line and any help text will be displayed below the
field. Your template should include both the pure-min.0.6.0.css
and
base.css
stylesheets. The inline_checkbox
parameter is explained
above see Checkbox
Stacked¶
A Stacked form can be created using this technique too:
<div class="pure-g">
<div class="pure-u-1 pure-u-lg-2-3">
{% include '_form.html' with legend='Stacked Form' multipart=True inline_checkbox=True %}
</div>
</div>
The purecss class pure-u-lg-2-3
will display the form on two thirds of a
large screen and on smaller screens the pure-u-1
class will allocate ~100%
In forms.py
the class for the input field should be set as pure-input-1
which is a pure class that sets the width of the field to 100%. For aligned
forms base.css
modifies pure-input-1
width to calc(100% - 12rem)
(12rem is the width of a the label on an aligned form). A sample form is shown
below:
from django import forms
class CoolForm(forms.Form):
repeat = forms.ChoiceField(choices=((0, 'Weekly'), (1, 'Monthly'), ))
times = forms.IntegerField()
reason = models.CharField(max_length=256)
all_day = forms.BooleanField(required=False)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for name in ("repeat", "times", "reason", ):
self.fields[name].widget.attrs.update({'class': 'pure-input-1'})
Google Analytics¶
To add the site tag to a site, add google_site_tag
to the context and:
{% include 'base/_google_site_tag.html' %}
Models¶
RetryModel
¶
This model will handle retry operations e.g. sending an email.
You can see an example of this in the Message
model in the mattermost
app:
https://gitlab.com/kb/mattermost/blob/master/mattermost/models.py#L109
Model requirements are as follows:
Use a model manager inherited from
RetryModelManager
.Create a
DEFAULT_MAX_RETRY_COUNT
(in your model inherited fromRetryModel
).
DEFAULT_MAX_RETRY_COUNT = 5
The DEFAULT_MAX_RETRY_COUNT
can be used when creating an instance of your
model (perhaps in a create_
… method in your model manager)e.g:
max_retry_count=self.model.DEFAULT_MAX_RETRY_COUNT,
Create a
process
method (will be called by theprocess
method inRetryModelManager
). This method must returnTrue
for success andFalse
for fail e.g.
def process(self):
"""Process the message.
.. note:: This method is running inside a transaction.
This method is called by ``RetryModelManager``
(see ``base/model_utils.py``).
"""
result = False
response = requests.post(self.channel.url)
if HTTPStatus.CREATED == response.status_code:
result = True
else:
logger.error("Cannot post message to Mattermost")
return result
Model manager requirements are as follows:
Create a
current
method which can simple returnall
rows.
def current(self):
return self.model.objects.exclude(deleted=True)
TimedCreateModifyDeleteModel
¶
Warning
We don’t delete data (unless there is a specific requirement for it).
The TimedCreateModifyDeleteModel
has set_deleted
, is_deleted
and
undelete
methods. To use the class:
class ContactManager(models.Manager):
def current(self):
return self.model.objects.exclude(deleted=True)
class Contact(TimedCreateModifyDeleteModel):
# ...
objects = ContactManager()
Tip
To mark an object as deleted in a Django view, see UpdateView not DeleteView
Paginate¶
If you have a GET
form (search or similar) on your view, then you will want
to include the URL parameters with the page
number:
{% include 'base/_paginate_with_parameters.html' %}
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
get_parameters = self.request.GET.copy()
if self.page_kwarg in get_parameters:
del get_parameters[self.page_kwarg]
context.update(
dict(
get_parameters=get_parameters.urlencode(),
)
)
return context
PDFObject¶
In the HTML template:
<div id="pdf-viewer"></div>
At the end of the template:
{% block script_extra %}
{{ block.super }}
{% include 'base/_pdfobject.html' %}
<script>PDFObject.embed("{% url 'document.download' document.pk %}", "#pdf-viewer");</script>
<style>
.pdfobject-container { height: 800px;}
.pdfobject { border: 1px solid #666; }
</style>
{% endblock script_extra %}
Note
If you get Failed to load PDF document in Google Chrome, see PDFObject
RedirectNextMixin¶
from base.view_utils import BaseMixin, RedirectNextMixin
Note
This example is for use with an update view and the POST
method.
Add the request
context processor to settings:
'context_processors': [
# ...
'django.template.context_processors.request',
In the calling template:
<a href="{% url 'dash.document.update' document.pk %}?next={{ request.path }}" class="pure-menu-link">
Tip
If your URL includes extra parameters, then try the following (from Stack Exchange, Django next with query parameters):
<a href="{% url 'dash.document.update' document.pk %}?next={{ request.get_full_path|urlencode }}" class="pure-menu-link">
In the view, add the RedirectNextMixin
:
from django.contrib.auth import REDIRECT_FIELD_NAME
from base.view_utils import BaseMixin, RedirectNextMixin
class ContactDetailView(
LoginRequiredMixin, StaffuserRequiredMixin,
RedirectNextMixin, BaseMixin, DetailView):
If required, add a get_success_url
method:
def get_success_url(self):
next_url = self.request.POST.get(REDIRECT_FIELD_NAME)
if next_url:
return next_url
else:
return reverse('dash.document.detail', args=[self.object.pk])
Our standard _form.html
template includes this section:
{% if next %}
<input type="hidden" name="next" value="{{ next }}" />
{% endif %}
</form>
In the menu of the form template:
<li class="pure-menu-item">
{% if next %}
<a href="{{ next }}" class="pure-menu-link">
<i class="fa fa-reply"></i>
</a>
{% else %}
<a href="{% url 'project.settings' %}" class="pure-menu-link">
<i class="fa fa-reply"></i>
Settings
</a>
{% endif %}
</li>
RequiredFieldForm¶
https://gitlab.com/kb/base/blob/master/base/form_utils.py
e.g:
class SnippetForm(RequiredFieldForm):
URL¶
Parameters¶
from django.urls import reverse
from base.url_utils import url_with_querystring
url = url_with_querystring(
reverse(order_add),
responsible=employee.id,
scheduled_for=datetime.date.today(),
)
>>> http://localhost/order/add/?responsible=5&scheduled_for=2011-03-17
Standard¶
We have a few standard URLs:
logout
login
project.home
the home page of the web site.project.dash
the home page for a member of staff (or logged in user if the project requires it).project.settings
, the project settings. Usually only accessible to a member of staff.web.contact
the contact / enquiry page of the web site. This is used by the GDPR unsubscribe page.