diff --git a/analytical/context_providers/matomo.py b/analytical/context_providers/matomo.py new file mode 100644 index 0000000..6a5449c --- /dev/null +++ b/analytical/context_providers/matomo.py @@ -0,0 +1,31 @@ +import utils +from django.conf import settings + +def consent_provider(request): + """ + Add Mamoto consent script to the requests context. + """ + # Do we require consent? + if getattr(settings, 'MATOMO_REQUIRE_CONSENT', False): + provide_script = True + if request.user.is_authenticated and not getattr(settings, "ALWAYS_TRACK_REGISTERED", True): + provide_script = False + if provide_script: + grant_class_name = getattr(settings, 'GRANT_CONSENT_TAG_CLASSNAME') + revoke_class_name = getattr(settings, 'REVOKE_CONSENT_CLASSNAME') + return {"consent_script":""" + %s; + %s + %s + """ % ( + utils.build_paq_cmd('requireConsent'), + utils.get_event_bind_js( + class_name=grant_class_name, + matomo_event="rememberConsentGiven", + ), + utils.get_event_bind_js( + class_name=revoke_class_name, + matomo_event="forgetConsentGiven", + ) + )} + return {'consent_script': ""} diff --git a/analytical/templatetags/matomo.py b/analytical/templatetags/matomo.py index c5b1c1c..ef6f3e6 100644 --- a/analytical/templatetags/matomo.py +++ b/analytical/templatetags/matomo.py @@ -7,13 +7,12 @@ from collections import namedtuple from itertools import chain from django.conf import settings -from django.template import Library, Node, TemplateSyntaxError - +from django.template import Library, Node, TemplateSyntaxError, Template from analytical.utils import ( disable_html, get_identity, get_required_setting, - is_internal_ip, + is_internal_ip ) # domain name (characters separated by a dot), optional port, optional URI path, no slash @@ -48,10 +47,8 @@ DEFAULT_SCOPE = 'page' MatomoVar = namedtuple('MatomoVar', ('index', 'name', 'value', 'scope')) - register = Library() - @register.tag def matomo(parser, token): """ @@ -109,8 +106,14 @@ class MatomoNode(Node): 'variables': '\n '.join(variables_code), 'commands': '\n '.join(commands) } + # Force the consent script to render so we can inject it into the template + consent_script = Template("{{consent_script}}").render(context) + if len(consent_script) > 1: + html += consent_script + if is_internal_ip(context, 'MATOMO'): html = disable_html(html, 'Matomo') + return html diff --git a/analytical/utils.py b/analytical/utils.py index be70b70..f7400f5 100644 --- a/analytical/utils.py +++ b/analytical/utils.py @@ -1,7 +1,7 @@ """ Utility function for django-analytical. """ - +from copy import deepcopy from django.conf import settings from django.core.exceptions import ImproperlyConfigured @@ -163,3 +163,90 @@ class AnalyticalException(Exception): be silenced in templates. """ silent_variable_failure = True + +def build_paq_cmd(cmd, args=[]): + """ + :Args: + - cmd: The command to be pushed to paq (i.e enableHeartbeatTimer or contentInteraction) + - args: Arguments to be added to the paq command. This is mainly + used when building commands to be used on manual event trigger. + + :Returns: + A complete '_paq.push([])' command in string form + """ + def __to_js_arg(arg): + """ + Turn 'arg' into its javascript counter-part + :Args: + - arg: the argument that's to be passed to the array in _paq.push() + :Return: + The javascript counter-part to the argument that was passed + """ + if isinstance(arg, dict): + arg_cpy = deepcopy(arg) + for k, v in arg_cpy.items(): + arg.pop(k) + arg[__to_js_arg(k)] = __to_js_arg(v) + return arg + elif isinstance(arg, bool): + if arg: + arg = "true" + else: + arg = "false" + elif isinstance(arg, list): + for elem_idx in range(len(arg)): + arg[elem_idx] = __to_js_arg(arg[elem_idx]) + + return arg + + paq = "_paq.push(['%s'" % (cmd) + if len(args) > 0: + paq += ", " + for arg_idx in range(len(args)): + current_arg = __to_js_arg(args[arg_idx]) + no_quotes = type(current_arg) in [bool, int, dict, list] + if arg_idx == len(args)-1: + if no_quotes: + segment = "%s]);" % (current_arg) + else: + segment = "'%s']);" % (current_arg) + else: + if no_quotes: + segment = "%s, "% (current_arg) + else: + segment = "'%s', " % (current_arg) + paq += segment + else: + paq += "]);" + return paq + +def get_event_bind_js( + class_name, matomo_event, + matomo_args=[], js_event="onclick", + ): + """ + Build a javascript command to bind an onClick event to some + element whose handler pushes something to _paq + :Args: + - class_name: Value of the 'class' attribute of the tag + the event is to be bound to. + - matomo_event: The matomo event to be pushed to _paq + such as enableHeartbeatTimer or contentInteraction + - matomo_args: The arguments to be passed with the matomo event + meaning + :Return: + A string of javascript that loops the elements found by + document.getElementByClassName and binds the motomo event + to each element that was found + """ + script = f""" + var elems = document.getElementByClassName('%s'); + for (var i=0; i++; i < elems.length){{ + elems[i].addEventListener('%s', + function(){{ + %s; + }} + ); + }} + """ % (class_name, js_event, build_paq_cmd(matomo_event, matomo_args)) + return script