Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Julian Haluska 2019-06-04 00:09:34 +02:00
commit 4b7bf0d228
19 changed files with 561 additions and 93 deletions

1
.bandit Symbolic link
View file

@ -0,0 +1 @@
tox.ini

1
.gitignore vendored
View file

@ -6,6 +6,7 @@
/build
/dist
/docs/_build
/MANIFEST
/docs/_templates/layout.html

View file

@ -1,39 +1,57 @@
language: python
cache: pip
dist: xenial
sudo: true
language: python
python:
- 2.7
- 3.4
- 3.5
- 3.6
- 2.7
- 3.4
- 3.5
- 3.6
- 3.7
env:
- DJANGO=1.7
- DJANGO=1.8
- DJANGO=1.9
- DJANGO=1.10
- DJANGO=1.11
- DJANGO=2.0
- DJANGO=2.1
- DJANGO=1.11
- DJANGO=2.1
- DJANGO=2.2
matrix:
allow_failures:
- env: TOXENV=bandit
exclude:
# Python/Django combinations that aren't officially supported
- { python: 3.5, env: DJANGO=1.7 }
- { python: 3.6, env: DJANGO=1.7 }
- { python: 3.6, env: DJANGO=1.8 }
- { python: 3.6, env: DJANGO=1.9 }
- { python: 3.6, env: DJANGO=1.10 }
- { python: 2.7, env: DJANGO=2.0 }
- { python: 2.7, env: DJANGO=2.1 }
- { python: 3.4, env: DJANGO=2.1 }
include:
- { python: 3.6, env: TOXENV=flake8 }
- { python: 3.6, env: TOXENV=readme }
# Work around Travis Python 3.7 issue: https://github.com/travis-ci/travis-ci/issues/9815
- { python: 3.7, env: DJANGO=1.11, dist: xenial, sudo: true }
- { python: 3.7, env: DJANGO=2.0, dist: xenial, sudo: true }
- { python: 3.7, env: DJANGO=2.1, dist: xenial, sudo: true }
# Python/Django combinations that aren't officially supported
- { env: DJANGO=1.11, python: 3.7 }
- { env: DJANGO=2.1, python: 2.7 }
- { env: DJANGO=2.1, python: 3.4 }
- { env: DJANGO=2.2, python: 2.7 }
- { env: DJANGO=2.2, python: 3.4 }
install:
- pip install tox-travis
- pip install tox-travis
script:
- tox
- tox
stages:
- lint
- test
- deploy
jobs:
include:
- { stage: lint, env: TOXENV=flake8, python: 3.7 }
- { stage: lint, env: TOXENV=bandit, python: 3.7 }
- { stage: lint, env: TOXENV=readme, python: 3.7 }
- stage: deploy
env:
python: 3.7
install: skip
script: skip
deploy:
provider: pypi
server: https://jazzband.co/projects/django-analytical/upload
distributions: sdist bdist_wheel
user: jazzband
password:
secure: JCr5hRjAeXuiISodCJf8HWd4BTJMpl2eiHI8NciPaSM9WwOOeUXxmlcP8+lWlXxgM4BYUC/O7Q90fkwj5x06n+z4oyJSEVerTvCDcpeZ68KMMG1tR1jTbHcxfEKoEvcs2J0fThJ9dIMtfbtUbIpzusJHkZPjsIy8HAJDw8knnJs=
on:
tags: true
repo: jazzband/django-analytical

View file

@ -5,8 +5,9 @@ from `Eric Davis`_, `Paul Oswald`_, `Uros Trebec`_, `Steven Skoczen`_,
`Philippe O. Wagner`_, `Max Arnold`_ , `Martín Gaitán`_, `Craig Bruce`_,
`Peter Bittner`_, `Scott Adams`_, `Eric Amador`_, `Alexandre Pocquet`_,
`Brad Pitcher`_, `Hugo Osvaldo Barrera`_, `Nikolay Korotkiy`_,
`Steve Schwarz`_, `Aleck Landgraf`_, `Marc Bourqui`_,
`Diederik van der Boor`_, `Matthäus G. Chajdas`_ and others.
`Steve Schwarz`_, `Aleck Landgraf`_, `Marc Bourqui`_,
`Diederik van der Boor`_, `Matthäus G. Chajdas`_, `Scott Karlin`_
and others.
Included Javascript code snippets for integration of the analytics
services were written by the respective service providers.
@ -46,3 +47,4 @@ The work on Intercom was made possible by `GreenKahuna`_.
.. _`Analytical`: https://github.com/jkrall/analytical
.. _`Bateau Knowledge`: http://www.bateauknowledge.nl/
.. _`GreenKahuna`: http://www.greenkahuna.com/
.. _`Scott Karlin`: https://github.com/sckarlin

