Wafer/UI
Contents |
[edit] Templates, general layout
The base template used to render all wafer pages is wafer/templates/wafer/base.html. If you look at it, you can find a couple of {% block … %} definitions, which are intended to be filled, or "extended" in template-parlance (The Django template language (DTL) seems to be derived from [jinja.pocoo.org/ Jinja2]).
For instance, consider the wafer.pages template, which gets used by the [ class-based] ShowPage view. Essentially, the template just fills the "content" block of the base template (thereby replacing the default provided in the base template) with the rendered contents of the Page, and displays an Edit button if the authenticated user has permissions to edit the page content.
[edit] How to customise
The lean default layout of wafer is probably good enough, but then again we will want to make changes to it. There are two types of changes:
- Style changes, these are done almost entirely through CSS
- Layout changes, which require an override of the base template
To do just style changes, we need to include additional CSS files in the HTML head. Since the wafer base template provides a extra_head block that we could just fill, one way to do this would be to provide a new DebConf base template that extended the wafer base template. Unfortunately, however, so far wafer hard-codes the namespace of the base template in the various places the template is referenced (https://github.com/CTPUG/wafer/issues/168), forcing us to provide a custom base template anyway, to be able to e.g. display a sponsors sidebar.
[edit] Layout changes
All that's required to make wafer use our own template is to provide a file templates/wafer/base.html in the dc16.dc.o project. At the time of writing, this file differs from upstream only through the addition of the "sponsors" <div;gt; below the content (and the extra stylesheet, see below). If #168 gets fixed, we could conceivably do all this within the existing template to minimise the diff.
That said, wafer uses the Bootstrap framework, which spans HTML, CSS and JavaScript, and I suspect that there isn't as clear of a layout-style-content separation anymore as we might be used to, and that more control has been moved to the CSS layer, topic of the next section:
[edit] Style changes
Thankfully (well, or not, gotta love CSS…), the "looks" of the rendered pages are almost entirely controlled through CSS. The overridden base template loads a custom stylesheet which currently only turns all headers red.
Due to use of the Bootstrap framework, I suspect most customisation will happen in custom stylesheets. This is for someone else to figure out though…
[edit] Menus
At the top of the page(s) runs a horizontal menu, defined by the WAFER_MENUS list in settings.py. Entries are dictionaries, wherein a dictionary that has a menu key specifies a sub-menu (recursively) with items. Links specify name instead of menu. The "Past DebConfs" sub-menu is a good example:
{"menu": "previous-debconfs", "label": _("Past DebConfs"), "items": [ {"name": "debconf%i" % i, "label": _("DebConf%i" % i), "url": "http://debconf%i.debconf.org/" % i} for i in range(0, 16) ]},
The menu structure itself seems to be hard-coded in the settings, but it is possible to add pages to the sub-menus without touching the code. For instance, to add an entry to the "About" sub-menu, the following two steps are required:
- Create a so-called "container page" with a slug named exactly like the menu ID, "about" in this case. The content can't be empty but is completely irrelevant. Also, the title is ignored, and this container page needs not be checked to show up in the menu, but presumably should be excluded from the static site generation.
{"menu": "about", "label": _("About"), "items": [ {"menu": "meals", "label": _("Meals (settings)"), "items":[]} ]},
- Create a regular page (with content), set this new (container) page "About" as its parent, and tick it to show up in the menu.
Unfortunately, I have yet to figure out sub-submenus. Adding to the menus items in settings.py an entry for e.g. "meals", creating a container page for "meals", and then creating a page "lunch" will mean two entries in the "About" menu:
- "Meals" (data taken from settings.py), with the menu name as the URL, e.g. /about/meals in this case, so actually exposing the container page.
- "Lunch" as second entry of the "About" menu (and not a sub-entry of "Meals"), though with a URL /about/meals/lunch.
If "Meals" in the database is selected to show up in the menu, there will be two entries for "Meals" under "About", one from the settings and one from the database. This will potentially get quite confusing.
[edit] Open questions
- Figure out sub-menus and how these could be created from the database.
[edit] Views and templates
In a nutshell, a request to Django gets routed to a view function by urls.py. A view function (or a class-based view) takes the HTTP request and is expected to return a HTTP response. Instead of hand-crafting HTML, views generally use templates, which get evaluated in a "context", and this context is provided by the view function. It's as simple as this, conceptually:
# highly simplified, and this won't work as you need Template and Context objects: 'Hello, {{ name }}\n'.render( {'name':'world' } )
Wafer provides templates for all its models and most operations. For instance, looking into wafer/talks/templates/wafer.talks, one can find templates used to display talk details, a list of talks, the talk submission/edit form, and the "deletion is permanent" warning displayed when a talk is about to be erased. Customisation for DebConf means forking those files into dc16.dc.o/templates/wafer.talks/, similar to the base template above.
It's notable how templates are really part of the view function and have access to all the data, including function calls etc.. Combined with conditional logic constructs of the templating language, templates themselves often implement e.g. access control or data post-processing.
Side note: while it certainly seems feeble to me to rely on access control in the view layer (rather than object-level mandatory access control, such as provided by Zope), it's the way things are done in Django, and there is no way around the view layer, at least I am fairly sure about that. So all we have to do is write faultless authorization code, and if we screw up, well, it's only personal data on the line…
With most of the logic in the templates, the view functions (well, actually wafer uses class-based views, which are more flexible and modern) can stay rather slim. Look at, for instance, the class handling the detail view of a talk: it merely injects a bit of access control and also ensures that the template has access to can_edit. All the rest is in the template, and deep within Django.
[edit] Forms
Traditionally, forms have been the source of much grief in web development, and they still are. Fortunately, Django already simplifies this (cf. the admin interface, which is entirely auto-created), and wafer additionally uses crispy-forms, which let you " easily build, customize and reuse forms using your favorite CSS framework, without writing template code and without having to take care of annoying details."
And indeed, looking at the TalkCreate view references a TalkForm class, in which all we do is define which fields to show (possibly overriding the widgets) in the metadata, and append a button a delete button.