From e50b247a87aa1ccc0a091daef417a97ea004345a Mon Sep 17 00:00:00 2001 From: Julian Haluska Date: Fri, 11 Jan 2019 23:43:48 +0100 Subject: [PATCH 01/15] Asking for consent implementation + documentation --- analytical/templatetags/piwik.py | 8 ++++++++ analytical/tests/test_tag_piwik.py | 5 +++++ docs/services/piwik.rst | 15 ++++++++++++++- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/analytical/templatetags/piwik.py b/analytical/templatetags/piwik.py index 9eebc4a..10819a3 100644 --- a/analytical/templatetags/piwik.py +++ b/analytical/templatetags/piwik.py @@ -42,6 +42,9 @@ TRACKING_CODE = """ VARIABLE_CODE = '_paq.push(["setCustomVariable", %(index)s, "%(name)s", "%(value)s", "%(scope)s"]);' # noqa IDENTITY_CODE = '_paq.push(["setUserId", "%(userid)s"]);' DISABLE_COOKIES_CODE = '_paq.push([\'disableCookies\']);' +ASK_FOR_CONSENT_CODE = '_paq.push([\'requireConsent\']);' +FORGET_CONSENT_CODE = 'document.getElementById("piwik_deny_consent").addEventListener("click", () => { _paq.push(["forgetConsentGiven"]); });' +REMEMBER_CONSENT_CODE = 'document.getElementById("piwik_give_consent").addEventListener("click", () => { _paq.push(["setConsentGiven"]); _paq.push(["rememberConsentGiven"]); });' DEFAULT_SCOPE = 'page' @@ -96,6 +99,11 @@ class PiwikNode(Node): if getattr(settings, 'PIWIK_DISABLE_COOKIES', False): commands.append(DISABLE_COOKIES_CODE) + if getattr(settings, 'PIWIK_ASK_FOR_CONSENT', False): + commands.append(ASK_FOR_CONSENT_CODE) + commands.append(FORGET_CONSENT_CODE) + commands.append(REMEMBER_CONSENT_CODE) + userid = get_identity(context, 'piwik') if userid is not None: variables_code = chain(variables_code, ( diff --git a/analytical/tests/test_tag_piwik.py b/analytical/tests/test_tag_piwik.py index 32661ee..4becce1 100644 --- a/analytical/tests/test_tag_piwik.py +++ b/analytical/tests/test_tag_piwik.py @@ -150,3 +150,8 @@ class PiwikTagTestCase(TagTestCase): def test_disable_cookies(self): r = PiwikNode().render(Context({})) self.assertTrue("_paq.push(['disableCookies']);" in r, r) + + @override_settings(PIWIK_ASK_FOR_CONSENT=True) + def test_disable_cookies(self): + r = PiwikNode().render(Context({})) + self.assertTrue("_paq.push([\'requireConsent\']);" in r, r) diff --git a/docs/services/piwik.rst b/docs/services/piwik.rst index e4c9aee..d012aca 100644 --- a/docs/services/piwik.rst +++ b/docs/services/piwik.rst @@ -145,11 +145,24 @@ set the context variable ``analytical_identity`` (for global configuration) or Disabling cookies ----------------- -If you want to `disable cookies`_, set :data:`PIWIKI_DISABLE_COOKIES` to +If you want to `disable cookies`_, set :data:`PIWIK_DISABLE_COOKIES` to :const:`True`. This is disabled by default. .. _`disable cookies`: https://matomo.org/faq/general/faq_157/ +Ask for consent +----------------- + +If you want to ask for consent set :data:`PIWIK_ASK_FOR_CONSENT` to +:const:`True`. This is disabled by default. + +To ask the visitor for consent just create DOM elements with the following id's: + +`piwik_deny_consent` - id for DOM element to click, if the user denies consent +`piwik_give_consent` - id for DOM element to click, if the user gives consent + +.. _`asking for consent`: https://developer.matomo.org/guides/tracking-javascript-guide#asking-for-consent + Internal IP addresses --------------------- From 5c5250692a55f44b1ffc0f9daa84e4b3e6b27d98 Mon Sep 17 00:00:00 2001 From: Julian Haluska Date: Fri, 11 Jan 2019 23:58:01 +0100 Subject: [PATCH 02/15] Fixed ask for consent test name --- analytical/tests/test_tag_piwik.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/analytical/tests/test_tag_piwik.py b/analytical/tests/test_tag_piwik.py index 4becce1..b760374 100644 --- a/analytical/tests/test_tag_piwik.py +++ b/analytical/tests/test_tag_piwik.py @@ -152,6 +152,6 @@ class PiwikTagTestCase(TagTestCase): self.assertTrue("_paq.push(['disableCookies']);" in r, r) @override_settings(PIWIK_ASK_FOR_CONSENT=True) - def test_disable_cookies(self): + def test_ask_for_consent(self): r = PiwikNode().render(Context({})) self.assertTrue("_paq.push([\'requireConsent\']);" in r, r) From 7e3f9d1de8431f50d2e38d722745b5acb636dfba Mon Sep 17 00:00:00 2001 From: Julian Haluska Date: Sat, 12 Jan 2019 11:41:07 +0100 Subject: [PATCH 03/15] Removed backslashes from strings, improved documentation text --- analytical/templatetags/piwik.py | 4 ++-- analytical/tests/test_tag_piwik.py | 2 +- docs/services/piwik.rst | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/analytical/templatetags/piwik.py b/analytical/templatetags/piwik.py index 10819a3..d863eef 100644 --- a/analytical/templatetags/piwik.py +++ b/analytical/templatetags/piwik.py @@ -41,8 +41,8 @@ TRACKING_CODE = """ VARIABLE_CODE = '_paq.push(["setCustomVariable", %(index)s, "%(name)s", "%(value)s", "%(scope)s"]);' # noqa IDENTITY_CODE = '_paq.push(["setUserId", "%(userid)s"]);' -DISABLE_COOKIES_CODE = '_paq.push([\'disableCookies\']);' -ASK_FOR_CONSENT_CODE = '_paq.push([\'requireConsent\']);' +DISABLE_COOKIES_CODE = "_paq.push(['disableCookies']);" +ASK_FOR_CONSENT_CODE = "_paq.push(['requireConsent']);" FORGET_CONSENT_CODE = 'document.getElementById("piwik_deny_consent").addEventListener("click", () => { _paq.push(["forgetConsentGiven"]); });' REMEMBER_CONSENT_CODE = 'document.getElementById("piwik_give_consent").addEventListener("click", () => { _paq.push(["setConsentGiven"]); _paq.push(["rememberConsentGiven"]); });' diff --git a/analytical/tests/test_tag_piwik.py b/analytical/tests/test_tag_piwik.py index b760374..0149eb4 100644 --- a/analytical/tests/test_tag_piwik.py +++ b/analytical/tests/test_tag_piwik.py @@ -154,4 +154,4 @@ class PiwikTagTestCase(TagTestCase): @override_settings(PIWIK_ASK_FOR_CONSENT=True) def test_ask_for_consent(self): r = PiwikNode().render(Context({})) - self.assertTrue("_paq.push([\'requireConsent\']);" in r, r) + self.assertTrue("_paq.push(['requireConsent']);" in r, r) diff --git a/docs/services/piwik.rst b/docs/services/piwik.rst index d012aca..869e1f6 100644 --- a/docs/services/piwik.rst +++ b/docs/services/piwik.rst @@ -146,7 +146,7 @@ Disabling cookies ----------------- If you want to `disable cookies`_, set :data:`PIWIK_DISABLE_COOKIES` to -:const:`True`. This is disabled by default. +:const:`True`. By default, cookies are enabled (i.e. :const:False). .. _`disable cookies`: https://matomo.org/faq/general/faq_157/ @@ -154,12 +154,12 @@ Ask for consent ----------------- If you want to ask for consent set :data:`PIWIK_ASK_FOR_CONSENT` to -:const:`True`. This is disabled by default. +:const:`True`. By default, no consent by the visitor is needed (i.e. :const:False). -To ask the visitor for consent just create DOM elements with the following id's: +To ask the visitor for consent in your page, create DOM elements with the following id's: -`piwik_deny_consent` - id for DOM element to click, if the user denies consent -`piwik_give_consent` - id for DOM element to click, if the user gives consent +`piwik_deny_consent` - id for element to click when the user denies consent +`piwik_give_consent` - id for element to click when the user gives consent .. _`asking for consent`: https://developer.matomo.org/guides/tracking-javascript-guide#asking-for-consent From 263afc38e99b5e04add6ff5ac91239ddf77cff7a Mon Sep 17 00:00:00 2001 From: Julian Haluska Date: Sat, 12 Jan 2019 12:16:27 +0100 Subject: [PATCH 04/15] Fixed line length (now is < 100) --- analytical/templatetags/piwik.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/analytical/templatetags/piwik.py b/analytical/templatetags/piwik.py index d863eef..bd1410a 100644 --- a/analytical/templatetags/piwik.py +++ b/analytical/templatetags/piwik.py @@ -43,8 +43,17 @@ VARIABLE_CODE = '_paq.push(["setCustomVariable", %(index)s, "%(name)s", "%(value IDENTITY_CODE = '_paq.push(["setUserId", "%(userid)s"]);' DISABLE_COOKIES_CODE = "_paq.push(['disableCookies']);" ASK_FOR_CONSENT_CODE = "_paq.push(['requireConsent']);" -FORGET_CONSENT_CODE = 'document.getElementById("piwik_deny_consent").addEventListener("click", () => { _paq.push(["forgetConsentGiven"]); });' -REMEMBER_CONSENT_CODE = 'document.getElementById("piwik_give_consent").addEventListener("click", () => { _paq.push(["setConsentGiven"]); _paq.push(["rememberConsentGiven"]); });' +FORGET_CONSENT_CODE = """ +document.getElementById("piwik_deny_consent").addEventListener("click", +() => { + paq.push(["forgetConsentGiven"]); +}); +""" +REMEMBER_CONSENT_CODE = """ document.getElementById("piwik_give_consent").addEventListener("click", +() => { + _paq.push(["setConsentGiven"]); _paq.push(["rememberConsentGiven"]); +}); +""" DEFAULT_SCOPE = 'page' From c3065fc0c96e762f064176f16554af440055cadc Mon Sep 17 00:00:00 2001 From: Julian Haluska Date: Tue, 15 Jan 2019 21:40:05 +0100 Subject: [PATCH 05/15] Changed usage of id to class name. Code adjustments (compatibility, naming) --- analytical/templatetags/piwik.py | 38 ++++++++++++++++++++------------ docs/services/piwik.rst | 19 +++++++++++----- 2 files changed, 37 insertions(+), 20 deletions(-) diff --git a/analytical/templatetags/piwik.py b/analytical/templatetags/piwik.py index bd1410a..77269cc 100644 --- a/analytical/templatetags/piwik.py +++ b/analytical/templatetags/piwik.py @@ -42,18 +42,30 @@ TRACKING_CODE = """ VARIABLE_CODE = '_paq.push(["setCustomVariable", %(index)s, "%(name)s", "%(value)s", "%(scope)s"]);' # noqa IDENTITY_CODE = '_paq.push(["setUserId", "%(userid)s"]);' DISABLE_COOKIES_CODE = "_paq.push(['disableCookies']);" -ASK_FOR_CONSENT_CODE = "_paq.push(['requireConsent']);" -FORGET_CONSENT_CODE = """ -document.getElementById("piwik_deny_consent").addEventListener("click", -() => { - paq.push(["forgetConsentGiven"]); -}); -""" -REMEMBER_CONSENT_CODE = """ document.getElementById("piwik_give_consent").addEventListener("click", -() => { - _paq.push(["setConsentGiven"]); _paq.push(["rememberConsentGiven"]); -}); -""" + +GIVE_CONSENT_CLASS = "piwik_give_consent" +REMOVE_CONSENT_CLASS = "piwik_remove_consent" +ASK_FOR_CONSENT_CODE = """ +_paq.push(['requireConsent']); + +var elements = document.getElementsByClassName("{}"); +for (var i = 0; i < elements.length; i++) {{ + elements[i].addEventListener("click", + function () {{ + _paq.push(["forgetConsentGiven"]); + }} + ); +}} + +var elements = document.getElementsByClassName("{}"); +for (var i = 0; i < elements.length; i++) {{ + elements[i].addEventListener("click", + function () {{ + _paq.push(["rememberConsentGiven"]); + }} + ); +}} +""".format(REMOVE_CONSENT_CLASS, GIVE_CONSENT_CLASS) DEFAULT_SCOPE = 'page' @@ -110,8 +122,6 @@ class PiwikNode(Node): if getattr(settings, 'PIWIK_ASK_FOR_CONSENT', False): commands.append(ASK_FOR_CONSENT_CODE) - commands.append(FORGET_CONSENT_CODE) - commands.append(REMEMBER_CONSENT_CODE) userid = get_identity(context, 'piwik') if userid is not None: diff --git a/docs/services/piwik.rst b/docs/services/piwik.rst index 869e1f6..ad2e06a 100644 --- a/docs/services/piwik.rst +++ b/docs/services/piwik.rst @@ -146,20 +146,27 @@ Disabling cookies ----------------- If you want to `disable cookies`_, set :data:`PIWIK_DISABLE_COOKIES` to -:const:`True`. By default, cookies are enabled (i.e. :const:False). +:const:`True`. By default, cookies are enabled (i.e. :const:`False`). .. _`disable cookies`: https://matomo.org/faq/general/faq_157/ Ask for consent ----------------- -If you want to ask for consent set :data:`PIWIK_ASK_FOR_CONSENT` to -:const:`True`. By default, no consent by the visitor is needed (i.e. :const:False). +If you do not want to track visitors without permission, you can `ask for consent`_ first. +To enable this, set :data:`PIWIK_ASK_FOR_CONSENT` to :const:`True`. By default, no consent for tracking is needed (i.e. :const:`False`). -To ask the visitor for consent in your page, create DOM elements with the following id's: +To give and remove consent in your page, create DOM elements with the following classes: -`piwik_deny_consent` - id for element to click when the user denies consent -`piwik_give_consent` - id for element to click when the user gives consent +`piwik_give_consent` - class name for element to click when visitors want to **give** consent +`piwik_remove_consent` - class name for element to click when visitors want to **remove** consent + +Examples: + # button to allow tracking + + + # button to remove tracking consent + .. _`asking for consent`: https://developer.matomo.org/guides/tracking-javascript-guide#asking-for-consent From 845089d4e647e669562a3d572a062a3158537440 Mon Sep 17 00:00:00 2001 From: Julian Haluska Date: Sun, 9 Jun 2019 21:46:39 +0200 Subject: [PATCH 06/15] Added 'ask for consent' logic and documentation to new matomo files --- analytical/templatetags/matomo.py | 29 ++++++++++++++++++++++++++++- analytical/tests/test_tag_matomo.py | 5 +++++ docs/services/matomo.rst | 20 ++++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/analytical/templatetags/matomo.py b/analytical/templatetags/matomo.py index 98bdf8a..00dd88d 100644 --- a/analytical/templatetags/matomo.py +++ b/analytical/templatetags/matomo.py @@ -41,7 +41,31 @@ TRACKING_CODE = """ VARIABLE_CODE = '_paq.push(["setCustomVariable", %(index)s, "%(name)s", "%(value)s", "%(scope)s"]);' # noqa IDENTITY_CODE = '_paq.push(["setUserId", "%(userid)s"]);' -DISABLE_COOKIES_CODE = '_paq.push([\'disableCookies\']);' +DISABLE_COOKIES_CODE = "_paq.push(['disableCookies']);" + +GIVE_CONSENT_CLASS = "piwik_give_consent" +REMOVE_CONSENT_CLASS = "piwik_remove_consent" +ASK_FOR_CONSENT_CODE = """ +_paq.push(['requireConsent']); + +var elements = document.getElementsByClassName("{}"); +for (var i = 0; i < elements.length; i++) {{ + elements[i].addEventListener("click", + function () {{ + _paq.push(["forgetConsentGiven"]); + }} + ); +}} + +var elements = document.getElementsByClassName("{}"); +for (var i = 0; i < elements.length; i++) {{ + elements[i].addEventListener("click", + function () {{ + _paq.push(["rememberConsentGiven"]); + }} + ); +}} +""".format(REMOVE_CONSENT_CLASS, GIVE_CONSENT_CLASS) DEFAULT_SCOPE = 'page' @@ -96,6 +120,9 @@ class MatomoNode(Node): if getattr(settings, 'MATOMO_DISABLE_COOKIES', False): commands.append(DISABLE_COOKIES_CODE) + if getattr(settings, 'MATOMO_ASK_FOR_CONSENT', False): + commands.append(ASK_FOR_CONSENT_CODE) + userid = get_identity(context, 'matomo') if userid is not None: variables_code = chain(variables_code, ( diff --git a/analytical/tests/test_tag_matomo.py b/analytical/tests/test_tag_matomo.py index d3d6785..01e6cb2 100644 --- a/analytical/tests/test_tag_matomo.py +++ b/analytical/tests/test_tag_matomo.py @@ -150,3 +150,8 @@ class MatomoTagTestCase(TagTestCase): def test_disable_cookies(self): r = MatomoNode().render(Context({})) self.assertTrue("_paq.push(['disableCookies']);" in r, r) + + @override_settings(PIWIK_ASK_FOR_CONSENT=True) + def test_ask_for_consent(self): + r = PiwikNode().render(Context({})) + self.assertTrue("_paq.push(['requireConsent']);" in r, r) \ No newline at end of file diff --git a/docs/services/matomo.rst b/docs/services/matomo.rst index 0aa4731..8527795 100644 --- a/docs/services/matomo.rst +++ b/docs/services/matomo.rst @@ -149,6 +149,26 @@ If you want to `disable cookies`_, set :data:`MATOMO_DISABLE_COOKIES` to .. _`disable cookies`: https://matomo.org/faq/general/faq_157/ +Ask for consent +----------------- + +If you do not want to track visitors without permission, you can `ask for consent`_ first. +To enable this, set :data:`MATOMO_ASK_FOR_CONSENT` to :const:`True`. By default, no consent for tracking is needed (i.e. :const:`False`). + +To give and remove consent in your page, create DOM elements with the following classes: + +`piwik_give_consent` - class name for element to click when visitors want to **give** consent +`piwik_remove_consent` - class name for element to click when visitors want to **remove** consent + +Examples: + # button to allow tracking + + + # button to remove tracking consent + + +.. _`asking for consent`: https://developer.matomo.org/guides/tracking-javascript-guide#asking-for-consent + Internal IP addresses --------------------- From 3a652cd6bce737e6b15ecee84b44bb9f42c688a7 Mon Sep 17 00:00:00 2001 From: Julian Haluska Date: Sun, 9 Jun 2019 21:50:17 +0200 Subject: [PATCH 07/15] Adjusted remaining 'piwik' naming to 'matomo' --- analytical/templatetags/matomo.py | 4 ++-- analytical/tests/test_tag_matomo.py | 4 ++-- docs/services/matomo.rst | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/analytical/templatetags/matomo.py b/analytical/templatetags/matomo.py index 00dd88d..977cb91 100644 --- a/analytical/templatetags/matomo.py +++ b/analytical/templatetags/matomo.py @@ -43,8 +43,8 @@ VARIABLE_CODE = '_paq.push(["setCustomVariable", %(index)s, "%(name)s", "%(value IDENTITY_CODE = '_paq.push(["setUserId", "%(userid)s"]);' DISABLE_COOKIES_CODE = "_paq.push(['disableCookies']);" -GIVE_CONSENT_CLASS = "piwik_give_consent" -REMOVE_CONSENT_CLASS = "piwik_remove_consent" +GIVE_CONSENT_CLASS = "matomo_give_consent" +REMOVE_CONSENT_CLASS = "matomo_remove_consent" ASK_FOR_CONSENT_CODE = """ _paq.push(['requireConsent']); diff --git a/analytical/tests/test_tag_matomo.py b/analytical/tests/test_tag_matomo.py index 01e6cb2..3ebb47c 100644 --- a/analytical/tests/test_tag_matomo.py +++ b/analytical/tests/test_tag_matomo.py @@ -151,7 +151,7 @@ class MatomoTagTestCase(TagTestCase): r = MatomoNode().render(Context({})) self.assertTrue("_paq.push(['disableCookies']);" in r, r) - @override_settings(PIWIK_ASK_FOR_CONSENT=True) + @override_settings(MATOMO_ASK_FOR_CONSENT=True) def test_ask_for_consent(self): - r = PiwikNode().render(Context({})) + r = MatomoNode().render(Context({})) self.assertTrue("_paq.push(['requireConsent']);" in r, r) \ No newline at end of file diff --git a/docs/services/matomo.rst b/docs/services/matomo.rst index 8527795..77b742a 100644 --- a/docs/services/matomo.rst +++ b/docs/services/matomo.rst @@ -157,15 +157,15 @@ To enable this, set :data:`MATOMO_ASK_FOR_CONSENT` to :const:`True`. By default, To give and remove consent in your page, create DOM elements with the following classes: -`piwik_give_consent` - class name for element to click when visitors want to **give** consent -`piwik_remove_consent` - class name for element to click when visitors want to **remove** consent +`matomo_give_consent` - class name for element to click when visitors want to **give** consent +`matomo_remove_consent` - class name for element to click when visitors want to **remove** consent Examples: # button to allow tracking - + # button to remove tracking consent - + .. _`asking for consent`: https://developer.matomo.org/guides/tracking-javascript-guide#asking-for-consent From a61b949267c202d8718e3ac2abed57a0aab38546 Mon Sep 17 00:00:00 2001 From: ronardcaktus Date: Thu, 19 Feb 2026 07:34:38 -0500 Subject: [PATCH 08/15] Add envrc to gitignore --- .gitignore | 10 ++++ pyproject.toml | 126 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 pyproject.toml diff --git a/.gitignore b/.gitignore index 6e8fe88..aaebe01 100644 --- a/.gitignore +++ b/.gitignore @@ -2,12 +2,19 @@ /*.geany /.idea /.tox +/.vscode +/.envrc + +*.pyc +*.pyo + /.coverage /build /dist /docs/_build /MANIFEST +/playground /docs/_templates/layout.html @@ -15,3 +22,6 @@ *.pyo *.egg-info + +/requirements.txt +/uv.lock diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..69bd006 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,126 @@ +[build-system] +build-backend = "setuptools.build_meta" +requires = ["setuptools>=80"] + +[project] +name = "django-analytical" +dynamic = ["version"] +description = "Analytics service integration for Django projects" +readme = "README.rst" +license = "MIT" +license-files = ["LICENSE.txt"] +authors = [ + {name = "Joost Cassee", email = "joost@cassee.net"}, + {name = "Joshua Krall", email = "joshuakrall@pobox.com"}, + {name = "Aleck Landgraf", email = "aleck.landgraf@buildingenergy.com"}, + {name = "Alexandre Pocquet", email = "apocquet@lecko.fr"}, + {name = "Bateau Knowledge", email = "info@bateauknowledge.nl"}, + {name = "Bogdan Bodnar", email = "bogdanbodnar@mail.com"}, + {name = "Brad Pitcher", email = "bradpitcher@gmail.com"}, + {name = "Corentin Mercier", email = "corentin@mercier.link"}, + {name = "Craig Bruce", email = "craig@eyesopen.com"}, + {name = "Daniel Vitiello", email = "ezdismissal@gmail.com"}, + {name = "David Smith", email = "smithdc@gmail.com"}, + {name = "Diederik van der Boor", email = "vdboor@edoburu.nl"}, + {name = "Eric Amador", email = "eric.amador14@gmail.com"}, + {name = "Eric Davis", email = "eric@davislv.com"}, + {name = "Eric Wang", email = "gnawrice@gmail.com"}, + {name = "Erick Massip", email = "ericmassip1@gmail.com"}, + {name = "Garrett Coakley", email = "garrettc@users.noreply.github.com"}, + {name = "Garrett Robinson", email = "garrett.f.robinson@gmail.com"}, + {name = "GreenKahuna", email = "info@greenkahuna.com"}, + {name = "Hugo Osvaldo Barrera", email = "hugo@barrera.io"}, + {name = "Ian Ramsay", email = "ianalexr@yahoo.com"}, + {name = "Iván Raskovsky", email = "raskovsky+git@gmail.com"}, + {name = "James Paden", email = "james@xemion.com"}, + {name = "Jannis Leidel", email = "jannis@leidel.info"}, + {name = "Julien Grenier", email = "julien.grenier42@gmail.com"}, + {name = "Kevin Olbrich", email = "ko@sv01.de"}, + {name = "Marc Bourqui", email = "m.bourqui@edsi-tech.com"}, + {name = "Martey Dodoo", email = "martey@mobolic.com"}, + {name = "Martín Gaitán", email = "gaitan@gmail.com"}, + {name = "Matthäus G. Chajdas", email = "dev@anteru.net"}, + {name = "Max Arnold", email = "arnold.maxim@gmail.com"}, + {name = "Nikolay Korotkiy", email = "sikmir@gmail.com"}, + {name = "Paul Oswald", email = "pauloswald@gmail.com"}, + {name = "Peter Bittner", email = "django@bittner.it"}, + {name = "Petr Dlouhý", email = "petr.dlouhy@email.cz"}, + {name = "Philippe O. Wagner", email = "admin@arteria.ch"}, + {name = "Pi Delport", email = "pjdelport@gmail.com"}, + {name = "Sandra Mau", email = "sandra.mau@gmail.com"}, + {name = "Scott Adams", email = "scottadams80@gmail.com"}, + {name = "Scott Karlin", email = "gitlab@karlin-online.com"}, + {name = "Sean Wallace", email = "sean@lowpro.ca"}, + {name = "Sid Mitra", email = "sidmitra.del@gmail.com"}, + {name = "Simon Ye", email = "sye737@gmail.com"}, + {name = "Steve Schwarz", email = "steve@agilitynerd.com"}, + {name = "Steven Skoczen", email = "steven.skoczen@wk.com"}, + {name = "Tim Gates", email = "tim.gates@iress.com"}, + {name = "Tinnet Coronam", email = "tinnet@coronam.net"}, + {name = "Uros Trebec", email = "uros@trebec.org"}, + {name = "Walter Renner", email = "walter.renner@me.com"}, +] +maintainers = [ + {name = "Jazzband community", email = "jazzband-bot@users.noreply.github.com"}, + {name = "Peter Bittner", email = "django@bittner.it"}, +] +keywords=[ + "django", + "analytics", +] +classifiers=[ + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Framework :: Django", + "Framework :: Django :: 4.2", + "Framework :: Django :: 5.1", + "Framework :: Django :: 5.2", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: Dynamic Content", + "Topic :: Software Development :: Libraries :: Python Modules", +] +requires-python = ">=3.9" +dependencies = [ + "django>=4.2", +] + +[project.urls] +Homepage = "https://github.com/jazzband/django-analytical" +Documentation = "https://django-analytical.readthedocs.io/" + +[tool.coverage.report] +show_missing = true +skip_covered = true + +[tool.coverage.run] +source = ["analytical"] + +[tool.pytest.ini_options] +addopts = "--junitxml=tests/unittests-report.xml --color=yes --verbose" +DJANGO_SETTINGS_MODULE = "tests.testproject.settings" +norecursedirs = ["playground"] + +[tool.ruff.format] +quote-style = "single" + +[tool.ruff.lint.flake8-quotes] +inline-quotes = "single" + +[tool.setuptools] +packages = [ + "analytical", + "analytical.templatetags", +] + +[tool.setuptools.dynamic] +version = {attr = "analytical.__version__"} From 11564d8cfc623e14d798d4b220eab27b6a2f740f Mon Sep 17 00:00:00 2001 From: ronardcaktus Date: Thu, 19 Feb 2026 08:06:48 -0500 Subject: [PATCH 09/15] Add consent class and test --- analytical/templatetags/matomo.py | 4 ++-- tests/unit/test_tag_matomo.py | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/analytical/templatetags/matomo.py b/analytical/templatetags/matomo.py index 9107516..4ff6c2e 100644 --- a/analytical/templatetags/matomo.py +++ b/analytical/templatetags/matomo.py @@ -46,8 +46,8 @@ VARIABLE_CODE = ( IDENTITY_CODE = '_paq.push(["setUserId", "%(userid)s"]);' DISABLE_COOKIES_CODE = "_paq.push(['disableCookies']);" -GIVE_CONSENT_CLASS = "matomo_give_consent" -REMOVE_CONSENT_CLASS = "matomo_remove_consent" +GIVE_CONSENT_CLASS = 'matomo_give_consent' +REMOVE_CONSENT_CLASS = 'matomo_remove_consent' ASK_FOR_CONSENT_CODE = """ _paq.push(['requireConsent']); diff --git a/tests/unit/test_tag_matomo.py b/tests/unit/test_tag_matomo.py index 0765c12..c6ebd93 100644 --- a/tests/unit/test_tag_matomo.py +++ b/tests/unit/test_tag_matomo.py @@ -159,3 +159,8 @@ class MatomoTagTestCase(TagTestCase): def test_disable_cookies(self): r = MatomoNode().render(Context({})) assert "_paq.push(['disableCookies']);" in r + + @override_settings(MATOMO_ASK_FOR_CONSENT=True) + def test_ask_for_consent(self): + r = MatomoNode().render(Context({})) + self.assertTrue("_paq.push(['requireConsent']);" in r, r) From c9434208b13d9ee3e93842c17cbacbf971f03ad5 Mon Sep 17 00:00:00 2001 From: ronardcaktus Date: Thu, 19 Feb 2026 08:09:49 -0500 Subject: [PATCH 10/15] Undo comment --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7bfb247..69bd006 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -108,7 +108,7 @@ source = ["analytical"] [tool.pytest.ini_options] addopts = "--junitxml=tests/unittests-report.xml --color=yes --verbose" DJANGO_SETTINGS_MODULE = "tests.testproject.settings" -# norecursedirs = ["playground"] +norecursedirs = ["playground"] [tool.ruff.format] quote-style = "single" From 69dd23c7977d5fcc81e0e1453befef185413184a Mon Sep 17 00:00:00 2001 From: ronardcaktus Date: Sat, 21 Feb 2026 06:35:03 -0500 Subject: [PATCH 11/15] Remove personal references and fix doc styling --- .gitignore | 2 -- docs/services/matomo.rst | 8 +++++--- pyproject.toml | 1 - 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 9c8a4a7..5275f3c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,6 @@ /.idea /.tox /.vscode -/.envrc -/playground *.pyc *.pyo diff --git a/docs/services/matomo.rst b/docs/services/matomo.rst index 4538576..a59394f 100644 --- a/docs/services/matomo.rst +++ b/docs/services/matomo.rst @@ -150,17 +150,19 @@ If you want to `disable cookies`_, set :data:`MATOMO_DISABLE_COOKIES` to .. _`disable cookies`: https://matomo.org/faq/general/faq_157/ Ask for consent ------------------ +--------------- If you do not want to track visitors without permission, you can `ask for consent`_ first. -To enable this, set :data:`MATOMO_ASK_FOR_CONSENT` to :const:`True`. By default, no consent for tracking is needed (i.e. :const:`False`). +To enable this, set :data:`MATOMO_ASK_FOR_CONSENT` to :const:`True`. +By default, no consent for tracking is needed (i.e. :const:`False`). To give and remove consent in your page, create DOM elements with the following classes: `matomo_give_consent` - class name for element to click when visitors want to **give** consent `matomo_remove_consent` - class name for element to click when visitors want to **remove** consent -Examples: +Examples:: + # button to allow tracking diff --git a/pyproject.toml b/pyproject.toml index 69bd006..b51a2f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -108,7 +108,6 @@ source = ["analytical"] [tool.pytest.ini_options] addopts = "--junitxml=tests/unittests-report.xml --color=yes --verbose" DJANGO_SETTINGS_MODULE = "tests.testproject.settings" -norecursedirs = ["playground"] [tool.ruff.format] quote-style = "single" From 5c6566f7dbb463e9a9ba908c10d06be144c9ed08 Mon Sep 17 00:00:00 2001 From: ronardcaktus Date: Tue, 24 Feb 2026 20:14:56 -0500 Subject: [PATCH 12/15] Ask contribuitors to pyproject.toml & update changelog --- CHANGELOG.rst | 1 + pyproject.toml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4f3cc3f..9100a8d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,6 +3,7 @@ * Fix GA gtag user_id setup and add support for custom dimensions (Erick Massip) * Change spelling of "JavaScript" across all files in docstrings and docs (Peter Bittner) +* Ask site visitors for consent when using Matomo (Julian Haluska & Ronard Luna) Version 3.2.0 ------------- diff --git a/pyproject.toml b/pyproject.toml index b51a2f8..bee00a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,6 +59,8 @@ authors = [ {name = "Tinnet Coronam", email = "tinnet@coronam.net"}, {name = "Uros Trebec", email = "uros@trebec.org"}, {name = "Walter Renner", email = "walter.renner@me.com"}, + {name = "Julian Haluska", email = "mail@julianhaluska.de"}, + {name = "Ronard Luna", email = "rlunag@proton.me"}, ] maintainers = [ {name = "Jazzband community", email = "jazzband-bot@users.noreply.github.com"}, From f4ef953f6ea2ee56e4daefa12e74d6813ef1a255 Mon Sep 17 00:00:00 2001 From: Julian Haluska Date: Fri, 11 Jan 2019 23:43:48 +0100 Subject: [PATCH 13/15] Asking for consent implementation + documentation --- analytical/templatetags/piwik.py | 126 ++++++++++++++++++++ analytical/tests/test_tag_piwik.py | 157 ++++++++++++++++++++++++ docs/services/piwik.rst | 184 +++++++++++++++++++++++++++++ 3 files changed, 467 insertions(+) create mode 100644 analytical/templatetags/piwik.py create mode 100644 analytical/tests/test_tag_piwik.py create mode 100644 docs/services/piwik.rst diff --git a/analytical/templatetags/piwik.py b/analytical/templatetags/piwik.py new file mode 100644 index 0000000..10819a3 --- /dev/null +++ b/analytical/templatetags/piwik.py @@ -0,0 +1,126 @@ +""" +Piwik template tags and filters. +""" + +from __future__ import absolute_import + +from collections import namedtuple +from itertools import chain +import re + +from django.conf import settings +from django.template import Library, Node, TemplateSyntaxError + +from analytical.utils import (is_internal_ip, disable_html, + get_required_setting, get_identity) + + +# domain name (characters separated by a dot), optional port, optional URI path, no slash +DOMAINPATH_RE = re.compile(r'^(([^./?#@:]+\.)*[^./?#@:]+)+(:[0-9]+)?(/[^/?#@:]+)*$') + +# numeric ID +SITEID_RE = re.compile(r'^\d+$') + +TRACKING_CODE = """ + + +""" # noqa + +VARIABLE_CODE = '_paq.push(["setCustomVariable", %(index)s, "%(name)s", "%(value)s", "%(scope)s"]);' # noqa +IDENTITY_CODE = '_paq.push(["setUserId", "%(userid)s"]);' +DISABLE_COOKIES_CODE = '_paq.push([\'disableCookies\']);' +ASK_FOR_CONSENT_CODE = '_paq.push([\'requireConsent\']);' +FORGET_CONSENT_CODE = 'document.getElementById("piwik_deny_consent").addEventListener("click", () => { _paq.push(["forgetConsentGiven"]); });' +REMEMBER_CONSENT_CODE = 'document.getElementById("piwik_give_consent").addEventListener("click", () => { _paq.push(["setConsentGiven"]); _paq.push(["rememberConsentGiven"]); });' + +DEFAULT_SCOPE = 'page' + +PiwikVar = namedtuple('PiwikVar', ('index', 'name', 'value', 'scope')) + + +register = Library() + + +@register.tag +def piwik(parser, token): + """ + Piwik tracking template tag. + + Renders Javascript code to track page visits. You must supply + your Piwik domain (plus optional URI path), and tracked site ID + in the ``PIWIK_DOMAIN_PATH`` and the ``PIWIK_SITE_ID`` setting. + + Custom variables can be passed in the ``piwik_vars`` context + variable. It is an iterable of custom variables as tuples like: + ``(index, name, value[, scope])`` where scope may be ``'page'`` + (default) or ``'visit'``. Index should be an integer and the + other parameters should be strings. + """ + bits = token.split_contents() + if len(bits) > 1: + raise TemplateSyntaxError("'%s' takes no arguments" % bits[0]) + return PiwikNode() + + +class PiwikNode(Node): + def __init__(self): + self.domain_path = \ + get_required_setting('PIWIK_DOMAIN_PATH', DOMAINPATH_RE, + "must be a domain name, optionally followed " + "by an URI path, no trailing slash (e.g. " + "piwik.example.com or my.piwik.server/path)") + self.site_id = \ + get_required_setting('PIWIK_SITE_ID', SITEID_RE, + "must be a (string containing a) number") + + def render(self, context): + custom_variables = context.get('piwik_vars', ()) + + complete_variables = (var if len(var) >= 4 else var + (DEFAULT_SCOPE,) + for var in custom_variables) + + variables_code = (VARIABLE_CODE % PiwikVar(*var)._asdict() + for var in complete_variables) + + commands = [] + if getattr(settings, 'PIWIK_DISABLE_COOKIES', False): + commands.append(DISABLE_COOKIES_CODE) + + if getattr(settings, 'PIWIK_ASK_FOR_CONSENT', False): + commands.append(ASK_FOR_CONSENT_CODE) + commands.append(FORGET_CONSENT_CODE) + commands.append(REMEMBER_CONSENT_CODE) + + userid = get_identity(context, 'piwik') + if userid is not None: + variables_code = chain(variables_code, ( + IDENTITY_CODE % {'userid': userid}, + )) + + html = TRACKING_CODE % { + 'url': self.domain_path, + 'siteid': self.site_id, + 'variables': '\n '.join(variables_code), + 'commands': '\n '.join(commands) + } + if is_internal_ip(context, 'PIWIK'): + html = disable_html(html, 'Piwik') + return html + + +def contribute_to_analytical(add_node): + PiwikNode() # ensure properly configured + add_node('body_bottom', PiwikNode) diff --git a/analytical/tests/test_tag_piwik.py b/analytical/tests/test_tag_piwik.py new file mode 100644 index 0000000..4becce1 --- /dev/null +++ b/analytical/tests/test_tag_piwik.py @@ -0,0 +1,157 @@ +""" +Tests for the Piwik template tags and filters. +""" + +from django.contrib.auth.models import User +from django.http import HttpRequest +from django.template import Context +from django.test.utils import override_settings + +from analytical.templatetags.piwik import PiwikNode +from analytical.tests.utils import TagTestCase +from analytical.utils import AnalyticalException + + +@override_settings(PIWIK_DOMAIN_PATH='example.com', PIWIK_SITE_ID='345') +class PiwikTagTestCase(TagTestCase): + """ + Tests for the ``piwik`` template tag. + """ + + def test_tag(self): + r = self.render_tag('piwik', 'piwik') + self.assertTrue('"//example.com/"' in r, r) + self.assertTrue("_paq.push(['setSiteId', 345]);" in r, r) + self.assertTrue('img src="//example.com/piwik.php?idsite=345"' + in r, r) + + def test_node(self): + r = PiwikNode().render(Context({})) + self.assertTrue('"//example.com/";' in r, r) + self.assertTrue("_paq.push(['setSiteId', 345]);" in r, r) + self.assertTrue('img src="//example.com/piwik.php?idsite=345"' + in r, r) + + @override_settings(PIWIK_DOMAIN_PATH='example.com/piwik', + PIWIK_SITE_ID='345') + def test_domain_path_valid(self): + r = self.render_tag('piwik', 'piwik') + self.assertTrue('"//example.com/piwik/"' in r, r) + + @override_settings(PIWIK_DOMAIN_PATH='example.com:1234', + PIWIK_SITE_ID='345') + def test_domain_port_valid(self): + r = self.render_tag('piwik', 'piwik') + self.assertTrue('"//example.com:1234/";' in r, r) + + @override_settings(PIWIK_DOMAIN_PATH='example.com:1234/piwik', + PIWIK_SITE_ID='345') + def test_domain_port_path_valid(self): + r = self.render_tag('piwik', 'piwik') + self.assertTrue('"//example.com:1234/piwik/"' in r, r) + + @override_settings(PIWIK_DOMAIN_PATH=None) + def test_no_domain(self): + self.assertRaises(AnalyticalException, PiwikNode) + + @override_settings(PIWIK_SITE_ID=None) + def test_no_siteid(self): + self.assertRaises(AnalyticalException, PiwikNode) + + @override_settings(PIWIK_SITE_ID='x') + def test_siteid_not_a_number(self): + self.assertRaises(AnalyticalException, PiwikNode) + + @override_settings(PIWIK_DOMAIN_PATH='http://www.example.com') + def test_domain_protocol_invalid(self): + self.assertRaises(AnalyticalException, PiwikNode) + + @override_settings(PIWIK_DOMAIN_PATH='example.com/') + def test_domain_slash_invalid(self): + self.assertRaises(AnalyticalException, PiwikNode) + + @override_settings(PIWIK_DOMAIN_PATH='example.com:123:456') + def test_domain_multi_port(self): + self.assertRaises(AnalyticalException, PiwikNode) + + @override_settings(PIWIK_DOMAIN_PATH='example.com:') + def test_domain_incomplete_port(self): + self.assertRaises(AnalyticalException, PiwikNode) + + @override_settings(PIWIK_DOMAIN_PATH='example.com:/piwik') + def test_domain_uri_incomplete_port(self): + self.assertRaises(AnalyticalException, PiwikNode) + + @override_settings(PIWIK_DOMAIN_PATH='example.com:12df') + def test_domain_port_invalid(self): + self.assertRaises(AnalyticalException, PiwikNode) + + @override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1']) + def test_render_internal_ip(self): + req = HttpRequest() + req.META['REMOTE_ADDR'] = '1.1.1.1' + context = Context({'request': req}) + r = PiwikNode().render(context) + self.assertTrue(r.startswith( + ''), r) + + def test_uservars(self): + context = Context({'piwik_vars': [(1, 'foo', 'foo_val'), + (2, 'bar', 'bar_val', 'page'), + (3, 'spam', 'spam_val', 'visit')]}) + r = PiwikNode().render(context) + msg = 'Incorrect Piwik custom variable rendering. Expected:\n%s\nIn:\n%s' + for var_code in ['_paq.push(["setCustomVariable", 1, "foo", "foo_val", "page"]);', + '_paq.push(["setCustomVariable", 2, "bar", "bar_val", "page"]);', + '_paq.push(["setCustomVariable", 3, "spam", "spam_val", "visit"]);']: + self.assertIn(var_code, r, msg % (var_code, r)) + + @override_settings(ANALYTICAL_AUTO_IDENTIFY=True) + def test_default_usertrack(self): + context = Context({ + 'user': User(username='BDFL', first_name='Guido', last_name='van Rossum') + }) + r = PiwikNode().render(context) + msg = 'Incorrect Piwik user tracking rendering.\nNot found:\n%s\nIn:\n%s' + var_code = '_paq.push(["setUserId", "BDFL"]);' + self.assertIn(var_code, r, msg % (var_code, r)) + + def test_piwik_usertrack(self): + context = Context({ + 'piwik_identity': 'BDFL' + }) + r = PiwikNode().render(context) + msg = 'Incorrect Piwik user tracking rendering.\nNot found:\n%s\nIn:\n%s' + var_code = '_paq.push(["setUserId", "BDFL"]);' + self.assertIn(var_code, r, msg % (var_code, r)) + + def test_analytical_usertrack(self): + context = Context({ + 'analytical_identity': 'BDFL' + }) + r = PiwikNode().render(context) + msg = 'Incorrect Piwik user tracking rendering.\nNot found:\n%s\nIn:\n%s' + var_code = '_paq.push(["setUserId", "BDFL"]);' + self.assertIn(var_code, r, msg % (var_code, r)) + + @override_settings(ANALYTICAL_AUTO_IDENTIFY=True) + def test_disable_usertrack(self): + context = Context({ + 'user': User(username='BDFL', first_name='Guido', last_name='van Rossum'), + 'piwik_identity': None + }) + r = PiwikNode().render(context) + msg = 'Incorrect Piwik user tracking rendering.\nFound:\n%s\nIn:\n%s' + var_code = '_paq.push(["setUserId", "BDFL"]);' + self.assertNotIn(var_code, r, msg % (var_code, r)) + + @override_settings(PIWIK_DISABLE_COOKIES=True) + def test_disable_cookies(self): + r = PiwikNode().render(Context({})) + self.assertTrue("_paq.push(['disableCookies']);" in r, r) + + @override_settings(PIWIK_ASK_FOR_CONSENT=True) + def test_disable_cookies(self): + r = PiwikNode().render(Context({})) + self.assertTrue("_paq.push([\'requireConsent\']);" in r, r) diff --git a/docs/services/piwik.rst b/docs/services/piwik.rst new file mode 100644 index 0000000..d012aca --- /dev/null +++ b/docs/services/piwik.rst @@ -0,0 +1,184 @@ +================================== +Matomo (formerly Piwik) -- open source web analytics +================================== + +Piwik_ is an open analytics platform currently used by individuals, +companies and governments all over the world. With Piwik, your data +will always be yours, because you run your own analytics server. + +.. _Piwik: http://www.piwik.org/ + + +Installation +============ + +To start using the Piwik integration, you must have installed the +django-analytical package and have added the ``analytical`` application +to :const:`INSTALLED_APPS` in your project :file:`settings.py` file. +See :doc:`../install` for details. + +Next you need to add the Piwik template tag to your templates. This +step is only needed if you are not using the generic +:ttag:`analytical.*` tags. If you are, skip to +:ref:`piwik-configuration`. + +The Piwik tracking code is inserted into templates using a template +tag. Load the :mod:`piwik` template tag library and insert the +:ttag:`piwik` tag. Because every page that you want to track must +have the tag, it is useful to add it to your base template. Insert +the tag at the bottom of the HTML body as recommended by the +`Piwik best practice for Integration Plugins`_:: + + {% load piwik %} + ... + {% piwik %} + + + +.. _`Piwik best practice for Integration Plugins`: http://piwik.org/integrate/how-to/ + + + +.. _piwik-configuration: + +Configuration +============= + +Before you can use the Piwik integration, you must first define +domain name and optional URI path to your Piwik server, as well as +the Piwik ID of the website you're tracking with your Piwik server, +in your project settings. + + +Setting the domain +------------------ + +Your Django project needs to know where your Piwik server is located. +Typically, you'll have Piwik installed on a subdomain of its own +(e.g. ``piwik.example.com``), otherwise it runs in a subdirectory of +a website of yours (e.g. ``www.example.com/piwik``). Set +:const:`PIWIK_DOMAIN_PATH` in the project :file:`settings.py` file +accordingly:: + + PIWIK_DOMAIN_PATH = 'piwik.example.com' + +If you do not set a domain the tracking code will not be rendered. + + +Setting the site ID +------------------- + +Your Piwik server can track several websites. Each website has its +site ID (this is the ``idSite`` parameter in the query string of your +browser's address bar when you visit the Piwik Dashboard). Set +:const:`PIWIK_SITE_ID` in the project :file:`settings.py` file to +the value corresponding to the website you're tracking:: + + PIWIK_SITE_ID = '4' + +If you do not set the site ID the tracking code will not be rendered. + + +.. _piwik-uservars: + +User variables +-------------- + +Piwik supports sending `custom variables`_ along with default statistics. If +you want to set a custom variable, use the context variable ``piwik_vars`` when +you render your template. It should be an iterable of custom variables +represented by tuples like: ``(index, name, value[, scope])``, where scope may +be ``'page'`` (default) or ``'visit'``. ``index`` should be an integer and the +other parameters should be strings. :: + + context = Context({ + 'piwik_vars': [(1, 'foo', 'Sir Lancelot of Camelot'), + (2, 'bar', 'To seek the Holy Grail', 'page'), + (3, 'spam', 'Blue', 'visit')] + }) + return some_template.render(context) + +Piwik default settings allow up to 5 custom variables for both scope. Variable +mapping betweeen index and name must stay constant, or the latest name +override the previous one. + +If you use the same user variables in different views and its value can +be computed from the HTTP request, you can also set them in a context +processor that you add to the :data:`TEMPLATE_CONTEXT_PROCESSORS` list +in :file:`settings.py`. + +.. _`custom variables`: http://developer.piwik.org/guides/tracking-javascript-guide#custom-variables + + +.. _piwik-user-tracking: + +User tracking +------------- + +If you use the standard Django authentication system, you can allow Piwik to +`track individual users`_ by setting the :data:`ANALYTICAL_AUTO_IDENTIFY` +setting to :const:`True`. This is enabled by default. Piwik will identify +users based on their ``username``. + +If you disable this settings, or want to customize what user id to use, you can +set the context variable ``analytical_identity`` (for global configuration) or +``piwik_identity`` (for Piwik specific configuration). Setting one to +:const:`None` will disable the user tracking feature:: + + # Piwik will identify this user as 'BDFL' if ANALYTICAL_AUTO_IDENTIFY is True or unset + request.user = User(username='BDFL', first_name='Guido', last_name='van Rossum') + + # Piwik will identify this user as 'Guido van Rossum' + request.user = User(username='BDFL', first_name='Guido', last_name='van Rossum') + context = Context({ + 'piwik_identity': request.user.get_full_name() + }) + + # Piwik will not identify this user (but will still collect statistics) + request.user = User(username='BDFL', first_name='Guido', last_name='van Rossum') + context = Context({ + 'piwik_identity': None + }) + +.. _`track individual users`: http://developer.piwik.org/guides/tracking-javascript-guide#user-id + +Disabling cookies +----------------- + +If you want to `disable cookies`_, set :data:`PIWIK_DISABLE_COOKIES` to +:const:`True`. This is disabled by default. + +.. _`disable cookies`: https://matomo.org/faq/general/faq_157/ + +Ask for consent +----------------- + +If you want to ask for consent set :data:`PIWIK_ASK_FOR_CONSENT` to +:const:`True`. This is disabled by default. + +To ask the visitor for consent just create DOM elements with the following id's: + +`piwik_deny_consent` - id for DOM element to click, if the user denies consent +`piwik_give_consent` - id for DOM element to click, if the user gives consent + +.. _`asking for consent`: https://developer.matomo.org/guides/tracking-javascript-guide#asking-for-consent + +Internal IP addresses +--------------------- + +Usually, you do not want to track clicks from your development or +internal IP addresses. By default, if the tags detect that the client +comes from any address in the :const:`ANALYTICAL_INTERNAL_IPS` (which +takes the value of :const:`INTERNAL_IPS` by default) the tracking code +is commented out. See :ref:`identifying-visitors` for important +information about detecting the visitor IP address. + + +---- + +Thanks go to Piwik for providing an excellent web analytics platform +entirely for free! Consider donating_ to ensure that they continue +their development efforts in the spirit of open source and freedom +for our personal data. + +.. _donating: http://piwik.org/donate/ From a01f9eaa4b1723a01129fe33b0a24f23179c92b0 Mon Sep 17 00:00:00 2001 From: Julian Haluska Date: Sun, 9 Jun 2019 21:46:39 +0200 Subject: [PATCH 14/15] Added 'ask for consent' logic and documentation to new matomo files --- analytical/tests/test_tag_matomo.py | 157 ++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 analytical/tests/test_tag_matomo.py diff --git a/analytical/tests/test_tag_matomo.py b/analytical/tests/test_tag_matomo.py new file mode 100644 index 0000000..01e6cb2 --- /dev/null +++ b/analytical/tests/test_tag_matomo.py @@ -0,0 +1,157 @@ +""" +Tests for the Matomo template tags and filters. +""" + +from django.contrib.auth.models import User +from django.http import HttpRequest +from django.template import Context +from django.test.utils import override_settings + +from analytical.templatetags.matomo import MatomoNode +from analytical.tests.utils import TagTestCase +from analytical.utils import AnalyticalException + + +@override_settings(MATOMO_DOMAIN_PATH='example.com', MATOMO_SITE_ID='345') +class MatomoTagTestCase(TagTestCase): + """ + Tests for the ``matomo`` template tag. + """ + + def test_tag(self): + r = self.render_tag('matomo', 'matomo') + self.assertTrue('"//example.com/"' in r, r) + self.assertTrue("_paq.push(['setSiteId', 345]);" in r, r) + self.assertTrue('img src="//example.com/piwik.php?idsite=345"' + in r, r) + + def test_node(self): + r = MatomoNode().render(Context({})) + self.assertTrue('"//example.com/";' in r, r) + self.assertTrue("_paq.push(['setSiteId', 345]);" in r, r) + self.assertTrue('img src="//example.com/piwik.php?idsite=345"' + in r, r) + + @override_settings(MATOMO_DOMAIN_PATH='example.com/matomo', + MATOMO_SITE_ID='345') + def test_domain_path_valid(self): + r = self.render_tag('matomo', 'matomo') + self.assertTrue('"//example.com/matomo/"' in r, r) + + @override_settings(MATOMO_DOMAIN_PATH='example.com:1234', + MATOMO_SITE_ID='345') + def test_domain_port_valid(self): + r = self.render_tag('matomo', 'matomo') + self.assertTrue('"//example.com:1234/";' in r, r) + + @override_settings(MATOMO_DOMAIN_PATH='example.com:1234/matomo', + MATOMO_SITE_ID='345') + def test_domain_port_path_valid(self): + r = self.render_tag('matomo', 'matomo') + self.assertTrue('"//example.com:1234/matomo/"' in r, r) + + @override_settings(MATOMO_DOMAIN_PATH=None) + def test_no_domain(self): + self.assertRaises(AnalyticalException, MatomoNode) + + @override_settings(MATOMO_SITE_ID=None) + def test_no_siteid(self): + self.assertRaises(AnalyticalException, MatomoNode) + + @override_settings(MATOMO_SITE_ID='x') + def test_siteid_not_a_number(self): + self.assertRaises(AnalyticalException, MatomoNode) + + @override_settings(MATOMO_DOMAIN_PATH='http://www.example.com') + def test_domain_protocol_invalid(self): + self.assertRaises(AnalyticalException, MatomoNode) + + @override_settings(MATOMO_DOMAIN_PATH='example.com/') + def test_domain_slash_invalid(self): + self.assertRaises(AnalyticalException, MatomoNode) + + @override_settings(MATOMO_DOMAIN_PATH='example.com:123:456') + def test_domain_multi_port(self): + self.assertRaises(AnalyticalException, MatomoNode) + + @override_settings(MATOMO_DOMAIN_PATH='example.com:') + def test_domain_incomplete_port(self): + self.assertRaises(AnalyticalException, MatomoNode) + + @override_settings(MATOMO_DOMAIN_PATH='example.com:/matomo') + def test_domain_uri_incomplete_port(self): + self.assertRaises(AnalyticalException, MatomoNode) + + @override_settings(MATOMO_DOMAIN_PATH='example.com:12df') + def test_domain_port_invalid(self): + self.assertRaises(AnalyticalException, MatomoNode) + + @override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1']) + def test_render_internal_ip(self): + req = HttpRequest() + req.META['REMOTE_ADDR'] = '1.1.1.1' + context = Context({'request': req}) + r = MatomoNode().render(context) + self.assertTrue(r.startswith( + ''), r) + + def test_uservars(self): + context = Context({'matomo_vars': [(1, 'foo', 'foo_val'), + (2, 'bar', 'bar_val', 'page'), + (3, 'spam', 'spam_val', 'visit')]}) + r = MatomoNode().render(context) + msg = 'Incorrect Matomo custom variable rendering. Expected:\n%s\nIn:\n%s' + for var_code in ['_paq.push(["setCustomVariable", 1, "foo", "foo_val", "page"]);', + '_paq.push(["setCustomVariable", 2, "bar", "bar_val", "page"]);', + '_paq.push(["setCustomVariable", 3, "spam", "spam_val", "visit"]);']: + self.assertIn(var_code, r, msg % (var_code, r)) + + @override_settings(ANALYTICAL_AUTO_IDENTIFY=True) + def test_default_usertrack(self): + context = Context({ + 'user': User(username='BDFL', first_name='Guido', last_name='van Rossum') + }) + r = MatomoNode().render(context) + msg = 'Incorrect Matomo user tracking rendering.\nNot found:\n%s\nIn:\n%s' + var_code = '_paq.push(["setUserId", "BDFL"]);' + self.assertIn(var_code, r, msg % (var_code, r)) + + def test_matomo_usertrack(self): + context = Context({ + 'matomo_identity': 'BDFL' + }) + r = MatomoNode().render(context) + msg = 'Incorrect Matomo user tracking rendering.\nNot found:\n%s\nIn:\n%s' + var_code = '_paq.push(["setUserId", "BDFL"]);' + self.assertIn(var_code, r, msg % (var_code, r)) + + def test_analytical_usertrack(self): + context = Context({ + 'analytical_identity': 'BDFL' + }) + r = MatomoNode().render(context) + msg = 'Incorrect Matomo user tracking rendering.\nNot found:\n%s\nIn:\n%s' + var_code = '_paq.push(["setUserId", "BDFL"]);' + self.assertIn(var_code, r, msg % (var_code, r)) + + @override_settings(ANALYTICAL_AUTO_IDENTIFY=True) + def test_disable_usertrack(self): + context = Context({ + 'user': User(username='BDFL', first_name='Guido', last_name='van Rossum'), + 'matomo_identity': None + }) + r = MatomoNode().render(context) + msg = 'Incorrect Matomo user tracking rendering.\nFound:\n%s\nIn:\n%s' + var_code = '_paq.push(["setUserId", "BDFL"]);' + self.assertNotIn(var_code, r, msg % (var_code, r)) + + @override_settings(MATOMO_DISABLE_COOKIES=True) + def test_disable_cookies(self): + r = MatomoNode().render(Context({})) + self.assertTrue("_paq.push(['disableCookies']);" in r, r) + + @override_settings(PIWIK_ASK_FOR_CONSENT=True) + def test_ask_for_consent(self): + r = PiwikNode().render(Context({})) + self.assertTrue("_paq.push(['requireConsent']);" in r, r) \ No newline at end of file From 114c65d060f3f714adde985dd7b3dc0b0b5b8ae9 Mon Sep 17 00:00:00 2001 From: ronardcaktus Date: Sat, 28 Feb 2026 08:55:13 -0500 Subject: [PATCH 15/15] Remove old files from pre-rebase --- analytical/templatetags/piwik.py | 126 ------------------- analytical/tests/test_tag_matomo.py | 157 ------------------------ analytical/tests/test_tag_piwik.py | 157 ------------------------ docs/services/piwik.rst | 184 ---------------------------- 4 files changed, 624 deletions(-) delete mode 100644 analytical/templatetags/piwik.py delete mode 100644 analytical/tests/test_tag_matomo.py delete mode 100644 analytical/tests/test_tag_piwik.py delete mode 100644 docs/services/piwik.rst diff --git a/analytical/templatetags/piwik.py b/analytical/templatetags/piwik.py deleted file mode 100644 index 10819a3..0000000 --- a/analytical/templatetags/piwik.py +++ /dev/null @@ -1,126 +0,0 @@ -""" -Piwik template tags and filters. -""" - -from __future__ import absolute_import - -from collections import namedtuple -from itertools import chain -import re - -from django.conf import settings -from django.template import Library, Node, TemplateSyntaxError - -from analytical.utils import (is_internal_ip, disable_html, - get_required_setting, get_identity) - - -# domain name (characters separated by a dot), optional port, optional URI path, no slash -DOMAINPATH_RE = re.compile(r'^(([^./?#@:]+\.)*[^./?#@:]+)+(:[0-9]+)?(/[^/?#@:]+)*$') - -# numeric ID -SITEID_RE = re.compile(r'^\d+$') - -TRACKING_CODE = """ - - -""" # noqa - -VARIABLE_CODE = '_paq.push(["setCustomVariable", %(index)s, "%(name)s", "%(value)s", "%(scope)s"]);' # noqa -IDENTITY_CODE = '_paq.push(["setUserId", "%(userid)s"]);' -DISABLE_COOKIES_CODE = '_paq.push([\'disableCookies\']);' -ASK_FOR_CONSENT_CODE = '_paq.push([\'requireConsent\']);' -FORGET_CONSENT_CODE = 'document.getElementById("piwik_deny_consent").addEventListener("click", () => { _paq.push(["forgetConsentGiven"]); });' -REMEMBER_CONSENT_CODE = 'document.getElementById("piwik_give_consent").addEventListener("click", () => { _paq.push(["setConsentGiven"]); _paq.push(["rememberConsentGiven"]); });' - -DEFAULT_SCOPE = 'page' - -PiwikVar = namedtuple('PiwikVar', ('index', 'name', 'value', 'scope')) - - -register = Library() - - -@register.tag -def piwik(parser, token): - """ - Piwik tracking template tag. - - Renders Javascript code to track page visits. You must supply - your Piwik domain (plus optional URI path), and tracked site ID - in the ``PIWIK_DOMAIN_PATH`` and the ``PIWIK_SITE_ID`` setting. - - Custom variables can be passed in the ``piwik_vars`` context - variable. It is an iterable of custom variables as tuples like: - ``(index, name, value[, scope])`` where scope may be ``'page'`` - (default) or ``'visit'``. Index should be an integer and the - other parameters should be strings. - """ - bits = token.split_contents() - if len(bits) > 1: - raise TemplateSyntaxError("'%s' takes no arguments" % bits[0]) - return PiwikNode() - - -class PiwikNode(Node): - def __init__(self): - self.domain_path = \ - get_required_setting('PIWIK_DOMAIN_PATH', DOMAINPATH_RE, - "must be a domain name, optionally followed " - "by an URI path, no trailing slash (e.g. " - "piwik.example.com or my.piwik.server/path)") - self.site_id = \ - get_required_setting('PIWIK_SITE_ID', SITEID_RE, - "must be a (string containing a) number") - - def render(self, context): - custom_variables = context.get('piwik_vars', ()) - - complete_variables = (var if len(var) >= 4 else var + (DEFAULT_SCOPE,) - for var in custom_variables) - - variables_code = (VARIABLE_CODE % PiwikVar(*var)._asdict() - for var in complete_variables) - - commands = [] - if getattr(settings, 'PIWIK_DISABLE_COOKIES', False): - commands.append(DISABLE_COOKIES_CODE) - - if getattr(settings, 'PIWIK_ASK_FOR_CONSENT', False): - commands.append(ASK_FOR_CONSENT_CODE) - commands.append(FORGET_CONSENT_CODE) - commands.append(REMEMBER_CONSENT_CODE) - - userid = get_identity(context, 'piwik') - if userid is not None: - variables_code = chain(variables_code, ( - IDENTITY_CODE % {'userid': userid}, - )) - - html = TRACKING_CODE % { - 'url': self.domain_path, - 'siteid': self.site_id, - 'variables': '\n '.join(variables_code), - 'commands': '\n '.join(commands) - } - if is_internal_ip(context, 'PIWIK'): - html = disable_html(html, 'Piwik') - return html - - -def contribute_to_analytical(add_node): - PiwikNode() # ensure properly configured - add_node('body_bottom', PiwikNode) diff --git a/analytical/tests/test_tag_matomo.py b/analytical/tests/test_tag_matomo.py deleted file mode 100644 index 01e6cb2..0000000 --- a/analytical/tests/test_tag_matomo.py +++ /dev/null @@ -1,157 +0,0 @@ -""" -Tests for the Matomo template tags and filters. -""" - -from django.contrib.auth.models import User -from django.http import HttpRequest -from django.template import Context -from django.test.utils import override_settings - -from analytical.templatetags.matomo import MatomoNode -from analytical.tests.utils import TagTestCase -from analytical.utils import AnalyticalException - - -@override_settings(MATOMO_DOMAIN_PATH='example.com', MATOMO_SITE_ID='345') -class MatomoTagTestCase(TagTestCase): - """ - Tests for the ``matomo`` template tag. - """ - - def test_tag(self): - r = self.render_tag('matomo', 'matomo') - self.assertTrue('"//example.com/"' in r, r) - self.assertTrue("_paq.push(['setSiteId', 345]);" in r, r) - self.assertTrue('img src="//example.com/piwik.php?idsite=345"' - in r, r) - - def test_node(self): - r = MatomoNode().render(Context({})) - self.assertTrue('"//example.com/";' in r, r) - self.assertTrue("_paq.push(['setSiteId', 345]);" in r, r) - self.assertTrue('img src="//example.com/piwik.php?idsite=345"' - in r, r) - - @override_settings(MATOMO_DOMAIN_PATH='example.com/matomo', - MATOMO_SITE_ID='345') - def test_domain_path_valid(self): - r = self.render_tag('matomo', 'matomo') - self.assertTrue('"//example.com/matomo/"' in r, r) - - @override_settings(MATOMO_DOMAIN_PATH='example.com:1234', - MATOMO_SITE_ID='345') - def test_domain_port_valid(self): - r = self.render_tag('matomo', 'matomo') - self.assertTrue('"//example.com:1234/";' in r, r) - - @override_settings(MATOMO_DOMAIN_PATH='example.com:1234/matomo', - MATOMO_SITE_ID='345') - def test_domain_port_path_valid(self): - r = self.render_tag('matomo', 'matomo') - self.assertTrue('"//example.com:1234/matomo/"' in r, r) - - @override_settings(MATOMO_DOMAIN_PATH=None) - def test_no_domain(self): - self.assertRaises(AnalyticalException, MatomoNode) - - @override_settings(MATOMO_SITE_ID=None) - def test_no_siteid(self): - self.assertRaises(AnalyticalException, MatomoNode) - - @override_settings(MATOMO_SITE_ID='x') - def test_siteid_not_a_number(self): - self.assertRaises(AnalyticalException, MatomoNode) - - @override_settings(MATOMO_DOMAIN_PATH='http://www.example.com') - def test_domain_protocol_invalid(self): - self.assertRaises(AnalyticalException, MatomoNode) - - @override_settings(MATOMO_DOMAIN_PATH='example.com/') - def test_domain_slash_invalid(self): - self.assertRaises(AnalyticalException, MatomoNode) - - @override_settings(MATOMO_DOMAIN_PATH='example.com:123:456') - def test_domain_multi_port(self): - self.assertRaises(AnalyticalException, MatomoNode) - - @override_settings(MATOMO_DOMAIN_PATH='example.com:') - def test_domain_incomplete_port(self): - self.assertRaises(AnalyticalException, MatomoNode) - - @override_settings(MATOMO_DOMAIN_PATH='example.com:/matomo') - def test_domain_uri_incomplete_port(self): - self.assertRaises(AnalyticalException, MatomoNode) - - @override_settings(MATOMO_DOMAIN_PATH='example.com:12df') - def test_domain_port_invalid(self): - self.assertRaises(AnalyticalException, MatomoNode) - - @override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1']) - def test_render_internal_ip(self): - req = HttpRequest() - req.META['REMOTE_ADDR'] = '1.1.1.1' - context = Context({'request': req}) - r = MatomoNode().render(context) - self.assertTrue(r.startswith( - ''), r) - - def test_uservars(self): - context = Context({'matomo_vars': [(1, 'foo', 'foo_val'), - (2, 'bar', 'bar_val', 'page'), - (3, 'spam', 'spam_val', 'visit')]}) - r = MatomoNode().render(context) - msg = 'Incorrect Matomo custom variable rendering. Expected:\n%s\nIn:\n%s' - for var_code in ['_paq.push(["setCustomVariable", 1, "foo", "foo_val", "page"]);', - '_paq.push(["setCustomVariable", 2, "bar", "bar_val", "page"]);', - '_paq.push(["setCustomVariable", 3, "spam", "spam_val", "visit"]);']: - self.assertIn(var_code, r, msg % (var_code, r)) - - @override_settings(ANALYTICAL_AUTO_IDENTIFY=True) - def test_default_usertrack(self): - context = Context({ - 'user': User(username='BDFL', first_name='Guido', last_name='van Rossum') - }) - r = MatomoNode().render(context) - msg = 'Incorrect Matomo user tracking rendering.\nNot found:\n%s\nIn:\n%s' - var_code = '_paq.push(["setUserId", "BDFL"]);' - self.assertIn(var_code, r, msg % (var_code, r)) - - def test_matomo_usertrack(self): - context = Context({ - 'matomo_identity': 'BDFL' - }) - r = MatomoNode().render(context) - msg = 'Incorrect Matomo user tracking rendering.\nNot found:\n%s\nIn:\n%s' - var_code = '_paq.push(["setUserId", "BDFL"]);' - self.assertIn(var_code, r, msg % (var_code, r)) - - def test_analytical_usertrack(self): - context = Context({ - 'analytical_identity': 'BDFL' - }) - r = MatomoNode().render(context) - msg = 'Incorrect Matomo user tracking rendering.\nNot found:\n%s\nIn:\n%s' - var_code = '_paq.push(["setUserId", "BDFL"]);' - self.assertIn(var_code, r, msg % (var_code, r)) - - @override_settings(ANALYTICAL_AUTO_IDENTIFY=True) - def test_disable_usertrack(self): - context = Context({ - 'user': User(username='BDFL', first_name='Guido', last_name='van Rossum'), - 'matomo_identity': None - }) - r = MatomoNode().render(context) - msg = 'Incorrect Matomo user tracking rendering.\nFound:\n%s\nIn:\n%s' - var_code = '_paq.push(["setUserId", "BDFL"]);' - self.assertNotIn(var_code, r, msg % (var_code, r)) - - @override_settings(MATOMO_DISABLE_COOKIES=True) - def test_disable_cookies(self): - r = MatomoNode().render(Context({})) - self.assertTrue("_paq.push(['disableCookies']);" in r, r) - - @override_settings(PIWIK_ASK_FOR_CONSENT=True) - def test_ask_for_consent(self): - r = PiwikNode().render(Context({})) - self.assertTrue("_paq.push(['requireConsent']);" in r, r) \ No newline at end of file diff --git a/analytical/tests/test_tag_piwik.py b/analytical/tests/test_tag_piwik.py deleted file mode 100644 index 4becce1..0000000 --- a/analytical/tests/test_tag_piwik.py +++ /dev/null @@ -1,157 +0,0 @@ -""" -Tests for the Piwik template tags and filters. -""" - -from django.contrib.auth.models import User -from django.http import HttpRequest -from django.template import Context -from django.test.utils import override_settings - -from analytical.templatetags.piwik import PiwikNode -from analytical.tests.utils import TagTestCase -from analytical.utils import AnalyticalException - - -@override_settings(PIWIK_DOMAIN_PATH='example.com', PIWIK_SITE_ID='345') -class PiwikTagTestCase(TagTestCase): - """ - Tests for the ``piwik`` template tag. - """ - - def test_tag(self): - r = self.render_tag('piwik', 'piwik') - self.assertTrue('"//example.com/"' in r, r) - self.assertTrue("_paq.push(['setSiteId', 345]);" in r, r) - self.assertTrue('img src="//example.com/piwik.php?idsite=345"' - in r, r) - - def test_node(self): - r = PiwikNode().render(Context({})) - self.assertTrue('"//example.com/";' in r, r) - self.assertTrue("_paq.push(['setSiteId', 345]);" in r, r) - self.assertTrue('img src="//example.com/piwik.php?idsite=345"' - in r, r) - - @override_settings(PIWIK_DOMAIN_PATH='example.com/piwik', - PIWIK_SITE_ID='345') - def test_domain_path_valid(self): - r = self.render_tag('piwik', 'piwik') - self.assertTrue('"//example.com/piwik/"' in r, r) - - @override_settings(PIWIK_DOMAIN_PATH='example.com:1234', - PIWIK_SITE_ID='345') - def test_domain_port_valid(self): - r = self.render_tag('piwik', 'piwik') - self.assertTrue('"//example.com:1234/";' in r, r) - - @override_settings(PIWIK_DOMAIN_PATH='example.com:1234/piwik', - PIWIK_SITE_ID='345') - def test_domain_port_path_valid(self): - r = self.render_tag('piwik', 'piwik') - self.assertTrue('"//example.com:1234/piwik/"' in r, r) - - @override_settings(PIWIK_DOMAIN_PATH=None) - def test_no_domain(self): - self.assertRaises(AnalyticalException, PiwikNode) - - @override_settings(PIWIK_SITE_ID=None) - def test_no_siteid(self): - self.assertRaises(AnalyticalException, PiwikNode) - - @override_settings(PIWIK_SITE_ID='x') - def test_siteid_not_a_number(self): - self.assertRaises(AnalyticalException, PiwikNode) - - @override_settings(PIWIK_DOMAIN_PATH='http://www.example.com') - def test_domain_protocol_invalid(self): - self.assertRaises(AnalyticalException, PiwikNode) - - @override_settings(PIWIK_DOMAIN_PATH='example.com/') - def test_domain_slash_invalid(self): - self.assertRaises(AnalyticalException, PiwikNode) - - @override_settings(PIWIK_DOMAIN_PATH='example.com:123:456') - def test_domain_multi_port(self): - self.assertRaises(AnalyticalException, PiwikNode) - - @override_settings(PIWIK_DOMAIN_PATH='example.com:') - def test_domain_incomplete_port(self): - self.assertRaises(AnalyticalException, PiwikNode) - - @override_settings(PIWIK_DOMAIN_PATH='example.com:/piwik') - def test_domain_uri_incomplete_port(self): - self.assertRaises(AnalyticalException, PiwikNode) - - @override_settings(PIWIK_DOMAIN_PATH='example.com:12df') - def test_domain_port_invalid(self): - self.assertRaises(AnalyticalException, PiwikNode) - - @override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1']) - def test_render_internal_ip(self): - req = HttpRequest() - req.META['REMOTE_ADDR'] = '1.1.1.1' - context = Context({'request': req}) - r = PiwikNode().render(context) - self.assertTrue(r.startswith( - ''), r) - - def test_uservars(self): - context = Context({'piwik_vars': [(1, 'foo', 'foo_val'), - (2, 'bar', 'bar_val', 'page'), - (3, 'spam', 'spam_val', 'visit')]}) - r = PiwikNode().render(context) - msg = 'Incorrect Piwik custom variable rendering. Expected:\n%s\nIn:\n%s' - for var_code in ['_paq.push(["setCustomVariable", 1, "foo", "foo_val", "page"]);', - '_paq.push(["setCustomVariable", 2, "bar", "bar_val", "page"]);', - '_paq.push(["setCustomVariable", 3, "spam", "spam_val", "visit"]);']: - self.assertIn(var_code, r, msg % (var_code, r)) - - @override_settings(ANALYTICAL_AUTO_IDENTIFY=True) - def test_default_usertrack(self): - context = Context({ - 'user': User(username='BDFL', first_name='Guido', last_name='van Rossum') - }) - r = PiwikNode().render(context) - msg = 'Incorrect Piwik user tracking rendering.\nNot found:\n%s\nIn:\n%s' - var_code = '_paq.push(["setUserId", "BDFL"]);' - self.assertIn(var_code, r, msg % (var_code, r)) - - def test_piwik_usertrack(self): - context = Context({ - 'piwik_identity': 'BDFL' - }) - r = PiwikNode().render(context) - msg = 'Incorrect Piwik user tracking rendering.\nNot found:\n%s\nIn:\n%s' - var_code = '_paq.push(["setUserId", "BDFL"]);' - self.assertIn(var_code, r, msg % (var_code, r)) - - def test_analytical_usertrack(self): - context = Context({ - 'analytical_identity': 'BDFL' - }) - r = PiwikNode().render(context) - msg = 'Incorrect Piwik user tracking rendering.\nNot found:\n%s\nIn:\n%s' - var_code = '_paq.push(["setUserId", "BDFL"]);' - self.assertIn(var_code, r, msg % (var_code, r)) - - @override_settings(ANALYTICAL_AUTO_IDENTIFY=True) - def test_disable_usertrack(self): - context = Context({ - 'user': User(username='BDFL', first_name='Guido', last_name='van Rossum'), - 'piwik_identity': None - }) - r = PiwikNode().render(context) - msg = 'Incorrect Piwik user tracking rendering.\nFound:\n%s\nIn:\n%s' - var_code = '_paq.push(["setUserId", "BDFL"]);' - self.assertNotIn(var_code, r, msg % (var_code, r)) - - @override_settings(PIWIK_DISABLE_COOKIES=True) - def test_disable_cookies(self): - r = PiwikNode().render(Context({})) - self.assertTrue("_paq.push(['disableCookies']);" in r, r) - - @override_settings(PIWIK_ASK_FOR_CONSENT=True) - def test_disable_cookies(self): - r = PiwikNode().render(Context({})) - self.assertTrue("_paq.push([\'requireConsent\']);" in r, r) diff --git a/docs/services/piwik.rst b/docs/services/piwik.rst deleted file mode 100644 index d012aca..0000000 --- a/docs/services/piwik.rst +++ /dev/null @@ -1,184 +0,0 @@ -================================== -Matomo (formerly Piwik) -- open source web analytics -================================== - -Piwik_ is an open analytics platform currently used by individuals, -companies and governments all over the world. With Piwik, your data -will always be yours, because you run your own analytics server. - -.. _Piwik: http://www.piwik.org/ - - -Installation -============ - -To start using the Piwik integration, you must have installed the -django-analytical package and have added the ``analytical`` application -to :const:`INSTALLED_APPS` in your project :file:`settings.py` file. -See :doc:`../install` for details. - -Next you need to add the Piwik template tag to your templates. This -step is only needed if you are not using the generic -:ttag:`analytical.*` tags. If you are, skip to -:ref:`piwik-configuration`. - -The Piwik tracking code is inserted into templates using a template -tag. Load the :mod:`piwik` template tag library and insert the -:ttag:`piwik` tag. Because every page that you want to track must -have the tag, it is useful to add it to your base template. Insert -the tag at the bottom of the HTML body as recommended by the -`Piwik best practice for Integration Plugins`_:: - - {% load piwik %} - ... - {% piwik %} - - - -.. _`Piwik best practice for Integration Plugins`: http://piwik.org/integrate/how-to/ - - - -.. _piwik-configuration: - -Configuration -============= - -Before you can use the Piwik integration, you must first define -domain name and optional URI path to your Piwik server, as well as -the Piwik ID of the website you're tracking with your Piwik server, -in your project settings. - - -Setting the domain ------------------- - -Your Django project needs to know where your Piwik server is located. -Typically, you'll have Piwik installed on a subdomain of its own -(e.g. ``piwik.example.com``), otherwise it runs in a subdirectory of -a website of yours (e.g. ``www.example.com/piwik``). Set -:const:`PIWIK_DOMAIN_PATH` in the project :file:`settings.py` file -accordingly:: - - PIWIK_DOMAIN_PATH = 'piwik.example.com' - -If you do not set a domain the tracking code will not be rendered. - - -Setting the site ID -------------------- - -Your Piwik server can track several websites. Each website has its -site ID (this is the ``idSite`` parameter in the query string of your -browser's address bar when you visit the Piwik Dashboard). Set -:const:`PIWIK_SITE_ID` in the project :file:`settings.py` file to -the value corresponding to the website you're tracking:: - - PIWIK_SITE_ID = '4' - -If you do not set the site ID the tracking code will not be rendered. - - -.. _piwik-uservars: - -User variables --------------- - -Piwik supports sending `custom variables`_ along with default statistics. If -you want to set a custom variable, use the context variable ``piwik_vars`` when -you render your template. It should be an iterable of custom variables -represented by tuples like: ``(index, name, value[, scope])``, where scope may -be ``'page'`` (default) or ``'visit'``. ``index`` should be an integer and the -other parameters should be strings. :: - - context = Context({ - 'piwik_vars': [(1, 'foo', 'Sir Lancelot of Camelot'), - (2, 'bar', 'To seek the Holy Grail', 'page'), - (3, 'spam', 'Blue', 'visit')] - }) - return some_template.render(context) - -Piwik default settings allow up to 5 custom variables for both scope. Variable -mapping betweeen index and name must stay constant, or the latest name -override the previous one. - -If you use the same user variables in different views and its value can -be computed from the HTTP request, you can also set them in a context -processor that you add to the :data:`TEMPLATE_CONTEXT_PROCESSORS` list -in :file:`settings.py`. - -.. _`custom variables`: http://developer.piwik.org/guides/tracking-javascript-guide#custom-variables - - -.. _piwik-user-tracking: - -User tracking -------------- - -If you use the standard Django authentication system, you can allow Piwik to -`track individual users`_ by setting the :data:`ANALYTICAL_AUTO_IDENTIFY` -setting to :const:`True`. This is enabled by default. Piwik will identify -users based on their ``username``. - -If you disable this settings, or want to customize what user id to use, you can -set the context variable ``analytical_identity`` (for global configuration) or -``piwik_identity`` (for Piwik specific configuration). Setting one to -:const:`None` will disable the user tracking feature:: - - # Piwik will identify this user as 'BDFL' if ANALYTICAL_AUTO_IDENTIFY is True or unset - request.user = User(username='BDFL', first_name='Guido', last_name='van Rossum') - - # Piwik will identify this user as 'Guido van Rossum' - request.user = User(username='BDFL', first_name='Guido', last_name='van Rossum') - context = Context({ - 'piwik_identity': request.user.get_full_name() - }) - - # Piwik will not identify this user (but will still collect statistics) - request.user = User(username='BDFL', first_name='Guido', last_name='van Rossum') - context = Context({ - 'piwik_identity': None - }) - -.. _`track individual users`: http://developer.piwik.org/guides/tracking-javascript-guide#user-id - -Disabling cookies ------------------ - -If you want to `disable cookies`_, set :data:`PIWIK_DISABLE_COOKIES` to -:const:`True`. This is disabled by default. - -.. _`disable cookies`: https://matomo.org/faq/general/faq_157/ - -Ask for consent ------------------ - -If you want to ask for consent set :data:`PIWIK_ASK_FOR_CONSENT` to -:const:`True`. This is disabled by default. - -To ask the visitor for consent just create DOM elements with the following id's: - -`piwik_deny_consent` - id for DOM element to click, if the user denies consent -`piwik_give_consent` - id for DOM element to click, if the user gives consent - -.. _`asking for consent`: https://developer.matomo.org/guides/tracking-javascript-guide#asking-for-consent - -Internal IP addresses ---------------------- - -Usually, you do not want to track clicks from your development or -internal IP addresses. By default, if the tags detect that the client -comes from any address in the :const:`ANALYTICAL_INTERNAL_IPS` (which -takes the value of :const:`INTERNAL_IPS` by default) the tracking code -is commented out. See :ref:`identifying-visitors` for important -information about detecting the visitor IP address. - - ----- - -Thanks go to Piwik for providing an excellent web analytics platform -entirely for free! Consider donating_ to ensure that they continue -their development efforts in the spirit of open source and freedom -for our personal data. - -.. _donating: http://piwik.org/donate/