View file

@ -1,4 +1,4 @@
Copyright (C) 2011-2016 Joost Cassee and others
Copyright (C) 2011-2019 Joost Cassee and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -1,7 +1,7 @@
django-analytical |latest-version|
==================================
|travis-ci| |coveralls| |health| |python-support| |license| |gitter| |jazzband|
|build-status| |coverage| |python-support| |license| |gitter| |jazzband|
The django-analytical application integrates analytics services into a
Django_ project.
@ -25,23 +25,20 @@ an asynchronous version of the Javascript code if possible.
.. |latest-version| image:: https://img.shields.io/pypi/v/django-analytical.svg
:alt: Latest version on PyPI
:target: https://pypi.python.org/pypi/django-analytical
.. |travis-ci| image:: https://img.shields.io/travis/jazzband/django-analytical/master.svg
:target: https://pypi.org/project/django-analytical/
.. |build-status| image:: https://img.shields.io/travis/jazzband/django-analytical/master.svg
:alt: Build status
:target: https://travis-ci.org/jazzband/django-analytical
.. |coveralls| image:: https://coveralls.io/repos/jazzband/django-analytical/badge.svg
.. |coverage| image:: https://img.shields.io/coveralls/github/jazzband/django-analytical/master.svg
:alt: Test coverage
:target: https://coveralls.io/r/jazzband/django-analytical
.. |health| image:: https://landscape.io/github/jazzband/django-analytical/master/landscape.svg?style=flat
:target: https://landscape.io/github/jazzband/django-analytical/master
:alt: Code health
.. |python-support| image:: https://img.shields.io/pypi/pyversions/django-analytical.svg
:target: https://pypi.python.org/pypi/django-analytical
:target: https://pypi.org/project/django-analytical/
:alt: Python versions
.. |license| image:: https://img.shields.io/pypi/l/django-analytical.svg
:alt: Software license
:target: https://github.com/jazzband/django-analytical/blob/master/LICENSE.txt
.. |gitter| image:: https://badges.gitter.im/Join%20Chat.svg
.. |gitter| image:: https://img.shields.io/gitter/room/jazzband/django-analytical.svg
:alt: Gitter chat room
:target: https://gitter.im/jazzband/django-analytical
.. |jazzband| image:: https://jazzband.co/static/img/badge.svg

View file

@ -5,5 +5,5 @@ Analytics service integration for Django projects
__author__ = "Joost Cassee"
__email__ = "joost@cassee.net"
__version__ = "2.5.0"
__copyright__ = "Copyright (C) 2011-2017 Joost Cassee and others"
__license__ = "MIT License"
__copyright__ = "Copyright (C) 2011-2019 Joost Cassee and contributors"
__license__ = "MIT"

View file

