From 81502c3d68c7be83baead48eb50105fb015725e5 Mon Sep 17 00:00:00 2001 From: Joost Cassee Date: Mon, 8 Apr 2019 15:40:05 +0200 Subject: [PATCH 01/17] Fix flake8 warnings by removing typing hints Unfortunately, typing does not exist in Python 2 so we cannot fix the warnings by importing the types. --- analytical/templatetags/intercom.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/analytical/templatetags/intercom.py b/analytical/templatetags/intercom.py index 888a0ab..a2d34a3 100644 --- a/analytical/templatetags/intercom.py +++ b/analytical/templatetags/intercom.py @@ -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) From c4934b4c968f6ad1e04c24c826e46f981b3dd994 Mon Sep 17 00:00:00 2001 From: Joost Cassee Date: Mon, 8 Apr 2019 16:42:28 +0200 Subject: [PATCH 02/17] Use xenial dist for Travis readme check This is required because Django now requires Sqlite version 3.8.3 or higher. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 69a13a3..cd922b7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,7 +27,7 @@ matrix: - { python: 3.4, env: DJANGO=2.1 } include: - { python: 3.6, env: TOXENV=flake8 } - - { python: 3.6, env: TOXENV=readme } + - { python: 3.6, env: TOXENV=readme, dist: xenial } # requires sqlite3>=3.8.3 which is not in trusty # 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 } From 4a07cb35e47343026182b4ee398693bbf625bc05 Mon Sep 17 00:00:00 2001 From: Peter Bittner Date: Mon, 8 Apr 2019 20:14:01 +0200 Subject: [PATCH 03/17] Replace deprecated readme check Make flake8 ignore build folders --- tox.ini | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 383fdba..a37c5e9 100644 --- a/tox.ini +++ b/tox.ini @@ -33,8 +33,10 @@ 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 = @@ -47,4 +49,5 @@ DJANGO = 2.1: django21 [flake8] +exclude = .cache,.git,.tox,build,dist max-line-length = 100 From 4e874d88aebe431c4e74db7a084dc8d4f4cf71d8 Mon Sep 17 00:00:00 2001 From: Peter Bittner Date: Mon, 8 Apr 2019 21:50:10 +0200 Subject: [PATCH 04/17] Add Django 2.2 to the test matrix Remove all officially unsupported Python+Django combinations Add Bandit check, add Codacy badge Add docs target in Tox configuration Use build stages with Travis CI --- .bandit | 1 + .travis.yml | 73 ++++++++++++++++++++++++++++++----------------------- README.rst | 4 +-- tox.ini | 46 ++++++++++++++++++++------------- 4 files changed, 74 insertions(+), 50 deletions(-) create mode 120000 .bandit diff --git a/.bandit b/.bandit new file mode 120000 index 0000000..bf39a01 --- /dev/null +++ b/.bandit @@ -0,0 +1 @@ +tox.ini \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index cd922b7..d9fa40a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,39 +1,50 @@ language: python -cache: pip - 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: 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, dist: xenial } # requires sqlite3>=3.8.3 which is not in trusty - # 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 + distributions: sdist bdist_wheel + user: bittner + password: + secure: JrgKNLiONVUmfg+Vt9NxDccXl9DbPqbXq8xfMY4tFb6CfRWCESgt26wPORyFJvP3FB9yvKXRL5Lb5AFWroTM+h1zlsdHmvEnSP4ldSVU8a2U35lqjZgN9lhViYtn2rt+wcvdWToBAAZolHKRzUoElVCHMt5kBQBSI1oSmNfCn6k= + on: + tags: true diff --git a/README.rst b/README.rst index 9b41962..80044b1 100644 --- a/README.rst +++ b/README.rst @@ -32,8 +32,8 @@ an asynchronous version of the Javascript code if possible. .. |coveralls| image:: https://coveralls.io/repos/jazzband/django-analytical/badge.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 +.. |health| image:: https://img.shields.io/codacy/grade/********************/master.svg + :target: https://www.codacy.com/app/jazzband/django-analytical :alt: Code health .. |python-support| image:: https://img.shields.io/pypi/pyversions/django-analytical.svg :target: https://pypi.python.org/pypi/django-analytical diff --git a/tox.ini b/tox.ini index a37c5e9..4c447e8 100644 --- a/tox.ini +++ b/tox.ini @@ -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,16 +16,30 @@ 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 = + rm -rf .tox/ django_analytical.egg-info/ build/ dist/ docs/_build/ + py3clean -v {toxinidir} +whitelist_externals = + rm + +[testenv:docs] +deps = sphinx +changedir = docs +commands = make html +whitelist_externals = make + [testenv:flake8] deps = flake8 commands = flake8 @@ -40,13 +52,13 @@ commands = [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 From 36808726196e673f02c8865e3e02ea495669c934 Mon Sep 17 00:00:00 2001 From: Peter Bittner Date: Mon, 8 Apr 2019 22:00:24 +0200 Subject: [PATCH 05/17] Travis still doesn't support Python 3.7 in the default image --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index d9fa40a..f9b5e00 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,6 @@ +dist: xenial +sudo: true + language: python python: - 2.7 From 29a95f036931e09a4e0bec4529330ec972ed5602 Mon Sep 17 00:00:00 2001 From: Peter Bittner Date: Mon, 8 Apr 2019 22:05:14 +0200 Subject: [PATCH 06/17] Allow Bandit to fail (for now) on Travis --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index f9b5e00..50ebdc3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,8 @@ env: - DJANGO=2.2 matrix: + allow_failures: + - env: TOXENV=bandit exclude: # Python/Django combinations that aren't officially supported - { env: DJANGO=1.11, python: 3.7 } From dc975aa2d3aafc9d5cf3ebaf487211a477103edb Mon Sep 17 00:00:00 2001 From: Peter Bittner Date: Tue, 9 Apr 2019 09:09:13 +0200 Subject: [PATCH 07/17] Use Shields.io badges uniformly --- README.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 80044b1..972d8b6 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,7 @@ django-analytical |latest-version| ================================== -|travis-ci| |coveralls| |health| |python-support| |license| |gitter| |jazzband| +|build-status| |coverage| |health| |python-support| |license| |gitter| |jazzband| The django-analytical application integrates analytics services into a Django_ project. @@ -25,23 +25,23 @@ 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://img.shields.io/codacy/grade/********************/master.svg :target: https://www.codacy.com/app/jazzband/django-analytical :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 From 367606c12c6dcf8795e16285c7b3444e43d2309e Mon Sep 17 00:00:00 2001 From: Peter Bittner Date: Tue, 9 Apr 2019 09:10:01 +0200 Subject: [PATCH 08/17] Remove code-health button (Codacy/Landscape) for now --- README.rst | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 972d8b6..0fa7527 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,7 @@ django-analytical |latest-version| ================================== -|build-status| |coverage| |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. @@ -32,9 +32,6 @@ an asynchronous version of the Javascript code if possible. .. |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://img.shields.io/codacy/grade/********************/master.svg - :target: https://www.codacy.com/app/jazzband/django-analytical - :alt: Code health .. |python-support| image:: https://img.shields.io/pypi/pyversions/django-analytical.svg :target: https://pypi.org/project/django-analytical/ :alt: Python versions From bfe92c716c91b525bfcb773925cfdfe5fd363e72 Mon Sep 17 00:00:00 2001 From: Peter Bittner Date: Tue, 9 Apr 2019 10:44:29 +0200 Subject: [PATCH 09/17] Ensure Tox targets clean and docs are working Update packaging information (Django 2.2, license) Remove problematic install_requires --- .gitignore | 1 + analytical/__init__.py | 4 ++-- docs/_ext/local.py | 5 ----- setup.py | 10 ++-------- tox.ini | 5 ++--- 5 files changed, 7 insertions(+), 18 deletions(-) diff --git a/.gitignore b/.gitignore index f3546f5..6e8fe88 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ /build /dist +/docs/_build /MANIFEST /docs/_templates/layout.html diff --git a/analytical/__init__.py b/analytical/__init__.py index d5354f6..9a9d9b1 100644 --- a/analytical/__init__.py +++ b/analytical/__init__.py @@ -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" diff --git a/docs/_ext/local.py b/docs/_ext/local.py index b3280ac..3dae85a 100644 --- a/docs/_ext/local.py +++ b/docs/_ext/local.py @@ -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", - ) diff --git a/setup.py b/setup.py index 51026df..66b55b1 100644 --- a/setup.py +++ b/setup.py @@ -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', - ], ) diff --git a/tox.ini b/tox.ini index 4c447e8..2eebb33 100644 --- a/tox.ini +++ b/tox.ini @@ -29,15 +29,14 @@ commands = bandit -r --ini tox.ini [testenv:clean] deps = pyclean commands = - rm -rf .tox/ django_analytical.egg-info/ build/ dist/ docs/_build/ py3clean -v {toxinidir} + rm -rf .tox/ django_analytical.egg-info/ build/ dist/ docs/_build/ whitelist_externals = rm [testenv:docs] deps = sphinx -changedir = docs -commands = make html +commands = sphinx-build -b html -d docs/_build/doctrees docs docs/_build/html whitelist_externals = make [testenv:flake8] From e5f8c199dc749f061fd78963e5ca1e748714fa01 Mon Sep 17 00:00:00 2001 From: Scott Karlin Date: Sat, 6 Apr 2019 17:55:12 -0400 Subject: [PATCH 10/17] Create Matomo module; leave Piwik to be deprecated With the rebranding of Piwik to Matomo, this commit: * copies the piwik module to matomo and rebrands * notes that the piwik module is deprecated * updates the javascript to the current Matomo version Implements #132 --- analytical/templatetags/analytical.py | 1 + analytical/templatetags/matomo.py | 118 +++++++++++++++++++ analytical/templatetags/piwik.py | 2 + analytical/tests/test_tag_matomo.py | 152 ++++++++++++++++++++++++ docs/install.rst | 7 +- docs/services/matomo.rst | 160 ++++++++++++++++++++++++++ docs/services/piwik.rst | 9 +- 7 files changed, 447 insertions(+), 2 deletions(-) create mode 100644 analytical/templatetags/matomo.py create mode 100644 analytical/tests/test_tag_matomo.py create mode 100644 docs/services/matomo.rst diff --git a/analytical/templatetags/analytical.py b/analytical/templatetags/analytical.py index 6e74bd7..f74684e 100644 --- a/analytical/templatetags/analytical.py +++ b/analytical/templatetags/analytical.py @@ -30,6 +30,7 @@ TAG_MODULES = [ 'analytical.intercom', 'analytical.kiss_insights', 'analytical.kiss_metrics', + 'analytical.matomo', 'analytical.mixpanel', 'analytical.olark', 'analytical.optimizely', diff --git a/analytical/templatetags/matomo.py b/analytical/templatetags/matomo.py new file mode 100644 index 0000000..98bdf8a --- /dev/null +++ b/analytical/templatetags/matomo.py @@ -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 = """ + + +""" # 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) diff --git a/analytical/templatetags/piwik.py b/analytical/templatetags/piwik.py index 9eebc4a..ec8b97d 100644 --- a/analytical/templatetags/piwik.py +++ b/analytical/templatetags/piwik.py @@ -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 will be deprecated', DeprecationWarning) # domain name (characters separated by a dot), optional port, optional URI path, no slash DOMAINPATH_RE = re.compile(r'^(([^./?#@:]+\.)*[^./?#@:]+)+(:[0-9]+)?(/[^/?#@:]+)*$') diff --git a/analytical/tests/test_tag_matomo.py b/analytical/tests/test_tag_matomo.py new file mode 100644 index 0000000..0e34beb --- /dev/null +++ b/analytical/tests/test_tag_matomo.py @@ -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/matomo.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/matomo.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) diff --git a/docs/install.rst b/docs/install.rst index b641065..1f97f07 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -155,6 +155,11 @@ settings required to enable each service are listed here: KISS_METRICS_API_KEY = '0123456789abcdef0123456789abcdef01234567' +* :doc:`Matomo (formerly Piwik) `:: + + MATOMO_DOMAIN_PATH = 'your.matomo.server/optional/path' + MATOMO_SITE_ID = '123' + * :doc:`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) `:: +* :doc:`Piwik (deprecated, see Matomo) `:: PIWIK_DOMAIN_PATH = 'your.piwik.server/optional/path' PIWIK_SITE_ID = '123' diff --git a/docs/services/matomo.rst b/docs/services/matomo.rst new file mode 100644 index 0000000..0aa4731 --- /dev/null +++ b/docs/services/matomo.rst @@ -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 %} + + + +.. _`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. diff --git a/docs/services/piwik.rst b/docs/services/piwik.rst index e4c9aee..21ea04e 100644 --- a/docs/services/piwik.rst +++ b/docs/services/piwik.rst @@ -1,5 +1,5 @@ ================================== -Matomo (formerly Piwik) -- open source web analytics +Piwik (Deprecation Path) -- open source web analytics ================================== Piwik_ is an open analytics platform currently used by individuals, @@ -9,6 +9,13 @@ will always be yours, because you run your own analytics server. .. _Piwik: http://www.piwik.org/ +Deprecation Path +================ + +Note that Piwik is now known as Matomo. New projects should use the +Matomo integration. The Piwik integration in django-analytical will +eventually be deprecated. + Installation ============ From 3b1ab2bbed75befc69a0e59a0a99623a81fffe2d Mon Sep 17 00:00:00 2001 From: Scott Karlin Date: Mon, 8 Apr 2019 08:53:00 -0400 Subject: [PATCH 11/17] Matomo still uses "piwik" in img tag --- analytical/tests/test_tag_matomo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/analytical/tests/test_tag_matomo.py b/analytical/tests/test_tag_matomo.py index 0e34beb..573b79a 100644 --- a/analytical/tests/test_tag_matomo.py +++ b/analytical/tests/test_tag_matomo.py @@ -22,14 +22,14 @@ class MatomoTagTestCase(TagTestCase): 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/matomo.php?idsite=345"' + 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/matomo.php?idsite=345"' + self.assertTrue('img src="//example.com/piwik.php?idsite=345"' in r, r) @override_settings(MATOMO_DOMAIN_PATH='example.com/matomo', From ef0b19ccb2c38737ea8ceda707b3ea4c5d93f119 Mon Sep 17 00:00:00 2001 From: Scott Karlin Date: Mon, 8 Apr 2019 08:54:17 -0400 Subject: [PATCH 12/17] Clarify Piwik deprecation status --- analytical/templatetags/piwik.py | 2 +- docs/services/piwik.rst | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/analytical/templatetags/piwik.py b/analytical/templatetags/piwik.py index ec8b97d..37d9777 100644 --- a/analytical/templatetags/piwik.py +++ b/analytical/templatetags/piwik.py @@ -15,7 +15,7 @@ from analytical.utils import (is_internal_ip, disable_html, get_required_setting, get_identity) import warnings -warnings.warn('The Piwik module will be deprecated', DeprecationWarning) +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]+)?(/[^/?#@:]+)*$') diff --git a/docs/services/piwik.rst b/docs/services/piwik.rst index 21ea04e..852c19f 100644 --- a/docs/services/piwik.rst +++ b/docs/services/piwik.rst @@ -1,5 +1,5 @@ ================================== -Piwik (Deprecation Path) -- open source web analytics +Piwik (deprecated) -- open source web analytics ================================== Piwik_ is an open analytics platform currently used by individuals, @@ -9,12 +9,13 @@ will always be yours, because you run your own analytics server. .. _Piwik: http://www.piwik.org/ -Deprecation Path -================ +Deprecated +========== Note that Piwik is now known as Matomo. New projects should use the -Matomo integration. The Piwik integration in django-analytical will -eventually be deprecated. +Matomo integration. The Piwik integration in django-analytical is +deprecated and eventually will be removed. + Installation ============ From 02b0768c9706046b5c4cab82d4b1bac6ca592c5e Mon Sep 17 00:00:00 2001 From: Scott Karlin Date: Mon, 8 Apr 2019 09:08:34 -0400 Subject: [PATCH 13/17] Correct code alignment (PEP8) --- analytical/tests/test_tag_matomo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/analytical/tests/test_tag_matomo.py b/analytical/tests/test_tag_matomo.py index 573b79a..d3d6785 100644 --- a/analytical/tests/test_tag_matomo.py +++ b/analytical/tests/test_tag_matomo.py @@ -98,8 +98,8 @@ class MatomoTagTestCase(TagTestCase): def test_uservars(self): context = Context({'matomo_vars': [(1, 'foo', 'foo_val'), - (2, 'bar', 'bar_val', 'page'), - (3, 'spam', 'spam_val', 'visit')]}) + (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"]);', From d5de14ebdc08ca480adff4cda3141c4815926292 Mon Sep 17 00:00:00 2001 From: Scott Karlin Date: Mon, 8 Apr 2019 18:25:17 -0400 Subject: [PATCH 14/17] Update authors and license date --- AUTHORS.rst | 6 ++++-- LICENSE.txt | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 7cc73a7..a32ba85 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -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 diff --git a/LICENSE.txt b/LICENSE.txt index 5a77e87..91979f6 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (C) 2011-2016 Joost Cassee and others +Copyright (C) 2011-2019 Joost Cassee and others Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From b96bf0050a4ed6f6706271e8bba5c579149b6055 Mon Sep 17 00:00:00 2001 From: Scott Karlin Date: Tue, 9 Apr 2019 17:33:43 -0400 Subject: [PATCH 15/17] Update copyright line --- LICENSE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE.txt b/LICENSE.txt index 91979f6..596cd51 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (C) 2011-2019 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 From 84e7dab3d29e99781935a140aacd50d2d2791d1d Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Thu, 18 Apr 2019 12:46:16 +0200 Subject: [PATCH 16/17] Make sure Google Analytics calls for ga('set', ...) happen before ga('send', ...) The changes for the old ga.js code were addressed in #127, before the new analytics.js code was merged by #135 (commits mention #108) The display_features and commands could be merged to avoid unneeded blank lines. --- analytical/templatetags/google_analytics_js.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/analytical/templatetags/google_analytics_js.py b/analytical/templatetags/google_analytics_js.py index 377d1f8..d23467e 100644 --- a/analytical/templatetags/google_analytics_js.py +++ b/analytical/templatetags/google_analytics_js.py @@ -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'); """ -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') From c7f8dc21ad3b51238461dfa6eb739d13c0031fce Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Tue, 23 Apr 2019 11:23:13 +0200 Subject: [PATCH 17/17] Updated Travis config with Jazzband release credentials. --- .travis.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 50ebdc3..6731930 100644 --- a/.travis.yml +++ b/.travis.yml @@ -47,9 +47,11 @@ jobs: script: skip deploy: provider: pypi + server: https://jazzband.co/projects/django-analytical/upload distributions: sdist bdist_wheel - user: bittner + user: jazzband password: - secure: JrgKNLiONVUmfg+Vt9NxDccXl9DbPqbXq8xfMY4tFb6CfRWCESgt26wPORyFJvP3FB9yvKXRL5Lb5AFWroTM+h1zlsdHmvEnSP4ldSVU8a2U35lqjZgN9lhViYtn2rt+wcvdWToBAAZolHKRzUoElVCHMt5kBQBSI1oSmNfCn6k= + secure: JCr5hRjAeXuiISodCJf8HWd4BTJMpl2eiHI8NciPaSM9WwOOeUXxmlcP8+lWlXxgM4BYUC/O7Q90fkwj5x06n+z4oyJSEVerTvCDcpeZ68KMMG1tR1jTbHcxfEKoEvcs2J0fThJ9dIMtfbtUbIpzusJHkZPjsIy8HAJDw8knnJs= on: tags: true + repo: jazzband/django-analytical