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, then
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:
zone_id
which specifies the
id
of the drop area of your page (the default isfiledrop-zone
). unless usingRequiredFieldForm
this must be specified for each subsequentFileDropWidget
as thezone_id
must be unique.default_text
which specifies the text to be displayed when there is no file dropped (default is Drop a file…)
click_text
the text a button that allows choosing a file from the file system (default is or click here)
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
FileDropWidget
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' %}
Management Commands
postgres-test-connection
For local connections:
python manage.py postgres-test-connection www_hatherleigh_info patrick
For remote connections:
# Windows
python manage.py postgres-test-connection -host 10.3.1.7 -name www_hatherleigh_info -port 5432 -pass my-pass -user patrick
# Linux
python manage.py postgres-test-connection localhost 5432 www_hatherleigh_info patrick my-pass
Tip
Source code is in
base/management/commands/postgres-test-connection.py
.
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
Pagination
Tip
For basic Django pagination, see Pagination…
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
Tip
Source code in _paginate_with_parameters.html.
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
To escape
the parameters (make sure the string is safe), try:
from django.utils.html import escape
payment_details = escape(self.request.GET.get("payment_details"))
Tip
This may not be a good way to do this… (PK 12/01/2023)
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.project.tasks
list of tasks for the Workflow appweb.contact
the contact / enquiry page of the web site. This is used by the GDPR unsubscribe page.