@ -30,6 +30,7 @@ TAG_MODULES = [
'analytical.intercom',
'analytical.kiss_insights',
'analytical.kiss_metrics',
'analytical.matomo',
'analytical.mixpanel',
'analytical.olark',
'analytical.optimizely',

View file

@ -30,14 +30,12 @@ m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
}})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', '{property_id}', 'auto', {create_fields});
{display_features}
ga('send', 'pageview');
{commands}
{commands}ga('send', 'pageview');
</script>
"""
REQUIRE_DISPLAY_FEATURES = "ga('require', 'displayfeatures');"
CUSTOM_VAR_CODE = "ga('set', '{name}', {value});"
ANONYMIZE_IP_CODE = "ga('set', 'anonymizeIp', true);"
REQUIRE_DISPLAY_FEATURES = "ga('require', 'displayfeatures');\n"
CUSTOM_VAR_CODE = "ga('set', '{name}', {value});\n"
ANONYMIZE_IP_CODE = "ga('set', 'anonymizeIp', true);\n"
register = Library()
@ -70,11 +68,13 @@ class GoogleAnalyticsJsNode(Node):
commands = self._get_custom_var_commands(context)
commands.extend(self._get_other_commands(context))
display_features = getattr(settings, 'GOOGLE_ANALYTICS_DISPLAY_ADVERTISING', False)
if display_features:
commands.insert(0, REQUIRE_DISPLAY_FEATURES)
html = SETUP_CODE.format(
property_id=self.property_id,
create_fields=json.dumps(create_fields),
display_features=REQUIRE_DISPLAY_FEATURES if display_features else '',
commands=" ".join(commands),
commands="".join(commands),
)
if is_internal_ip(context, 'GOOGLE_ANALYTICS'):
html = disable_html(html, 'Google Analytics')

View file

@ -8,8 +8,8 @@ import hashlib
import hmac
import json
import sys
import time
import re
import time
from django.conf import settings
from django.template import Library, Node, TemplateSyntaxError
@ -29,7 +29,7 @@ TRACKING_CODE = """
register = Library()
def _timestamp(when): # type: (datetime) -> float
def _timestamp(when):
"""
Python 2 compatibility for `datetime.timestamp()`.
"""
@ -37,7 +37,7 @@ def _timestamp(when): # type: (datetime) -> float
when.timestamp())
def _hashable_bytes(data): # type: (AnyStr) -> bytes
def _hashable_bytes(data):
"""
Coerce strings to hashable bytes.
"""
@ -49,7 +49,7 @@ def _hashable_bytes(data): # type: (AnyStr) -> bytes
raise TypeError(data)
def intercom_user_hash(data): # type: (AnyStr) -> Optional[str]
def intercom_user_hash(data):
"""
Return a SHA-256 HMAC `user_hash` as expected by Intercom, if configured.
@ -117,9 +117,9 @@ class IntercomNode(Node):
# (If both user_id and email are present, the user_id field takes precedence.)
# See:
# https://www.intercom.com/help/configure-intercom-for-your-product-or-site/staying-secure/enable-identity-verification-on-your-web-product
user_hash_data = params.get('user_id', params.get('email')) # type: Optional[str]
user_hash_data = params.get('user_id', params.get('email'))
if user_hash_data:
user_hash = intercom_user_hash(str(user_hash_data)) # type: Optional[str]
user_hash = intercom_user_hash(str(user_hash_data))
if user_hash is not None:
params.setdefault('user_hash', user_hash)

View file

@ -0,0 +1,118 @@
"""
Matomo 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 = """
<script type="text/javascript">
var _paq = window._paq || [];
%(variables)s
%(commands)s
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="//%(url)s/";
_paq.push(['setTrackerUrl', u+'matomo.php']);
_paq.push(['setSiteId', %(siteid)s]);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
})();
</script>
<noscript><p><img src="//%(url)s/piwik.php?idsite=%(siteid)s" style="border:0;" alt="" /></p></noscript>
""" # 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\']);'
DEFAULT_SCOPE = 'page'
MatomoVar = namedtuple('MatomoVar', ('index', 'name', 'value', 'scope'))
register = Library()
@register.tag
def matomo(parser, token):
"""
Matomo tracking template tag.
Renders Javascript code to track page visits. You must supply
your Matomo domain (plus optional URI path), and tracked site ID
in the ``MATOMO_DOMAIN_PATH`` and the ``MATOMO_SITE_ID`` setting.
Custom variables can be passed in the ``matomo_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 MatomoNode()
class MatomoNode(Node):
def __init__(self):
self.domain_path = \
get_required_setting('MATOMO_DOMAIN_PATH', DOMAINPATH_RE,
"must be a domain name, optionally followed "
"by an URI path, no trailing slash (e.g. "
"matomo.example.com or my.matomo.server/path)")
self.site_id = \
get_required_setting('MATOMO_SITE_ID', SITEID_RE,
"must be a (string containing a) number")
def render(self, context):
custom_variables = context.get('matomo_vars', ())
complete_variables = (var if len(var) >= 4 else var + (DEFAULT_SCOPE,)
for var in custom_variables)
variables_code = (VARIABLE_CODE % MatomoVar(*var)._asdict()
for var in complete_variables)
commands = []
if getattr(settings, 'MATOMO_DISABLE_COOKIES', False):
commands.append(DISABLE_COOKIES_CODE)
userid = get_identity(context, 'matomo')
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, 'MATOMO'):
html = disable_html(html, 'Matomo')
return html
def contribute_to_analytical(add_node):
MatomoNode() # ensure properly configured
add_node('body_bottom', MatomoNode)

View file

@ -14,6 +14,8 @@ from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import (is_internal_ip, disable_html,
get_required_setting, get_identity)
import warnings
warnings.warn('The Piwik module is deprecated; use the Matomo module.', DeprecationWarning)
# domain name (characters separated by a dot), optional port, optional URI path, no slash
DOMAINPATH_RE = re.compile(r'^(([^./?#@:]+\.)*[^./?#@:]+)+(:[0-9]+)?(/[^/?#@:]+)*$')

View file

@ -0,0 +1,152 @@
"""
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(
'<!-- Matomo disabled on internal IP address'), r)
self.assertTrue(r.endswith('-->'), 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)

