In order to customize Argus, we a number of techniques. These are described below. Some of this information can also be found here
note: A Django application is organized into Apps (see below). In order to prevent confusion, in this write-up, a full Django application is referred to as a Site, and the individual apps as Apps.
Every Django Site is goverened by a settings
file that contains the settings for running a
particular instance of a Site. Settings may import other settings files. For geant-argus
we have
settings files located in the src/geant_argus/settings
. We have settings for dev
and prod
for
running geant-argus in either development or deployed environments (test/uat/prod all make use of
the prod.py
settings. Settings tied to a specific deployed environment are managed using puppet).
There is also the base.py
settings file which contains settings that are valid for all instances
of geant-argus. This file includes argus.site.settings.base
as the base Argus settings.
Django has the concept of Apps. An app is a collection of code and/or data models and a Django
site is a collection of one of more Apps. Which Apps are loaded is indicated in the
INSTALLED_APPS
setting. For Geant Argus we have the following additional apps
geant_argus
an app containing our all our customizationsargus_site
a references to theargus.site
package as an app (required for some template overriding)django_htmx
the generic package that implements htmx for a django Siteargus.htmx
the (new) Argus front-end using htmx
Some of our extra Apps are appended to the default INSTALLED_APPS
setting, while others are
prepended. When resolving a certain resource, such as a template, Django traverses the installed
Apps in forward direction and returns the first resource that matches its name. This means that
prepended Apps can override existing resources, while appended Apps cannot.
See also: https://docs.djangoproject.com/en/5.0/ref/applications/
Like many web frameworks, Django uses templates for rendering (html) pages. Templates are
identified by a their relative path in valid templates/
directories. Because every App can have
their own templates/
directory, it is possible to override an existing template by creating a
new file with the same name in another App templates/
directory. We use this for example for
implementation of the incidents details page htmx/incidents/incident_detail.html
which overrides
a template from the argus.htmx
App.
note Argus has a default setting TEMPLATES[0]["DIRS"]
that disables Django's behaviour of
resolving templates in the apps' directories. We reset this setting in our base.py
settings file
note by reverting this setting, Django can by default no longer resolve the templates in the
argus.site
package, since this is not marked as a Djanog App. We mark this as a Django app by
creating our own custom AppConfig
class that refers to this package (see
geant_argus.argus_site.apps.py
) so that argus.site
templates are resolved.
See also: https://docs.djangoproject.com/en/5.0/howto/overriding-templates/
While not necessarily a customization of existing Argus behaviour, it is useful to mention template
tags. Template tags (and filters) are the mechanism with which to extend Django's templating
functionality. We currently don't implement our own template tags, but do have some template
filters. Djanog does not allow arbitrary expressions when rendering templates. Instead, if you want
to modify a value passed into a template, you need to use a filter. Filters can
be defined in python module inside an App's templatetags/
directory:
# myfilters.py
from django import template
register = template.Library()
@register.filter
def my_filter(arg1, arg2):
...
my_filter
can then be used inside a Django template as following:
{% load myfilters %}
{{ my_value|my_filter:some_argument }}
The filter must first be loaded through the {% load myfilters %}
directive. This searches
every installed app for a myfilters.py
in its templatetags
directory and loads that file. The
subsequent template interpolation call invokes my_filter
with my_value
as its first argument
and some_argument
as its second argument. Filters can have one or two arguments. For filters that
have only one argument you can omit the :...
when invoking the filter. The return value of the
filter is used when rendering the template.
See also: https://docs.djangoproject.com/en/5.0/howto/custom-template-tags/
Another useful way to add some functionality is the concept of context processors. When rendering a
template in a Django view, you must supply a context that contains all the variables that you need
to access in your template. Some variables are tied to that specific view, but sometimes you want
to add a variable into every view of the Site, such as the current logged in user, or another
global variable. In that case it is useful to use a context processor. These are functions that,
when registered, are called every time a template is rendered. They take in the current request
as a single argument and must return a dictionary. This dictionary is then merged with the current
context and eventually passed to the target template. A context processor function can
be activated by adding a reference to it to the TEMPLATES[0]["OPTIONS"]["context_processors"]
setting. An example of a context processor we use is the geant_argus.context_processors.geant_theme
context processor, which inject a theme
variable (see also Theme below)
See also: https://docs.djangoproject.com/en/5.0/ref/templates/api/#writing-your-own-context-processors
Additional url endpoints (views) can be added to the url_patterns
variable in geant_argus.urls
.
That module is assigned to the ROOT_URLFCONF
setting and extends the default argus.site.urls
urls.
Django has the concept of middleware. These are functions or classes that can add behaviour to
every request made in the Site. They take in the current Request
and a get_response
function
and must return a Response
, usually the result of the get_response
function. They can also
terminate early by returning a custom Response
(for example a 401 Unauthorized
for
authentication middleware) The get_response
function calls the next middleware in the chain
(middleware can be stacked!) or the view itself.
We currently have added middleware to validate incident metadata (although this may be changed in the future in case Argus exposes a hook for validating incident metadata directly)
See also: https://docs.djangoproject.com/en/5.0/topics/http/middleware/
A way to update the data model is to add database migrations. These can add new resources models
(tables) to the database or update existing ones. It is not recommended to add or modify columns
to existing tables, since the code using that model is most likely not goverened by us and
therefore unaware of the changes we made, but it is possible to make small tweaks to a table, such
as adding an index. Migrations can depend on other migrations in the same app but also on
migrations in a different app. See geant_argus.migrations.0002_incident_metadata_description_gin_idx.py
for an example.
The incident listing table has a default set of columns. For geant-argus we want to customize
these columns to show information that is relevant to Geant for every incident. This can be done
by overriding or extending the INCIDENT_TABLE_COLUMNS
setting. See also
argus.htmx.settings.INCIDENT_TABLE_COLUMNS
For geant-argus we implement custom incident filtering capabilities. We need to be able to filter
by custom boolean rules (combination of AND and OR filters) and we provide this functionality
through a filtering backend plugin. The api of this plugin is in its early stages, but currently
involves pointing Argus to a module containing the relevant objects through the
ARGUS_FILTER_BACKEND
settings. See also argus.filter.default
for which objects to expose and
their default implementation