From 40493faabb5a61c55cd7eeb99bdb1e8095617620 Mon Sep 17 00:00:00 2001 From: "SilverStrings (Matt)" <37874207+SilverStrings024@users.noreply.github.com> Date: Mon, 12 Jul 2021 10:31:05 -0400 Subject: [PATCH 1/8] Adding tracking support Added tracking support --- analytical/templatetags/matomo.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/analytical/templatetags/matomo.py b/analytical/templatetags/matomo.py index 15334fb..07739d6 100644 --- a/analytical/templatetags/matomo.py +++ b/analytical/templatetags/matomo.py @@ -14,6 +14,8 @@ from analytical.utils import ( get_identity, get_required_setting, is_internal_ip, + build_paq_cmd, + get_event_bind_js ) # domain name (characters separated by a dot), optional port, optional URI path, no slash @@ -48,10 +50,8 @@ DEFAULT_SCOPE = 'page' MatomoVar = namedtuple('MatomoVar', ('index', 'name', 'value', 'scope')) - register = Library() - @register.tag def matomo(parser, token): """ @@ -97,6 +97,19 @@ class MatomoNode(Node): if getattr(settings, 'MATOMO_DISABLE_COOKIES', False): commands.append(DISABLE_COOKIES_CODE) + if getattr(settings, "MATOMO_REQUIRE_CONSENT", False): + grant_class_name = settings.GRANT_CONSENT_TAG_CLASSNAME + revoke_class_name = settings.REVOKE_CONSENT_CLASSNAME + commands.append(build_paq_cmd('requireConsent')) + commands.append(get_event_bind_js( + class_name=grant_class_name, + matomo_event="rememberConsentGiven", + )) + commands.append(get_event_bind_js( + class_name=revoke_class_name, + matomo_event="forgetConsentGiven", + )) + userid = get_identity(context, 'matomo') if userid is not None: variables_code = chain(variables_code, ( From 0bc55d1b10b7fc48d8db24d87442de6f2e0c7910 Mon Sep 17 00:00:00 2001 From: "SilverStrings (Matt)" <37874207+SilverStrings024@users.noreply.github.com> Date: Mon, 12 Jul 2021 10:31:59 -0400 Subject: [PATCH 2/8] Added utility functions Added paq builder function and function to build javascript for binding events --- analytical/utils.py | 52 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/analytical/utils.py b/analytical/utils.py index be70b70..84e7a73 100644 --- a/analytical/utils.py +++ b/analytical/utils.py @@ -163,3 +163,55 @@ 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 + """ + paq = "_paq.push(['%s', " % (cmd) + if len(args) > 0: + for arg_idx in range(len(args)): + if arg_idx == len(args)-1: + paq += "'%s'])" % (args[arg_idx]) + else: + paq += "'%s', " % (args[arg_idx]) + 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 \ No newline at end of file From 3761e7308a11c6a94f3d98a941ba08de02e3d0cf Mon Sep 17 00:00:00 2001 From: "SilverStrings (Matt)" <37874207+SilverStrings024@users.noreply.github.com> Date: Mon, 12 Jul 2021 10:34:30 -0400 Subject: [PATCH 3/8] Add files via upload Added missing new end line --- analytical/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/analytical/utils.py b/analytical/utils.py index 84e7a73..8ab5563 100644 --- a/analytical/utils.py +++ b/analytical/utils.py @@ -214,4 +214,5 @@ def get_event_bind_js( ); }} """ % (class_name, js_event, build_paq_cmd(matomo_event, matomo_args)) - return script \ No newline at end of file + return script + From 85c651f6f60c46f2e2eafb3c4b14ef961a7b2a2c Mon Sep 17 00:00:00 2001 From: "SilverStrings (Matt)" <37874207+SilverStrings024@users.noreply.github.com> Date: Mon, 12 Jul 2021 18:03:02 -0400 Subject: [PATCH 4/8] Update utils.py to improve `build_paq_cmd()` Update `utils.build_paq_cmd()` to convert all elements in the args list to their javascript counter-parts. This will recursively convert the keys and values of a dictionary until there are no more nested dictionaries and will also convert all the elements of a list as well. Also made it so things like booleans, lists, and dictionaries are not surrounded in quotation marks so they're treated like a normal javascript object. If that behavior needs changed. Please let me know --- analytical/utils.py | 42 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/analytical/utils.py b/analytical/utils.py index 8ab5563..d2200ee 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 @@ -174,13 +174,49 @@ def build_paq_cmd(cmd, args=[]): :Returns: A complete '_paq.push([])' command in string form """ + def __to_js_arg(arg): + """ + Turn the argument into a js variable. + True -> true + False -> false + """ + # Recursively handle dictionaries + 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 + # Handle bools + + elif isinstance(arg, bool): + if arg: + arg = "true" + else: + arg = "false" + # Handle lists + 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: for arg_idx in range(len(args)): + current_arg = __to_js_arg(args[arg_idx]) + no_quotes = type(current_arg) in [bool, dict, list] if arg_idx == len(args)-1: - paq += "'%s'])" % (args[arg_idx]) + if no_quotes: + segment = "%s])" % (current_arg) + else: + segment = "'%s'])" % (current_arg) else: - paq += "'%s', " % (args[arg_idx]) + if no_quotes: + segment = "%s, "% (current_arg) + else: + segment = "'%s', " % (current_arg) + paq += segment else: paq += "])" return paq From 3a514444f329fe49258daa8a399968211d9246c8 Mon Sep 17 00:00:00 2001 From: "SilverStrings (Matt)" <37874207+SilverStrings024@users.noreply.github.com> Date: Mon, 12 Jul 2021 18:06:37 -0400 Subject: [PATCH 5/8] Update utils.py Updated to use the tested and better documented version (Is not tested with Matomo or Django. Only with python) --- analytical/utils.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/analytical/utils.py b/analytical/utils.py index d2200ee..62679d8 100644 --- a/analytical/utils.py +++ b/analytical/utils.py @@ -176,9 +176,10 @@ def build_paq_cmd(cmd, args=[]): """ def __to_js_arg(arg): """ - Turn the argument into a js variable. - True -> true - False -> false + :Args: + - arg: The variable (Matomo argument) to be converted to JS. + :Return: + Javascript version of the passed arg parameter """ # Recursively handle dictionaries if isinstance(arg, dict): @@ -187,18 +188,14 @@ def build_paq_cmd(cmd, args=[]): arg.pop(k) arg[__to_js_arg(k)] = __to_js_arg(v) return arg - # Handle bools - elif isinstance(arg, bool): if arg: arg = "true" else: arg = "false" - # Handle lists 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) From d1d1be189ac485034f9f4e3f5eaec04746887d0f Mon Sep 17 00:00:00 2001 From: SilverStrings024 Date: Tue, 13 Jul 2021 00:35:26 -0400 Subject: [PATCH 6/8] Added conditional tracking consent for Matomo Modified utils.build_paq_cmd to remove the trailing comma and white space as well as add a semi colon to the end of the push command. Added context_providers directory with matomo.py file with a context provider that builds the tracking consent code and adds it to the context. Modified matomo.MatomoNode.render. Removed original ugly settings check code with simply rendering the 'consent_script' context variable and adding it to the html variable only if its length is greater than 1. NOTE: The context provider and rendering it (line 110 templatetags/matomo.py) are untested. Both utils.build_paq_cmd and utils.get_event_bind_js are tested but need need more rigorous testing to be sure it won't break. --- analytical/context_providers/matomo.py | 34 ++++++++++++++++++++++++++ analytical/templatetags/matomo.py | 26 ++++++-------------- analytical/utils.py | 23 ++++++++--------- 3 files changed, 54 insertions(+), 29 deletions(-) create mode 100644 analytical/context_providers/matomo.py diff --git a/analytical/context_providers/matomo.py b/analytical/context_providers/matomo.py new file mode 100644 index 0000000..9c90e7b --- /dev/null +++ b/analytical/context_providers/matomo.py @@ -0,0 +1,34 @@ +import utils +from django.conf import settings + +def matomo_consent_provider(request): + """ + Add Mamoto consent script to the requests context. + :Cases: + - If MATOMO_REQURE_CONSENT is True OR If ALWAYS_TRACK_REGISTERED True == continue on + - If ALWAYS_TRACK_REGISTERED is True AND the user is authenticated + """ + # 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': ""} \ No newline at end of file diff --git a/analytical/templatetags/matomo.py b/analytical/templatetags/matomo.py index 07739d6..c88e5bd 100644 --- a/analytical/templatetags/matomo.py +++ b/analytical/templatetags/matomo.py @@ -7,15 +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, - build_paq_cmd, - get_event_bind_js + is_internal_ip ) # domain name (characters separated by a dot), optional port, optional URI path, no slash @@ -97,19 +94,6 @@ class MatomoNode(Node): if getattr(settings, 'MATOMO_DISABLE_COOKIES', False): commands.append(DISABLE_COOKIES_CODE) - if getattr(settings, "MATOMO_REQUIRE_CONSENT", False): - grant_class_name = settings.GRANT_CONSENT_TAG_CLASSNAME - revoke_class_name = settings.REVOKE_CONSENT_CLASSNAME - commands.append(build_paq_cmd('requireConsent')) - commands.append(get_event_bind_js( - class_name=grant_class_name, - matomo_event="rememberConsentGiven", - )) - commands.append(get_event_bind_js( - class_name=revoke_class_name, - matomo_event="forgetConsentGiven", - )) - userid = get_identity(context, 'matomo') if userid is not None: variables_code = chain(variables_code, ( @@ -122,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 62679d8..f7400f5 100644 --- a/analytical/utils.py +++ b/analytical/utils.py @@ -167,8 +167,8 @@ class AnalyticalException(Exception): 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 + - 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: @@ -176,12 +176,12 @@ def build_paq_cmd(cmd, args=[]): """ def __to_js_arg(arg): """ + Turn 'arg' into its javascript counter-part :Args: - - arg: The variable (Matomo argument) to be converted to JS. + - arg: the argument that's to be passed to the array in _paq.push() :Return: - Javascript version of the passed arg parameter + The javascript counter-part to the argument that was passed """ - # Recursively handle dictionaries if isinstance(arg, dict): arg_cpy = deepcopy(arg) for k, v in arg_cpy.items(): @@ -196,18 +196,20 @@ def build_paq_cmd(cmd, args=[]): 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) + 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, dict, list] + no_quotes = type(current_arg) in [bool, int, dict, list] if arg_idx == len(args)-1: if no_quotes: - segment = "%s])" % (current_arg) + segment = "%s]);" % (current_arg) else: - segment = "'%s'])" % (current_arg) + segment = "'%s']);" % (current_arg) else: if no_quotes: segment = "%s, "% (current_arg) @@ -215,7 +217,7 @@ def build_paq_cmd(cmd, args=[]): segment = "'%s', " % (current_arg) paq += segment else: - paq += "])" + paq += "]);" return paq def get_event_bind_js( @@ -248,4 +250,3 @@ def get_event_bind_js( }} """ % (class_name, js_event, build_paq_cmd(matomo_event, matomo_args)) return script - From 2d228686ade0e53e0d1203648399a82f744d6fbd Mon Sep 17 00:00:00 2001 From: SilverStrings024 Date: Tue, 13 Jul 2021 00:42:00 -0400 Subject: [PATCH 7/8] Added blank like at the end of context_providers/matomo.py for Flake8 Renamed context provider to 'consent_provider' to make more sense when adding it to your context_provider --- analytical/context_providers/matomo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/analytical/context_providers/matomo.py b/analytical/context_providers/matomo.py index 9c90e7b..df1049a 100644 --- a/analytical/context_providers/matomo.py +++ b/analytical/context_providers/matomo.py @@ -1,7 +1,7 @@ import utils from django.conf import settings -def matomo_consent_provider(request): +def consent_provider(request): """ Add Mamoto consent script to the requests context. :Cases: @@ -31,4 +31,4 @@ def matomo_consent_provider(request): matomo_event="forgetConsentGiven", ) )} - return {'consent_script': ""} \ No newline at end of file + return {'consent_script': ""} From b4611da6d3162158f5bc7ef0f9b2f04024547308 Mon Sep 17 00:00:00 2001 From: "SilverStrings (Matt)" <37874207+SilverStrings024@users.noreply.github.com> Date: Tue, 13 Jul 2021 00:45:10 -0400 Subject: [PATCH 8/8] Update matomo.py Removed a random note I forgot to take out --- analytical/context_providers/matomo.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/analytical/context_providers/matomo.py b/analytical/context_providers/matomo.py index df1049a..6a5449c 100644 --- a/analytical/context_providers/matomo.py +++ b/analytical/context_providers/matomo.py @@ -4,9 +4,6 @@ from django.conf import settings def consent_provider(request): """ Add Mamoto consent script to the requests context. - :Cases: - - If MATOMO_REQURE_CONSENT is True OR If ALWAYS_TRACK_REGISTERED True == continue on - - If ALWAYS_TRACK_REGISTERED is True AND the user is authenticated """ # Do we require consent? if getattr(settings, 'MATOMO_REQUIRE_CONSENT', False):