View file

@ -19,8 +19,3 @@ def setup(app):
rolename="lookup",
indextemplate="pair: %s; field lookup type",
)
app.add_description_unit(
directivename="decorator",
rolename="dec",
indextemplate="pair: %s; function decorator",
)

View file

@ -155,6 +155,11 @@ settings required to enable each service are listed here:
KISS_METRICS_API_KEY = '0123456789abcdef0123456789abcdef01234567'
* :doc:`Matomo (formerly Piwik) <services/matomo>`::
MATOMO_DOMAIN_PATH = 'your.matomo.server/optional/path'
MATOMO_SITE_ID = '123'
* :doc:`Mixpanel <services/mixpanel>`::
MIXPANEL_API_TOKEN = '0123456789abcdef0123456789abcdef'
@ -171,7 +176,7 @@ settings required to enable each service are listed here:
PERFORMABLE_API_KEY = '123abc'
* :doc:`Matomo (formerly Piwik) <services/piwik>`::
* :doc:`Piwik (deprecated, see Matomo) <services/piwik>`::
PIWIK_DOMAIN_PATH = 'your.piwik.server/optional/path'
PIWIK_SITE_ID = '123'

160
docs/services/matomo.rst Normal file
View file

@ -0,0 +1,160 @@
==================================
Matomo (formerly Piwik) -- open source web analytics
==================================
Matomo_ is an open analytics platform currently used by individuals,
companies and governments all over the world.
.. _Matomo: http://matomo.org/
Installation
============
To start using the Matomo 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 Matomo 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:`matomo-configuration`.
The Matomo tracking code is inserted into templates using a template
tag. Load the :mod:`matomo` template tag library and insert the
:ttag:`matomo` 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
`Matomo best practice for Integration Plugins`_::
{% load matomo %}
...
{% matomo %}
</body>
</html>
.. _`Matomo best practice for Integration Plugins`: http://matomo.org/integrate/how-to/
.. _matomo-configuration:
Configuration
=============
Before you can use the Matomo integration, you must first define
domain name and optional URI path to your Matomo server, as well as
the Matomo ID of the website you're tracking with your Matomo server,
in your project settings.
Setting the domain
------------------
Your Django project needs to know where your Matomo server is located.
Typically, you'll have Matomo installed on a subdomain of its own
(e.g. ``matomo.example.com``), otherwise it runs in a subdirectory of
a website of yours (e.g. ``www.example.com/matomo``). Set
:const:`MATOMO_DOMAIN_PATH` in the project :file:`settings.py` file
accordingly::
MATOMO_DOMAIN_PATH = 'matomo.example.com'
If you do not set a domain the tracking code will not be rendered.
Setting the site ID
-------------------
Your Matomo 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 Matomo Dashboard). Set
:const:`MATOMO_SITE_ID` in the project :file:`settings.py` file to
the value corresponding to the website you're tracking::
MATOMO_SITE_ID = '4'
If you do not set the site ID the tracking code will not be rendered.
.. _matomo-uservars:
User variables
--------------
Matomo supports sending `custom variables`_ along with default statistics. If
you want to set a custom variable, use the context variable ``matomo_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({
'matomo_vars': [(1, 'foo', 'Sir Lancelot of Camelot'),
(2, 'bar', 'To seek the Holy Grail', 'page'),
(3, 'spam', 'Blue', 'visit')]
})
return some_template.render(context)
Matomo 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.matomo.org/guides/tracking-javascript-guide#custom-variables
.. _matomo-user-tracking:
User tracking
-------------
If you use the standard Django authentication system, you can allow Matomo to
`track individual users`_ by setting the :data:`ANALYTICAL_AUTO_IDENTIFY`
setting to :const:`True`. This is enabled by default. Matomo 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
``matomo_identity`` (for Matomo specific configuration). Setting one to
:const:`None` will disable the user tracking feature::
# Matomo 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')
# Matomo will identify this user as 'Guido van Rossum'
request.user = User(username='BDFL', first_name='Guido', last_name='van Rossum')
context = Context({
'matomo_identity': request.user.get_full_name()
})
# Matomo will not identify this user (but will still collect statistics)
request.user = User(username='BDFL', first_name='Guido', last_name='van Rossum')
context = Context({
'matomo_identity': None
})
.. _`track individual users`: http://developer.matomo.org/guides/tracking-javascript-guide#user-id
Disabling cookies
-----------------
If you want to `disable cookies`_, set :data:`MATOMO_DISABLE_COOKIES` to
:const:`True`. This is disabled by default.
.. _`disable cookies`: https://matomo.org/faq/general/faq_157/
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.

