Package: django-phac-aspc-helpers
Django HelpersProvides a series of helpers to provide a consistent experience across
PHAC-ASPC's Django based projects.Third party applicationsBy using this library, the following django applications will automatically be
added to your django project:django-axesdjango-environdjango-modeltranslationdjango-structlogQuick startpipinstalldjango-phac_aspc-helpers# settings.pyfromphac_aspc.django.settings.utilsimportconfigure_apps,configure_middlewarefromphac_aspc.django.settingsimport*INSTALLED_APPS=configure_apps([...])MIDDLEWARE=configure_middleware([...])NoteReplace [...] above with the corresponding existing configuration from
your project.Theconfigure_appsandconfigure_middlewareutility functions will insert
the appropriate applications into their correct location in your project's
application and middleware lists.# urls.pyfromphac_aspc.django.helpers.urlsimporturlpatternsasphac_aspc_helper_urlsurlpatterns=[...*phac_aspc_helper_urls,]NoteAdd*phac_aspc_helper_urlsto the list orurlpatternsexported by
your project'surlsmodule.JinjaIf you are using jinja, you can use the phac_aspc jinja utils and django
templatetags by adding them to the global environment like this:importphac_aspc.django.helpers.templatetagsasphac_aspcimportphac_aspc.django.helpers.jinja_utilsasinclude_from_dtldefenvironment(**options):env=Environment(**options)env.globals.update({"phac_aspc":phac_aspc,"include_from_dtl":include_from_dtl,})returnenvFor more information, refer to the Jinjadocumentation.Environment variablesSeveral settings or behaviours implemented by this library can be controlled via
environment variables. By default, these environment variables are read from the.envfile in your project's root. This is done via thedjango-environlibrary; refer to their documentation on how to format special data types like lists.Alternatively, these environment variables can be declared in yoursettings.pyitself. There are two important caveats when doing so:settings.pydeclarations take precedence over any instances of the same
env var in your.envfileany env vars declared insettings.pyfor this librarymustbe declaredbeforeany imports fromphac_aspcoccur!similarly, you should not consumephac_aspcmodules anywhere that executes
prior to Django's consumption of your app's settings module (e.g. inmanage.py)phac_aspcmodules that don't, directly or indirectly, depend on these
env vars are theoretically safe anywhere,butwe don't currently identify
these modules, or make promises that any given module won't start depending
on env vars in the futureAll env vars for this library are prefixed withPHAC_ASPC_. AvailablePHAC_ASPC_env vars are listed under their coresponding "feature" sections below.global_from_envThis library also provides a utility that automatically declares an un-prefixed module
level global from a prefixed env var. It is particularly useful when declaring
django settings. The default prefix used isPHAC_ASPC_fromphac_aspc.django.settings.utilsimportglobal_from_envglobal_from_env(SESSION_COOKIE_AGE=(int,1200),)The example above creates the module level globalSESSION_COOKIE_AGEtaking the
value of the env var namedPHAC_ASPC_SESSION_COOKIE_AGE, defaulting to 1200 if no
env var is found. As with other configuration env vars for this library, the value
can either come from Django settings or from a.envfile.An alternative prefix can also be provided as well, to use this with other env var
name spaces.fromphac_aspc.django.settings.utilsimportglobal_from_envglobal_from_env(prefix='MY_PREFIX_',SESSION_COOKIE_AGE=(int,1200),)FeaturesWeb Experience Toolkit (WET)The Web Experience Toolkit is bundled with the helpers, and can easily be added
to your templates.Your base template should be modified to:Specify the current language in the lang attribute of the HTML elementInclude the WET CSS files inside the HEAD elementInclude the WET script files at the end of your BODY elementA minimum base template may look like this:{%loadphac_aspc_wet%}{%loadphac_aspc_localization%}<html lang="{%phac_aspc_localization_lang%}"><head>{%phac_aspc_wet_css%}</head><body><h1>Minimum base template</h1>{%blockcontent%}{%endblock%}{%phac_aspc_wet_scripts%}</body></html>or if you're using Jinja:<html lang="{{phac_aspc.phac_aspc_localization_lang()}}"><head>{{phac_aspc.phac_aspc_wet_css()}}</head><body><h1>Minimum base template</h1>{%blockcontent%}{%endblock%}{{phac_aspc.phac_aspc_wet_scripts()}}</body></html>Bundled WET releasesProductVersionWeb Experience Toolkit (WET)v4.0.56.4Canada.ca (GCWeb)v12.5.0Sign in using MicrosoftBy adding a few environment variables, authentication using Microsoft's
identity platform is automatically configured via theAuthliblibrary. Setting thePHAC_ASPC_OAUTH_PROVIDERvariable to "microsoft" enables
OAuth and adds the following new routes:/en-ca/phac_aspc_helper_login (phac_aspc_helper_login)/fr-ca/phac_aspc_helper_login (phac_aspc_helper_login)/en-ca/phac_aspc_helper_authorize (phac_aspc_authorize)/fr-ca/phac_aspc_helper_authorize (phac_aspc_authorize)Thephac_aspc_authorizeURLs above must be added to the list of redirect URLs
in the Azure App Registration.The login flow is triggered by redirecting the browser to the named routephac_aspc_helper_login. The browser will automatically redirect the user to
Microsoft's Sign in page and after successful authentication, return the user to
the redirect route namedphac_aspc_authorizealong with a token containing
information about the user.By default, the authentication backend will look for a user who's username is
the user's uuid from Microsoft - if not found a new user is created.  To
customize this behaviour, a custom authentication backend class can be specified
viaPHAC_ASPC_OAUTH_USE_BACKENDinsettings.py.After successful authentication, the user is redirected to/.  To customize
this behaviour, setPHAC_ASPC_OAUTH_REDIRECT_ON_LOGINinsettings.pyto the
name of the desired route.Redirecting to a specific page is also supported through the?next=<url>query parameter. See "Template Tag" examples below.PHAC_ASPC_OAUTH_USE_BACKEND="custom.authentication.backend"PHAC_ASPC_OAUTH_REDIRECT_ON_LOGIN="home"# pylint: disable=wrong-import-position, unused-wildcard-import, wildcard-importfromphac_aspc.django.settingsimport*NoteIt is important that these settings be declaredbeforethe wildcard import.Here is an example custom backend that sets the user's name to the value
provided by the identity service.fromtypingimportAnyfromdjango.contrib.authimportget_user_modelfromdjango.contrib.auth.backendsimportBaseBackendfromdjango.contrib.auth.base_userimportAbstractBaseUserfromdjango.http.requestimportHttpRequestclassOAuthBackend(BaseBackend):def_get(self,user_info,value,default=""):returnuser_info[value]ifvalueinuser_infoelsedefaultdef_should_update(self,user_info,value,current):v=self._get(user_info,value)returnv!=""andv!=currentdef_sync_user(self,user,user_info,force=False):if(forceorself._should_update(user_info,"email",user.email)orself._should_update(user_info,"name",user.first_name)):user.email=self._get(user_info,"email",user.email)user.first_name=self._get(user_info,"name",user.first_name)user.save()defauthenticate(self,request:HttpRequest,user_info:dict|None=None,**kwargs:Any,)->AbstractBaseUser|None:ifuser_infoisnotNone:user_model=get_user_model()try:user=user_model.objects.get(username=user_info["oid"])self._sync_user(user,user_info)exceptuser_model.DoesNotExist:user=user_model(username=user_info["oid"])self._sync_user(user,user_info,True)returnuserreturnNonedefget_user(self,user_id):user_model=get_user_model()try:returnuser_model.objects.get(pk=user_id)exceptuser_model.DoesNotExist:returnNoneSign-in with Microsoft Environment VariablesVariableTypePurposePHAC_ASPC_OAUTH_PROVIDERstrOnly "microsoft" is supported at the moment.PHAC_ASPC_OAUTH_APP_CLIENT_IDstrClient ID (from the App Registration)PHAC_ASPC_OAUTH_APP_CLIENT_SECRETstrClient Secret (from the App Registration)PHAC_ASPC_OAUTH_MICROSOFT_TENANTstrMicrosoft Tenant IDTemplate TagA "Sign in with Microsoft" button is available as a template tag:{%loadphac_aspc_auth%}{%phac_aspc_auth_signin_microsoft_button%}To redirect to a specific page after logging in, pass thenextquery parameter through to the template tag as an argument.e.g. with Jinja, on a login page where the URL ends with?next=/some-protected-page/:{{ phac_aspc.phac_aspc_auth_signin_microsoft_button(request.GET.urlencode()) }}Handling ErrorsIf there are any errors during the authentication flow, they are displayed to
the user via theerror.htmltemplate.  The template can be extended using standard django templating by
creating atemplates/phac_aspc/helpers/auth/error.htmlfile in the host
project.Strings and localesStrings displayed to the user during the authentication flow are available in
Canada's both official languages.  These strings can be customized by creating
templates in the host project.  Here is a list of strings and templates used by
the authentication flow:TemplateContexterror_title.htmlError page title tag valueerror_page_description.htmlDescription of error page (meta tag)error_type_general.htmlError header displayed for general exceptionserror_type_oauth.htmlError header displayed for authentication errorserror_retry.htmlText of retry linkmicrosoft_logo.htmlAlt text of sign the Microsoft logo in signin buttonsignin_with_microsoft.htmlText displayed in sign in buttonNoteString templates should be placed in thetemplates/phac_aspc/helpers/stringsdirectory.Security ControlsAC-7 Automatic lockout of users after invalid login attemptsdjango-axesis used to monitor and lockout
users who fail to successfully authenticate.The default configuration makes the following configuration changes to django:An attempt is identified by the combination of incoming IP address and
the username,Both successful logins and failures are recorded to the database,The django project is assumed to be behind 1 reverse proxy (SSL),After 3 login failures, the account is locked out for 30 minutes.To require an administrator to unlock the account, or to alter the lockout
duration, you can modify theAXES_COOLOFF_TIMEsetting.# settings.py# Examples of AXES_COOLOFF_TIME settingsAXES_COOLOFF_TIME=None# An administrator must unlock the accountAXES_COOLOFF_TIME=2# Accounts will be locked out for 2 hoursFor more information regarding available configuration options, visit
django-axes'sdocumentationThere are also a few command line management commands available, for example to
remove all of the lockouts you can run:python-mmanageaxes_resetSee theusagedocumentation for more details.AC-11 Session TimeoutsThe default configuration makes the following configuration changes to django:Sessions timeout in 20 minutes,Sessions cookies are marked as secure,Sessions cookies are discarded when the browser is closed,Any requests to the server automatically extends the session.You can override any of these settings by adding them below the settings import
line. For example to use 30 minutes sessions:#settings.pyfromphac_aspc.django.settingsimport*SESSION_COOKIE_AGE=1800Configuration parameters can also be overridden using environment variables.
For example here is a.envfile that achieves the same result as above.# .envPHAC_ASPC_SESSION_COOKIE_AGE=1800For more information on sessions, refer to Django'sdocumentationAdditionally the Session Timeout UI control is available to warn users their
session is about to expire, and provide mechanisms to automatically renew the
session by clicking anywhere on the page, or by clicking on the "extend session"
button when less than 3 minutes remain.To use it, make sure your base template has WET setup as describedabove, and add the following line somewhere in
your body tag.{%phac_aspc_wet_session_timeout_dialog'logout'%}or if you're using Jinja{{phac_aspc.phac_aspc_wet_session_timeout_dialog(dict(request=request),'logout')}}Make sure the above is included on every page where a user can be signed in,
preferably in the base template for the entire site.For more information on session timeout, visit thedocumentation.Session Timeout Environment variablesAll variables are prefixed withPHAC_ASPC_to avoid name conflicts.VariableTypePurposePHAC_ASPC_SESSION_COOKIE_AGEintSession expiry in secondsPHAC_ASPC_SESSION_COOKIE_SECUREboolUse secure cookies (HTTPS only)LocalizationDjango will be configured to support English (en-ca) and French (fr-ca). This
can be changed in your projects settings usingLANGUAGESandLANGUAGE_CODE.For more information on Django's localization, see theirdocumentation.Localization Environment variablesVariableTypePurposePHAC_ASPC_LANGUAGE_CODEstrDefault languagelang template tagIn your templates, retrieve the current language code using thelangtag.{%loadlocalization%}<html lang="{%lang%}">Or in you're using Jinja<html lang="{{phac_aspc.localization.lang()}}">translate decoratorUse this decorator on your models to add translations viadjango-modeltranslation. The example below adds translations for thetitlefield.fromdjango.dbimportmodelsfromphac_aspc.django.localization.decoratorsimporttranslate@translate('title')classPerson(models.Model):name=models.CharField(max_length=255)title=models.CharField(max_length=255)add_admin decoratorUse this decorator on your model to quickly add it to Django's admin interface.fromdjango.dbimportmodelsfromphac_aspc.django.admin.decoratorsimportadd_admin@add_admin()classPerson(models.Model):...You can also specify inline models as well as additionalModelAdminparameters viainlinesandadmin_optionsrespectively.classSignalLocation(models.Model):signal=models.ForeignKey("Signal",on_delete=models.CASCADE)location=models.String()@add_admin(admin_options={'filter_horizontal':('source',)},inlines=[SignalLocation])classSignal(models.Model):title=models.CharField(max_length=400)location=models.ManyToManyField("Location",through='SignalLocation')Jinja2 and Django Template Language (DTL) cross-language-includesWe provide a Jinja2 utility function and a DTL template tag which allows each to
"include" from (render inline, using the including template's context) from one
template language to the other.include_from_dtlJinja2 utilAdd the util to your jinja environment:importphac_aspc.django.helpers.jinja_utilsasinclude_from_dtldefenvironment(**options):env=Environment(**options)env.globals.update({...,# other utils and constants"include_from_dtl":include_from_dtl,})returnenvAnd then use it from a Jinja2 template:{{ include_from_dtl("some/dtl/template/path.html") }}phac_aspc_include_from_jinjaDTL template tagTo register thephac_aspc_...template tags, addphac_aspc.django.helperstoINSTALLED_APPSin yoursettings.py.You can then use the template tag in your DTL templates:{% load phac_aspc_include_from_jinja %}
{% phac_aspc_include_from_jinja "some/Jinja2/template/path.jinja2" %}LoggingDefault logging configurationA ready-to-use default logging configuration is available fromphac_aspc.django.settings.logging,
with an environment variable based API for limited project-specific configuration. To use, just
import*from the module in to yoursettings.pyand setPHAC_ASPC_LOGGING_USE_HELPERS_CONFIG=truein either your.envfile orsettings.py(before thephac_aspcimports).#settings.pyfromphac_aspc.django.settings.loggingimport*# OR, along with all the other settings, via `from phac_aspc.django.settings import *`For alocal devenvironment, I recommend settingPHAC_ASPC_LOGGING_PRETTY_FORMAT_CONSOLE_LOGS=True,
to switch from JSON string formatted logs to friendlier console log formatting (coloured text, indentation, etc).For aGoogle Cloudenvironment, the default configuration of writing JSON strings to stdout is prod-ready.For anAzure Cloudenvironment, I recommend creating an Azure Monitor resource, getting your azure insights connection
string for it, and configuring the app to log to it via thePHAC_ASPC_LOGGING_AZURE_INSIGHTS_CONNECTION_STRINGenv var.
This will enable and use a pre-configured Azure log handler, outputing logs with JSON formatted message fields.In any production environment, you can optionally provide a Slack webhook viaPHAC_ASPC_LOGGING_SLACK_WEBHOOK_URL.
This will send error and critical level logs to the webhook's slack channel. Note: this slack logging handler filters
outdjango.security.DisallowedHostlogs, as they are a constant background noise. Other handlers still capture them.Default Logging Configuration environment variablesThese env vars configure the default logging configuration provided byphac_aspc.django.settings.logging. If you don't both
setPHAC_ASPC_LOGGING_USE_HELPERS_CONFIG=Trueand import those import those settings in your ownsettings.py, then the other
env vars here won't do anything.VariableTypePurposePHAC_ASPC_LOGGING_USE_HELPERS_CONFIGboolset to true to use the PHAC helper provided logging configurationPHAC_ASPC_LOGGING_LOWEST_LEVELstrlowest logging level to printPHAC_ASPC_LOGGING_MUTE_CONSOLE_HANDLERboolmutes the default console handler outputPHAC_ASPC_LOGGING_PRETTY_FORMAT_CONSOLE_LOGSboolpretty format console logs (coloured text)PHAC_ASPC_LOGGING_AZURE_INSIGHTS_CONNECTION_STRINGstrif set, add a Azure log handlerPHAC_ASPC_LOGGING_SLACK_WEBHOOK_URLstrif set, add a Slack Webhook handlerNotethese env vars are consumed only withinphac_aspc.django.settings.logging.
If usingconfigure_uniform_std_lib_and_structlog_loggingdirectly, these env vars
won't do anything.add_fields_to_all_logs_for_current_requestWhen the server is processing a request, this function adds additional key-value fields to the logging context.
Added context is will be present in subsequent logs*. Context is cleared between requests.* at least, it will be in the structlogevent_dictpassed to the formatter (rendering processor). All of the
default PHAC helper formatters will serialize the context in to the final log output. Custom formatters may not.Requires use of the PHAC helper's logging configuration and the django_structlog RequestMiddleware.
The PHAC helper's logging configuration ensures these context vars are rendered when logging,
and the django_structlog RequestMiddleware handles clearing the structlog contextvars between requests.Customized logging configuration via configure_uniform_std_lib_and_structlog_loggingDeeper customization can be achieved by forgoing a*import fromphac_aspc.django.settings.logging,
directly callingphac_aspc.django.helpers.logging.configure_logging.configure_uniform_std_lib_and_structlog_loggingin your settings.py instead. Be aware that this bypasses thePHAC_ASPC_HELPER_LOGGING_...env vars, which
are all only used in thephac_aspc.django.settings.loggingmodule.See theconfigure_uniform_std_lib_and_structlog_loggingdoc string for further details.E.g. Muting the default console handler and using a custom file handler with a custom formatter#settings.pyimportstructlogfromphac_aspc.django.helpers.logging.configure_loggingimport(configure_uniform_std_lib_and_structlog_logging,DEFAULT_STRUCTLOG_PRE_PROCESSORS,)LOGGING_CONFIG=Noneconfigure_uniform_std_lib_and_structlog_logging(mute_console_handler=True,additional_handler_configs={"file":{"class":"logging.handlers.RotatingFileHandler","level":"INFO","filename":"logs","mode":"a","encoding":"utf-8","maxBytes":500000,"backupCount":4"formatter":"custom_formatter",}},additional_formatter_functions={"custom_formatter":(lambda_logging,_name,structlog_event_dict:(f"{structlog_event_dict.timestamp}:{structlog_event_dict.event}"))})ContributingLocal developmentYou can consume the helpers project locally by installing it in editable mode. This is useful for extracting project features into this library. In your requirements.txt, comment out the line with django-phac_aspc-helpers, and add the following line with the file path to this repo, then re-install. Make sure to first uninstall the package if it was already installed, e.g.pip uninstall -y django-phac_aspc-helpers-e file:///absolute_path/to/django-phac_aspc-helpers