Templates and Pages
Templates structure free-floating molecules and organisms into a context: a specific page.
Navigation
Users should always know where they are and how to get back where they came from.
Standard navigation
These navigation elements should be present on almost every page. The following assumes that the page descends from hqwebapp/bootstrap3/base_section.html, which virtually all pages should.
- Top navigation
-
The top navigation should always be visible to users.
Individual tabs are defined in
tabclasses.py. Which tab is highlighted depends on the
url_prefix_formats
defined for each tab. Which items appear in a tab's dropdown is determined bydropdown_items
. - Side navigation
-
The side navigation appears almost everywhere. Major exceptions are the dashboard, which has no navigation, and app manager, which has a custom sidebar corresponding to the app's structure. Other pages should almost certainly have a sidebar.
The sidebar is also defined in tabclasses.py, controlled by the
sidebar_items
functions.Temporary sub-items that appear only when on a specific page, such as the "Create Form Data Export" item that appears under the main "Export Form Data" item only when you're creating a new export, can be added using the
subpages
property.Dynamic item names, such as displaying the username when editing a web user, can be enabled by passing a function to the menu item's
title
. - Breadcrumbs
-
Breadcrumbs are defined by the
parent_pages
in the template context (see below for how to populate). Standard pages should have a breadcrumb for the top nav section (e.g., "Data"), for the left sidebar section if there is one (e.g., "Edit Data"), and for the page itself. Multi-step workflows such as case import should also be reflected in breadcrumbs. All items in the breadcrumbs should be links, except for the last (the current page).A few pages override the
page_breadcrumbs
block defined in hqwebapp/bootstrap3/base_section.html. This should only be done in exceptional circumstances.
Page headers
A page header at the top of the page's main content helps the user stay oriented, and this is a good place to briefly introduce the user to the page and link to documentation.
This is an area where the technical implementation isn't well-standardized.
When possible, set page_title
in the template's context.
Some pages do create the header by hand.
Page header
In-page navigation
If a page has multiple sections, you have a few options to visually separate them:
- Split into multiple pages.
-
Enclose each section in a panel.
Note that app manager has its own panel styling,
.panel-appmanager
. - Set each section up as a tab. Avoid using pills for navigation, because they look similar to the toggles used in forms.
Panels
Tabs for in-page navigation
Class-Based Views
Ultimately all views are based on django's TemplateView
class,
if the view has a user-facing UI.
If not, then inheriting from a simple
View
will probably be enough.
The BaseDomainView
If your view has something to do with the domain
, then
it will probably inherit from this view.
What benefits do you have from using this view?
- The permission
login_and_domain_required
is already in effect established. -
self.domain
(the name of the domain) is already available as an instance variable for your class and is automatically inserted into the template's context. -
self.domain_object
is already available as an instance variable (theDomain
instance with the domain name given byself.domain
. -
The
self.page_url
property is already avaiable, provided that your page doesn't require other args beyonddomain
. If it does (for instance if your url looks like/a/[domain]/my/page/[unique-id]
, then you'll have to re-implementself.page_url
with the required arguments.
Here is an example of a class inheriting from the BaseDomainView
.
class MyView(BaseDomainView): urlname = 'my_view'
(Required) urlname
will be the string referenced when you do the url mapping for
this view in it's application's urls.py
.
(e.g.
url(r'^myview/$', MyView.as_view(), name=MyView.urlname)
.
template_name = 'all_the_views/my_view.html' page_title = gettext_lazy("My View") section_title = gettext_lazy("All the Views") @property def section_url(self): return reverse(MySectionPageRoot.urlnale, args=[self.domain])
(Required) section_title
and section_url
are the title and url
for the "tab" or major content area that the view is inside. For instance, Settings,
Users, Data, Reports, etc. (See
Creating a Base View for my Section for best
practices on abstracting this out so that every View in the same Section doesn't have to
define this each time).
Both the section_url
and page_title
are used in rendering
the page's breadcrumbs (it's the first crumb).
@property def parent_pages(self): return [ { 'title': MyParentPageView.page_title, 'url': reverse(MyParentPageView.urlname, args=[self.domain]), } ]
(Optional) This is what shows up as the hierarchy in the Breadcrumbs for the page, and is what's in between the first crumb (the section) and the last crumb (the current page).
@property def page_name(self): return "{domain}'s {page_title}".format( domain=self.domain, page_title=self.page_title, )
(Optional) page_name
by default returns page_title
. It's a
way of adding request-specific context to the page_title
, but still having
access to the page_title
from a class level for general referencing of the page.
@property def page_context(self): return { 'form': MyForm(), }
(Optional) While page_context
is optional, it's very likely you'll implement it,
as that's where you return all the variables specific to that page. If you are starting to create
a hierarchy where you're then adding to the page_context
via.
# note, this is an example of what NOT to do @property def page_context(self): context = super(MyView, self).page_context context.update({ 'form': MyForm(), }) return context
Then, first, ask why you need that specific hierarchy. Perhaps you want to just inherit from
a Base Section view? Second, this should never happen with page_context
. That's what
main_context
is for, as that covers the context for more than one page (or in the
next example, a whole section).
Creating a Base View for My Section
The best practice is to create a Base Section View for all of the views
that will be part of that section (or tab) so that section_title
section_url
and any section-specific context can be added to the
main_context
.
class BaseMySectionView(BaseDomainView): section_name = "My Section" @property def section_url(self): return reverse(TheRootOfMySection.urlname, args=[self.domain] @property def main_context(self): context = super(BaseMySectionView, self).main_context context.update({ 'my_section_emoji': "bears", }) return context
main_context
here is optional but a good place to insert any section-specific
template contexts in.
def dispatch(self, request, *args, **kwargs): return super(MyView, self).dispatch(request, *args, **kwargs)
Functional Views
When using functional views, "fake" the same context that class-based views generate.
Manually mimic the context provided by BasePageView.main_context and rendered by hqwebapp/bootstrap3/base_section.html. This is an area where we could use better standards / shared code.
context.update({ 'current_page': { 'title': page_name, 'page_name': page_name, 'parents': [ { 'title': grandparent_title, 'page_name': grandparent_name, 'url': grandparent_url, }, { 'title': parent_title, 'page_name': parent_name, 'url': parent_url, }, ], }, 'section': { 'page_name': MySection.section_name, 'url': reverse("section_url_name"), }, });