View file

@ -1,5 +1,5 @@
==================================
Matomo (formerly Piwik) -- open source web analytics
Piwik (deprecated) -- open source web analytics
==================================
Piwik_ is an open analytics platform currently used by individuals,
@ -9,6 +9,14 @@ will always be yours, because you run your own analytics server.
.. _Piwik: http://www.piwik.org/
Deprecated
==========
Note that Piwik is now known as Matomo. New projects should use the
Matomo integration. The Piwik integration in django-analytical is
deprecated and eventually will be removed.
Installation
============

View file

@ -61,6 +61,7 @@ setup(
license=package.__license__,
description=package.__doc__.strip(),
long_description=read_file('README.rst'),
long_description_content_type='text/x-rst',
author=package.__author__,
author_email=package.__email__,
packages=[
@ -74,13 +75,9 @@ setup(
'Development Status :: 5 - Production/Stable',
'Environment :: Web Environment',
'Framework :: Django',
'Framework :: Django :: 1.7',
'Framework :: Django :: 1.8',
'Framework :: Django :: 1.9',
'Framework :: Django :: 1.10',
'Framework :: Django :: 1.11',
'Framework :: Django :: 2.0',
'Framework :: Django :: 2.1',
'Framework :: Django :: 2.2',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
@ -99,7 +96,4 @@ setup(
url='https://github.com/jazzband/django-analytical',
download_url='https://github.com/jazzband/django-analytical/archive/master.zip',
cmdclass=cmdclass,
install_requires=[
'Django>=1.7.0',
],
)

52
tox.ini
View file

@ -1,15 +1,13 @@
[tox]
envlist =
# Python/Django combinations that are officially supported
py{27,34}-django17
py{27,34,35}-django18
py{27,34,35}-django19
py{27,34,35}-django110
py{27,34,35,36,37}-django111
py{34,35,36,37}-django20
py{35,36,37}-django21
py{27,34,35,36}-django111
py{35,36,37}-django{21,22}
flake8
bandit
readme
docs
clean
[testenv]
commands =
@ -18,33 +16,49 @@ commands =
deps =
coverage
coveralls
django17: Django>=1.7,<1.8
django18: Django>=1.8,<1.9
django19: Django>=1.9,<1.10
django110: Django>=1.10,<1.11
django111: Django>=1.11,<2.0
django20: Django>=2.0,<2.1
django21: Django>=2.1,<2.2
django22: Django>=2.2,<3.0
passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH
whitelist_externals = sh
[testenv:bandit]
deps = bandit
commands = bandit -r --ini tox.ini
[testenv:clean]
deps = pyclean
commands =
py3clean -v {toxinidir}
rm -rf .tox/ django_analytical.egg-info/ build/ dist/ docs/_build/
whitelist_externals =
rm
[testenv:docs]
deps = sphinx
commands = sphinx-build -b html -d docs/_build/doctrees docs docs/_build/html
whitelist_externals = make
[testenv:flake8]
deps = flake8
commands = flake8
[testenv:readme]
deps = readme_renderer
commands = python setup.py check --restructuredtext --strict
deps = twine
commands =
{envpython} setup.py -q sdist bdist_wheel
twine check dist/*
[travis:env]
DJANGO =
1.7: django17
1.8: django18
1.9: django19
1.10: django110
1.11: django111
2.0: django20
2.1: django21
2.2: django22
[bandit]
exclude = .cache,.git,.tox,build,dist,docs,tests
targets = .
[flake8]
exclude = .cache,.git,.tox,build,dist
max-line-length = 100