base **** .. highlight:: python 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 :ref:`apps_context_for_base_mixin` (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: .. image:: ./misc/inline-checkbox.png :scale: 60 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: - https://gitlab.com/kb/base/blob/master/base/templates/_form.html (``inline_checkbox``) - https://gitlab.com/kb/base/blob/master/base/templates/_form_field.html (``input_type``) 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: .. code-block:: html {% 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:: 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: .. code-block:: html 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: .. code-block:: html 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: .. code-block:: html {% block content %} {% include 'base/_fouc.html' %} Add the ``stuffIDontWantToFlash`` style to any elements you want to hide on the initial render: .. code-block:: html
Before working with the element, remove the style (to make it visible): .. code-block:: html function toggleFolderTree(event) { $(".stuffIDontWantToFlash").removeClass("stuffIDontWantToFlash"); $("#folderTree").slideToggle(); .. _app_base_forms: 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: .. code-block:: html
{% include '_form.html' with legend='Aligned Form' multipart=True inline_checkbox=True aligned=True %}
.. 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: .. code-block:: html
{% include '_form.html' with legend='Stacked Form' multipart=True inline_checkbox=True %}
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: .. code-block:: python 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'}) .. _base_google_analytics: Google Analytics ================ To add the site tag to a site, add ``google_site_tag`` to the context and: .. code-block:: html {% 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: 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, 3. 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) .. _timed_create_modify_delete_model: ``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 :ref:`update_view_not_delete_view` .. _pagination-with-parameters: Pagination ========== .. tip:: For basic Django pagination, see :doc:`dev-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::
At the end of the template:: {% block script_extra %} {{ block.super }} {% include 'base/_pdfobject.html' %} {% endblock script_extra %} .. note:: If you get *Failed to load PDF document* in Google Chrome, see :ref:`pdf_object_failed_to_load` 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: .. code-block:: html .. tip:: If your URL includes extra parameters, then try the following (from `Stack Exchange, Django next with query parameters`_): .. code-block:: html 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: .. code-block:: html {% if next %} {% endif %} In the menu of the form template: .. code-block:: html
  • {% if next %} {% else %} Settings {% endif %}
  • 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(), ) .. code-block:: bash >>> 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 :doc:`app-workflow` app - ``web.contact`` the contact / enquiry page of the web site. This is used by the :doc:`app-gdpr` unsubscribe page. .. _`_form_field.html`: https://gitlab.com/kb/base/blob/master/base/templates/_form_field.html#L23 .. _`_paginate_with_parameters.html`: https://gitlab.com/kb/base/-/blob/master/base/templates/base/_paginate_with_parameters.html .. _`An Accessible Way to Stop Your Content From Flashing (FOUC)`: https://alexsexton.com/blog/2010/09/an-accessible-way-to-stop-your-content-from-flashing-fouc/ .. _`Django, Verbose field names`: https://docs.djangoproject.com/en/2.1/topics/db/models/#verbose-field-names .. _`Stack Exchange, Django next with query parameters`: https://stackoverflow.com/questions/38167348/django-next-with-query-parameters .. _`Zebra Datepicker`: https://github.com/stefangabos/Zebra_Datepicker