base

https://gitlab.com/kb/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 or home 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:

_images/inline-checkbox.png

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 is filedrop-zone). unless using RequiredFieldForm this must be specified for each subsequent FileDropWidget as the zone_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:

  1. Use a model manager inherited from RetryModelManager.

  2. Create a DEFAULT_MAX_RETRY_COUNT (in your model inherited from RetryModel).

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,
  1. Create a process method (will be called by the process method in RetryModelManager). This method must return True for success and False 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:

  1. Create a current method which can simple return all 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 app

  • web.contact the contact / enquiry page of the web site. This is used by the GDPR unsubscribe page.