Compare commits

..

No commits in common. "main" and "v1.0.0" have entirely different histories.
main ... v1.0.0

135 changed files with 2605 additions and 6126 deletions

3
.coveragerc Normal file
View file

@ -0,0 +1,3 @@
[run]
source = analytical
omit = analytical/tests/*

View file

@ -1,35 +0,0 @@
name: Check
on:
pull_request:
branches:
- main
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
env:
- lint
- format
- audit
- package
- docs
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install prerequisites
run: python -m pip install tox
- name: Run ${{ matrix.env }}
run: tox -e ${{ matrix.env }}

View file

@ -1,44 +0,0 @@
name: Release
on:
push:
tags:
- '*'
env:
PIP_DISABLE_PIP_VERSION_CHECK: '1'
PY_COLORS: '1'
jobs:
build:
if: github.repository == 'jazzband/django-analytical'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: pip
cache-dependency-path: |
**/pyproject.toml
- name: Install dependencies
run: |
python -m pip install tox
- name: Build package
run: |
tox -e package
- name: Upload packages to Jazzband
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: jazzband
password: ${{ secrets.JAZZBAND_RELEASE_KEY }}
repository_url: https://jazzband.co/projects/django-analytical/upload

View file

@ -1,58 +0,0 @@
name: Test
on:
pull_request:
branches:
- main
push:
branches:
- main
env:
PIP_DISABLE_PIP_VERSION_CHECK: '1'
PY_COLORS: '1'
jobs:
python-django:
runs-on: ubuntu-latest
strategy:
max-parallel: 5
matrix:
python-version:
- '3.10'
- '3.11'
- '3.12'
- '3.13'
django-version:
- '4.2'
- '5.1'
- '5.2'
include:
- { python-version: '3.9', django-version: '4.2' }
exclude:
- { python-version: '3.13', django-version: '4.2' }
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip
cache-dependency-path: |
**/pyproject.toml
- name: Install dependencies
run: |
python -m pip install tox tox-gh-actions
- name: Tox tests (Python ${{ matrix.python-version }}, Django ${{ matrix.django-version }})
run: tox
env:
DJANGO: ${{ matrix.django-version }}
- name: Upload coverage
uses: codecov/codecov-action@v5
with:
name: Python ${{ matrix.python-version }}

22
.gitignore vendored
View file

@ -1,23 +1,15 @@
.*.sw?
/*.geany /*.geany
/.idea /.idea
/.tox /.tox
/.vscode /.coverage
/build
/dist
/MANIFEST
/docs/_templates/layout.html
*.pyc *.pyc
*.pyo *.pyo
/.coverage
/coverage.xml
/tests/*-report.json
/tests/*-report.xml
/build
/dist
/docs/_build
/docs/_templates/layout.html
/MANIFEST
*.egg-info *.egg-info
/requirements.txt
/uv.lock

View file

@ -1 +0,0 @@
repos: []

View file

@ -1,18 +0,0 @@
# Read the Docs configuration file
# https://docs.readthedocs.io/en/stable/config-file/v2.html
version: 2
build:
os: ubuntu-24.04
tools:
python: "3.12"
sphinx:
configuration: docs/conf.py
# We recommend specifying your dependencies to enable reproducible builds:
# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
# python:
# install:
# - requirements: docs/requirements.txt

22
.travis.yml Normal file
View file

@ -0,0 +1,22 @@
language: python
install:
- pip install coveralls tox
script:
- tox
env: # generate list with: $ tox -l | xargs -I ITEM echo " - TOXENV="ITEM
- TOXENV=py26-django14
- TOXENV=py26-django15
- TOXENV=py26-django16
- TOXENV=py27-django14
- TOXENV=py27-django15
- TOXENV=py27-django16
- TOXENV=py27-django17
- TOXENV=py27-django18
- TOXENV=py33-django15
- TOXENV=py33-django16
- TOXENV=py33-django17
- TOXENV=py33-django18
- TOXENV=py34-django15
- TOXENV=py34-django16
- TOXENV=py34-django17
- TOXENV=py34-django18

36
AUTHORS.rst Normal file
View file

@ -0,0 +1,36 @@
The django-analytical package was written by `Joost Cassee`_, with
contributions from `Eric Davis`_, `Paul Oswald`_, `Uros Trebec`_,
`Steven Skoczen`_, `Piet Delport`_, `Sandra Mau`_, `Simon Ye`_,
`Tinnet Coronam`_, `Philippe O. Wagner`_, `Max Arnold`_ , `Martín
Gaitán`_, `Craig Bruce`_, `Peter Bittner`_, `Scott Adams`_, `Eric Amador`_,
`Alexandre Pocquet`_ and others.
Included Javascript code snippets for integration of the analytics
services were written by the respective service providers.
The application was inspired by and uses ideas from Analytical_, Joshua
Krall's all-purpose analytics front-end for Rails.
The work on Crazy Egg was made possible by `Bateau Knowledge`_.
The work on Intercom was made possible by `GreenKahuna`_.
.. _`Joost Cassee`: mailto:joost@cassee.net
.. _`Eric Davis`: https://github.com/edavis
.. _`Paul Oswald`: https://github.com/poswald
.. _`Uros Trebec`: https://github.com/failedguidedog
.. _`Steven Skoczen`: https://github.com/skoczen
.. _`Piet Delport`: https://github.com/pjdelport
.. _`Sandra Mau`: https://github.com/xthepoet
.. _`Simon Ye`: https://github.com/yesimon
.. _`Tinnet Coronam`: https://github.com/tinnet
.. _`Philippe O. Wagner`: mailto:admin@arteria.ch
.. _`Max Arnold`: https://github.com/max-arnold
.. _`Martín Gaitán`: https://github.com/mgaitan
.. _`Craig Bruce`: https://github.com/craigbruce
.. _`Peter Bittner`: https://github.com/bittner
.. _`Scott Adams`: https://github.com/7wonders
.. _`Eric Amador`: https://github.com/amadornimbis
.. _`Alexandre Pocquet`: https://github.com/apocquet
.. _`Analytical`: https://github.com/jkrall/analytical
.. _`Bateau Knowledge`: http://www.bateauknowledge.nl/
.. _`GreenKahuna`: http://www.greenkahuna.com/

View file

@ -1,92 +1,3 @@
(unreleased)
------------
* 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
-------------
* Remove deprecated Piwik integration. Use Matomo instead! (Peter Bittner)
* Migrate packaging from setup.py to pyproject.toml with Ruff for linting
and formatting (Peter Bittner)
* Remove obsolete type attribute in script tags for JavaScript (Peter Bittner)
* Drop the end-of-life Python 3.8 as required by changed semantics of the
license metadata field (Peter Bittner)
* Remove AUTHORS file to avoid confusion; this is now metadata maintained
in pyproject.toml (Peter Bittner)
* Add more configuration options for Woopra (Peter Bittner)
Version 3.1.0
-------------
* Rename default branch from master to main (Peter Bittner, Jannis Leidel)
* Modernize packaging setup, add pyproject.toml (Peter Bittner)
* Integrate isort, reorganize imports (David Smith)
* Refactor test suite from Python unit tests to Pytest (David Smith)
* Add Heap integration (Garrett Coakley)
* Drop Django 3.1, cover Django 4.0 and Python 3.10 in test suite (David Smith)
Version 3.0.0
-------------
* Add support for Lucky Orange (Peter Bittner)
* Add missing instructions in Installation chapter of the docs (Peter Bittner)
* Migrate test setup to Pytest (David Smith, Peter Bittner, Pi Delport)
* Support Django 3.1 and Python 3.9, drop Django 1.11 and Python 2.7/3.5 (David Smith)
* Migrate from Travis CI to GitHub Actions (Jannis Leidel)
* Update accepted patterns (regex) for Google Analytics GTag (Taha Rushain)
* Scope Piwik warning to use of Piwik (Hugo Barrera)
* Add ``user_id`` to Google Analytics GTag (Sean Wallace)
Version 2.6.0
-------------
* Support Django 3.0 and Python 3.8, drop Django 2.1
* Add support for Google Analytics Tag Manager (Marc Bourqui)
* Add Matomo, the renamed version of Piwik (Scott Karlin)
* Move Joost's project over to the Jazzband
Version 2.5.0
-------------
* Add support for Google analytics.js (Marc Bourqui)
* Add support for Intercom HMAC identity verification (Pi Delport)
* Add support for Hotjar (Pi Delport)
* Make sure _trackPageview happens before other settings in Google Analytics
(Diederik van der Boor)
Version 2.4.0
-------------
* Support Django 2.0 (Matthäus G. Chajdas)
Version 2.3.0
-------------
* Add Facebook Pixel support (Pi Delport)
* Add Python 3.6 and Django 1.10 & 1.11 tests (Pi Delport)
* Drop Python 3.2 support
Version 2.2.2
-------------
* Allow port in Piwik domain path. (Alex Ramsay)
Version 2.2.1
-------------
* Fix a bug with the extra Google Analytics variables also pushing the `_gat.`
flag onto the configuration array.
Version 2.2.0
-------------
* Update Woopra JavaScript snippet (Aleck Landgraf)
Version 2.1.0
-------------
* Support Rating\@mail.ru (Nikolay Korotkiy)
* Support Yandex.Metrica (Nikolay Korotkiy)
* Add support for extra Google Analytics variables (Steve Schwarz)
* Remove support for Reinvigorate (service shut down)
Version 2.0.0
-------------
* Support Django 1.9, drop support for Django < 1.7 (Hugo Osvaldo Barrera)
* Support custom user models with an alternative username field (Brad Pitcher)
Version 1.0.0 Version 1.0.0
------------- -------------
* Add Piwik user variables support (Alexandre Pocquet) * Add Piwik user variables support (Alexandre Pocquet)
@ -142,7 +53,7 @@ Version 0.14.0
Version 0.13.0 Version 0.13.0
-------------- --------------
* Add support for the KISSmetrics alias feature (Sandra Mau) * Add support for the KISSmetrics alias feature (Sandra Mau)
* Update testing code for Django 1.4 (Pi Delport) * Update testing code for Django 1.4 (Piet Delport)
Version 0.12.0 Version 0.12.0
-------------- --------------
@ -218,7 +129,7 @@ Version 0.5.0
------------- -------------
* Split off Geckoboard support into django-geckoboard_. * Split off Geckoboard support into django-geckoboard_.
.. _django-geckoboard: https://pypi.org/project/django-geckoboard .. _django-geckoboard: http://pypi.python.org/pypi/django-geckoboard
Version 0.4.0 Version 0.4.0
------------- -------------

View file

@ -1,46 +0,0 @@
# Code of Conduct
As contributors and maintainers of the Jazzband projects, and in the interest of
fostering an open and welcoming community, we pledge to respect all people who
contribute through reporting issues, posting feature requests, updating documentation,
submitting pull requests or patches, and other activities.
We are committed to making participation in the Jazzband a harassment-free experience
for everyone, regardless of the level of experience, gender, gender identity and
expression, sexual orientation, disability, personal appearance, body size, race,
ethnicity, age, religion, or nationality.
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery
- Personal attacks
- Trolling or insulting/derogatory comments
- Public or private harassment
- Publishing other's private information, such as physical or electronic addresses,
without explicit permission
- Other unethical or unprofessional conduct
The Jazzband roadies have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are not
aligned to this Code of Conduct, or to ban temporarily or permanently any contributor
for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
By adopting this Code of Conduct, the roadies commit themselves to fairly and
consistently applying these principles to every aspect of managing the jazzband
projects. Roadies who do not follow or enforce the Code of Conduct may be permanently
removed from the Jazzband roadies.
This code of conduct applies both within project spaces and in public spaces when an
individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by
contacting the roadies at `roadies@jazzband.co`. All complaints will be reviewed and
investigated and will result in a response that is deemed necessary and appropriate to
the circumstances. Roadies are obligated to maintain confidentiality with regard to the
reporter of an incident.
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version
1.3.0, available at [https://contributor-covenant.org/version/1/3/0/][version]
[homepage]: https://contributor-covenant.org
[version]: https://contributor-covenant.org/version/1/3/0/

View file

@ -1,5 +0,0 @@
.. image:: https://jazzband.co/static/img/jazzband.svg
:target: https://jazzband.co/
:alt: Jazzband
This is a `Jazzband <https://jazzband.co>`_ project. By contributing you agree to abide by the `Contributor Code of Conduct <https://jazzband.co/about/conduct>`_ and follow the `guidelines <https://jazzband.co/about/guidelines>`_.

View file

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

View file

@ -1,3 +1,2 @@
include LICENSE.txt *.rst include LICENSE.txt *.rst
recursive-include docs *.rst *.py recursive-include docs *.rst *.py
recursive-include tests *.py

View file

@ -1,14 +1,14 @@
django-analytical |latest-version| django-analytical |latest-version|
================================== ==================================
|build-status| |coverage| |python-support| |license| |jazzband| |travis-ci| |coveralls| |downloads| |license|
The django-analytical application integrates analytics services into a The django-analytical application integrates analytics services into a
Django_ project. Django_ project.
.. start docs include .. start docs include
Using an analytics service with a Django project means adding JavaScript Using an analytics service with a Django project means adding Javascript
tracking code to the project templates. Of course, every service has tracking code to the project templates. Of course, every service has
its own specific installation instructions. Furthermore, you need to its own specific installation instructions. Furthermore, you need to
include your unique identifiers, which then end up in the templates. include your unique identifiers, which then end up in the templates.
@ -19,28 +19,25 @@ behind a generic interface, and keeps personal information and
configuration out of the templates. Its goal is to make the basic configuration out of the templates. Its goal is to make the basic
set-up very simple, while allowing advanced users to customize tracking. set-up very simple, while allowing advanced users to customize tracking.
Each service is set up as recommended by the services themselves, using Each service is set up as recommended by the services themselves, using
an asynchronous version of the JavaScript code if possible. an asynchronous version of the Javascript code if possible.
.. end docs include .. end docs include
.. |latest-version| image:: https://img.shields.io/pypi/v/django-analytical.svg .. |latest-version| image:: https://img.shields.io/pypi/v/django-analytical.svg
:alt: Latest version on PyPI :alt: Latest version on PyPI
:target: https://pypi.org/project/django-analytical/ :target: https://pypi.python.org/pypi/django-analytical
.. |build-status| image:: https://github.com/jazzband/django-analytical/workflows/Test/badge.svg .. |travis-ci| image:: https://travis-ci.org/jcassee/django-analytical.svg
:target: https://github.com/jazzband/django-analytical/actions :alt: Build status
:alt: GitHub Actions :target: https://travis-ci.org/jcassee/django-analytical
.. |coverage| image:: https://codecov.io/gh/jazzband/django-analytical/branch/main/graph/badge.svg .. |coveralls| image:: https://coveralls.io/repos/jcassee/django-analytical/badge.svg
:alt: Test coverage :alt: Test coverage
:target: https://codecov.io/gh/jazzband/django-analytical :target: https://coveralls.io/r/jcassee/django-analytical
.. |python-support| image:: https://img.shields.io/pypi/pyversions/django-analytical.svg .. |downloads| image:: https://img.shields.io/pypi/dm/django-analytical.svg
:target: https://pypi.org/project/django-analytical/ :alt: Monthly downloads from PyPI
:alt: Python versions :target: https://pypi.python.org/pypi/django-analytical
.. |license| image:: https://img.shields.io/pypi/l/django-analytical.svg .. |license| image:: https://img.shields.io/pypi/l/django-analytical.svg
:alt: Software license :alt: Software license
:target: https://github.com/jazzband/django-analytical/blob/main/LICENSE.txt :target: https://github.com/jcassee/django-analytical/blob/master/LICENSE.txt
.. |jazzband| image:: https://jazzband.co/static/img/badge.svg
:alt: Jazzband
:target: https://jazzband.co/projects/django-analytical
.. _`Django`: http://www.djangoproject.com/ .. _`Django`: http://www.djangoproject.com/
Currently Supported Services Currently Supported Services
@ -50,92 +47,65 @@ Currently Supported Services
* `Clickmap`_ visual click tracking * `Clickmap`_ visual click tracking
* `Clicky`_ traffic analysis * `Clicky`_ traffic analysis
* `Crazy Egg`_ visual click tracking * `Crazy Egg`_ visual click tracking
* `Facebook Pixel`_ advertising analytics * `Gaug.es`_ realtime traffic tracking
* `Gaug.es`_ real time web analytics
* `Google Analytics`_ traffic analysis * `Google Analytics`_ traffic analysis
* `GoSquared`_ traffic monitoring * `GoSquared`_ traffic monitoring
* `Heap`_ analytics and events tracking
* `Hotjar`_ analytics and user feedback
* `HubSpot`_ inbound marketing * `HubSpot`_ inbound marketing
* `Intercom`_ live chat and support * `Intercom`_ live chat and support
* `KISSinsights`_ feedback surveys * `KISSinsights`_ feedback surveys
* `KISSmetrics`_ funnel analysis * `KISSmetrics`_ funnel analysis
* `Lucky Orange`_ analytics and user feedback
* `Mixpanel`_ event tracking * `Mixpanel`_ event tracking
* `Olark`_ visitor chat * `Olark`_ visitor chat
* `Optimizely`_ A/B testing * `Optimizely`_ A/B testing
* `Performable`_ web analytics and landing pages * `Performable`_ web analytics and landing pages
* `Matomo (formerly Piwik)`_ open source web analytics * `Piwik`_ open source web analytics
* `Rating\@Mail.ru`_ web analytics * `Reinvigorate`_ visitor tracking
* `SnapEngage`_ live chat * `SnapEngage`_ live chat
* `Spring Metrics`_ conversion tracking * `Spring Metrics`_ conversion tracking
* `UserVoice`_ user feedback and helpdesk * `UserVoice`_ user feedback and helpdesk
* `Woopra`_ web analytics * `Woopra`_ web analytics
* `Yandex.Metrica`_ web analytics
.. _`Chartbeat`: http://www.chartbeat.com/ .. _`Chartbeat`: http://www.chartbeat.com/
.. _`Clickmap`: http://clickmap.ch/ .. _`Clickmap`: http://getclickmap.com/
.. _`Clicky`: http://getclicky.com/ .. _`Clicky`: http://getclicky.com/
.. _`Crazy Egg`: http://www.crazyegg.com/ .. _`Crazy Egg`: http://www.crazyegg.com/
.. _`Facebook Pixel`: https://developers.facebook.com/docs/facebook-pixel/ .. _`Gaug.es`: http://gaug.es/
.. _`Gaug.es`: http://get.gaug.es/
.. _`Google Analytics`: http://www.google.com/analytics/ .. _`Google Analytics`: http://www.google.com/analytics/
.. _`GoSquared`: http://www.gosquared.com/ .. _`GoSquared`: http://www.gosquared.com/
.. _`Heap`: https://heapanalytics.com/
.. _`Hotjar`: https://www.hotjar.com/
.. _`HubSpot`: http://www.hubspot.com/ .. _`HubSpot`: http://www.hubspot.com/
.. _`Intercom`: http://www.intercom.io/ .. _`Intercom`: http://www.intercom.io/
.. _`KISSinsights`: http://www.kissinsights.com/ .. _`KISSinsights`: http://www.kissinsights.com/
.. _`KISSmetrics`: http://www.kissmetrics.com/ .. _`KISSmetrics`: http://www.kissmetrics.com/
.. _`Lucky Orange`: http://www.luckyorange.com/
.. _`Mixpanel`: http://www.mixpanel.com/ .. _`Mixpanel`: http://www.mixpanel.com/
.. _`Olark`: http://www.olark.com/ .. _`Olark`: http://www.olark.com/
.. _`Optimizely`: http://www.optimizely.com/ .. _`Optimizely`: http://www.optimizely.com/
.. _`Performable`: http://www.performable.com/ .. _`Performable`: http://www.performable.com/
.. _`Matomo (formerly Piwik)`: https://matomo.org .. _`Piwik`: http://www.piwik.org/
.. _`Rating\@Mail.ru`: http://top.mail.ru/ .. _`Reinvigorate`: http://www.reinvigorate.net/
.. _`SnapEngage`: http://www.snapengage.com/ .. _`SnapEngage`: http://www.snapengage.com/
.. _`Spring Metrics`: http://www.springmetrics.com/ .. _`Spring Metrics`: http://www.springmetrics.com/
.. _`UserVoice`: http://www.uservoice.com/ .. _`UserVoice`: http://www.uservoice.com/
.. _`Woopra`: http://www.woopra.com/ .. _`Woopra`: http://www.woopra.com/
.. _`Yandex.Metrica`: http://metrica.yandex.com
Documentation and Support Documentation
------------------------- -------------
The documentation can be found in the ``docs`` directory or `read The documentation can be found in the ``docs`` directory or `read
online`_. The source code and issue tracker are generously `hosted by online`_. The source code and issue tracker are generously `hosted by
GitHub`_. GitHub`_.
.. _`read online`: https://django-analytical.readthedocs.io/ .. _`read online`: http://packages.python.org/django-analytical/
.. _`hosted by GitHub`: https://github.com/jazzband/django-analytical .. _`hosted by GitHub`: http://github.com/jcassee/django-analytical
.. _`Gitter chat room`: https://gitter.im/jazzband/django-analytical
How To Contribute How To Contribute
----------------- -----------------
.. start contribute include
If you want to help out with the development of django-analytical, by If you want to help out with the development of django-analytical, by
posting detailed bug reports, proposing new features or other analytics posting detailed bug reports, proposing new features or other analytics
services to support, or suggesting documentation improvements, use the services to support, or suggesting documentation improvements, use the
`issue tracker`_. If you want to get your hands dirty, great! Clone `issue tracker`_. If you want to get your hands dirty, great! Clone
the repository, make changes and place a `pull request`_. Creating an the repository, make changes and place a `pull request`_. Please do
issue to discuss your plans is useful. create an issue to discuss your plans.
At the end, don't forget to add yourself to the `list of authors`_ and .. _`issue tracker`: http://github.com/jcassee/django-analytical/issues
update the `changelog`_ with a short description of your contribution. .. _`pull request`: https://github.com/jcassee/django-analytical/pulls
We want you to stand out from the crowd as an open source superstar! ✦
This is a `Jazzband`_ project. By contributing you agree to abide by the
`Contributor Code of Conduct`_ and follow the `guidelines`_.
.. _`issue tracker`: https://github.com/jazzband/django-analytical/issues
.. _`pull request`: https://github.com/jazzband/django-analytical/pulls
.. _`list of authors`: https://github.com/jazzband/django-analytical/blob/main/pyproject.toml
.. _`changelog`: https://github.com/jazzband/django-analytical/blob/main/CHANGELOG.rst
.. _`Jazzband`: https://jazzband.co
.. _`Contributor Code of Conduct`: https://jazzband.co/about/conduct
.. _`guidelines`: https://jazzband.co/about/guidelines
.. end contribute include

View file

@ -1,5 +1,15 @@
""" """
Analytics service integration for Django projects. Analytics service integration for Django
========================================
The django-analytical application integrates analytics services into a
Django_ project. See the ``docs`` directory for more information.
.. _Django: http://www.djangoproject.com/
""" """
__version__ = '3.2.0' __author__ = "Joost Cassee"
__email__ = "joost@cassee.net"
__version__ = "1.0.0"
__copyright__ = "Copyright (C) 2011-2015 Joost Cassee and others"
__license__ = "MIT License"

View file

@ -2,14 +2,16 @@
Analytical template tags and filters. Analytical template tags and filters.
""" """
from __future__ import absolute_import
import logging import logging
from importlib import import_module
from django import template from django import template
from django.template import Node, TemplateSyntaxError from django.template import Node, TemplateSyntaxError
from django.utils.importlib import import_module
from analytical.utils import AnalyticalException from analytical.utils import AnalyticalException
TAG_LOCATIONS = ['head_top', 'head_bottom', 'body_top', 'body_bottom'] TAG_LOCATIONS = ['head_top', 'head_bottom', 'body_top', 'body_bottom']
TAG_POSITIONS = ['first', None, 'last'] TAG_POSITIONS = ['first', None, 'last']
TAG_MODULES = [ TAG_MODULES = [
@ -17,30 +19,23 @@ TAG_MODULES = [
'analytical.clickmap', 'analytical.clickmap',
'analytical.clicky', 'analytical.clicky',
'analytical.crazy_egg', 'analytical.crazy_egg',
'analytical.facebook_pixel',
'analytical.gauges', 'analytical.gauges',
'analytical.google_analytics', 'analytical.google_analytics',
'analytical.google_analytics_js',
'analytical.google_analytics_gtag',
'analytical.gosquared', 'analytical.gosquared',
'analytical.heap',
'analytical.hotjar',
'analytical.hubspot', 'analytical.hubspot',
'analytical.intercom', 'analytical.intercom',
'analytical.kiss_insights', 'analytical.kiss_insights',
'analytical.kiss_metrics', 'analytical.kiss_metrics',
'analytical.luckyorange',
'analytical.matomo',
'analytical.mixpanel', 'analytical.mixpanel',
'analytical.olark', 'analytical.olark',
'analytical.optimizely', 'analytical.optimizely',
'analytical.performable', 'analytical.performable',
'analytical.rating_mailru', 'analytical.piwik',
'analytical.reinvigorate',
'analytical.snapengage', 'analytical.snapengage',
'analytical.spring_metrics', 'analytical.spring_metrics',
'analytical.uservoice', 'analytical.uservoice',
'analytical.woopra', 'analytical.woopra',
'analytical.yandex_metrica',
] ]
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -66,11 +61,12 @@ class AnalyticalNode(Node):
self.nodes = [node_cls() for node_cls in template_nodes[location]] self.nodes = [node_cls() for node_cls in template_nodes[location]]
def render(self, context): def render(self, context):
return ''.join([node.render(context) for node in self.nodes]) return "".join([node.render(context) for node in self.nodes])
def _load_template_nodes(): def _load_template_nodes():
template_nodes = {loc: {pos: [] for pos in TAG_POSITIONS} for loc in TAG_LOCATIONS} template_nodes = dict((l, dict((p, []) for p in TAG_POSITIONS))
for l in TAG_LOCATIONS)
def add_node_cls(location, node, position=None): def add_node_cls(location, node, position=None):
template_nodes[location][position].append(node) template_nodes[location][position].append(node)
@ -82,15 +78,14 @@ def _load_template_nodes():
except AnalyticalException as e: except AnalyticalException as e:
logger.debug("not loading tags from '%s': %s", path, e) logger.debug("not loading tags from '%s': %s", path, e)
for location in TAG_LOCATIONS: for location in TAG_LOCATIONS:
template_nodes[location] = sum( template_nodes[location] = sum((template_nodes[location][p]
(template_nodes[location][p] for p in TAG_POSITIONS), [] for p in TAG_POSITIONS), [])
)
return template_nodes return template_nodes
def _import_tag_module(path): def _import_tag_module(path):
app_name, lib_name = path.rsplit('.', 1) app_name, lib_name = path.rsplit('.', 1)
return import_module('%s.templatetags.%s' % (app_name, lib_name)) return import_module("%s.templatetags.%s" % (app_name, lib_name))
template_nodes = _load_template_nodes() template_nodes = _load_template_nodes()

View file

@ -2,19 +2,23 @@
Chartbeat template tags and filters. Chartbeat template tags and filters.
""" """
from __future__ import absolute_import
import json import json
import re import re
from django.conf import settings from django.conf import settings
from django.contrib.sites.models import Site
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.template import Library, Node, TemplateSyntaxError from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import disable_html, get_required_setting, is_internal_ip from analytical.utils import is_internal_ip, disable_html, get_required_setting
USER_ID_RE = re.compile(r'^\d+$') USER_ID_RE = re.compile(r'^\d+$')
INIT_CODE = """<script>var _sf_startpt=(new Date()).getTime()</script>""" INIT_CODE = """<script type="text/javascript">var _sf_startpt=(new Date()).getTime()</script>"""
SETUP_CODE = """ SETUP_CODE = """
<script> <script type="text/javascript">
var _sf_async_config=%(config)s; var _sf_async_config=%(config)s;
(function(){ (function(){
function loadChartbeat() { function loadChartbeat() {
@ -32,7 +36,7 @@ SETUP_CODE = """
loadChartbeat : function() { oldonload(); loadChartbeat(); }; loadChartbeat : function() { oldonload(); loadChartbeat(); };
})(); })();
</script> </script>
""" # noqa """
DOMAIN_CONTEXT_KEY = 'chartbeat_domain' DOMAIN_CONTEXT_KEY = 'chartbeat_domain'
@ -44,18 +48,17 @@ def chartbeat_top(parser, token):
""" """
Top Chartbeat template tag. Top Chartbeat template tag.
Render the top JavaScript code for Chartbeat. Render the top Javascript code for Chartbeat.
""" """
bits = token.split_contents() bits = token.split_contents()
if len(bits) > 1: if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0]) raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return ChartbeatTopNode() return ChartbeatTopNode()
class ChartbeatTopNode(Node): class ChartbeatTopNode(Node):
def render(self, context): def render(self, context):
if is_internal_ip(context): if is_internal_ip(context):
return disable_html(INIT_CODE, 'Chartbeat') return disable_html(INIT_CODE, "Chartbeat")
return INIT_CODE return INIT_CODE
@ -64,7 +67,7 @@ def chartbeat_bottom(parser, token):
""" """
Bottom Chartbeat template tag. Bottom Chartbeat template tag.
Render the bottom JavaScript code for Chartbeat. You must supply Render the bottom Javascript code for Chartbeat. You must supply
your Chartbeat User ID (as a string) in the ``CHARTBEAT_USER_ID`` your Chartbeat User ID (as a string) in the ``CHARTBEAT_USER_ID``
setting. setting.
""" """
@ -73,12 +76,10 @@ def chartbeat_bottom(parser, token):
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0]) raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return ChartbeatBottomNode() return ChartbeatBottomNode()
class ChartbeatBottomNode(Node): class ChartbeatBottomNode(Node):
def __init__(self): def __init__(self):
self.user_id = get_required_setting( self.user_id = get_required_setting('CHARTBEAT_USER_ID', USER_ID_RE,
'CHARTBEAT_USER_ID', USER_ID_RE, 'must be (a string containing) a number' "must be (a string containing) a number")
)
def render(self, context): def render(self, context):
config = {'uid': self.user_id} config = {'uid': self.user_id}
@ -106,9 +107,7 @@ def _get_domain(context):
if 'django.contrib.sites' not in settings.INSTALLED_APPS: if 'django.contrib.sites' not in settings.INSTALLED_APPS:
return return
elif getattr(settings, 'CHARTBEAT_AUTO_DOMAIN', True): elif getattr(settings, 'CHARTBEAT_AUTO_DOMAIN', True):
from django.contrib.sites.models import Site
try: try:
return Site.objects.get_current().domain return Site.objects.get_current().domain
except (ImproperlyConfigured, Site.DoesNotExist): # pylint: disable=E1101 except (ImproperlyConfigured, Site.DoesNotExist): #pylint: disable=E1101
return return

View file

@ -2,15 +2,18 @@
Clickmap template tags and filters. Clickmap template tags and filters.
""" """
from __future__ import absolute_import
import re import re
from django.template import Library, Node, TemplateSyntaxError from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import disable_html, get_required_setting, is_internal_ip from analytical.utils import is_internal_ip, disable_html, get_required_setting
CLICKMAP_TRACKER_ID_RE = re.compile(r'^\w+$') CLICKMAP_TRACKER_ID_RE = re.compile(r'^\w+$')
TRACKING_CODE = """ TRACKING_CODE = """
<script> <script type="text/javascript">
var clickmapConfig = {tracker: '%(tracker_id)s', version:'2'}; var clickmapConfig = {tracker: '%(tracker_id)s', version:'2'};
window.clickmapAsyncInit = function(){ __clickmap.init(clickmapConfig); }; window.clickmapAsyncInit = function(){ __clickmap.init(clickmapConfig); };
(function() { var _cmf = document.createElement('script'); _cmf.async = true; (function() { var _cmf = document.createElement('script'); _cmf.async = true;
@ -30,7 +33,7 @@ def clickmap(parser, token):
""" """
Clickmap tracker template tag. Clickmap tracker template tag.
Renders JavaScript code to track page visits. You must supply Renders Javascript code to track page visits. You must supply
your clickmap tracker ID (as a string) in the ``CLICKMAP_TRACKER_ID`` your clickmap tracker ID (as a string) in the ``CLICKMAP_TRACKER_ID``
setting. setting.
""" """
@ -42,11 +45,9 @@ def clickmap(parser, token):
class ClickmapNode(Node): class ClickmapNode(Node):
def __init__(self): def __init__(self):
self.tracker_id = get_required_setting( self.tracker_id = get_required_setting('CLICKMAP_TRACKER_ID',
'CLICKMAP_TRACKER_ID', CLICKMAP_TRACKER_ID_RE,
CLICKMAP_TRACKER_ID_RE, "must be an alphanumeric string")
'must be an alphanumeric string',
)
def render(self, context): def render(self, context):
html = TRACKING_CODE % {'tracker_id': self.tracker_id} html = TRACKING_CODE % {'tracker_id': self.tracker_id}

View file

@ -2,21 +2,20 @@
Clicky template tags and filters. Clicky template tags and filters.
""" """
from __future__ import absolute_import
import json import json
import re import re
from django.template import Library, Node, TemplateSyntaxError from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import ( from analytical.utils import get_identity, is_internal_ip, disable_html, \
disable_html, get_required_setting
get_identity,
get_required_setting,
is_internal_ip,
)
SITE_ID_RE = re.compile(r'^\d+$') SITE_ID_RE = re.compile(r'^\d+$')
TRACKING_CODE = """ TRACKING_CODE = """
<script> <script type="text/javascript">
var clicky = { log: function(){ return; }, goal: function(){ return; }}; var clicky = { log: function(){ return; }, goal: function(){ return; }};
var clicky_site_ids = clicky_site_ids || []; var clicky_site_ids = clicky_site_ids || [];
clicky_site_ids.push(%(site_id)s); clicky_site_ids.push(%(site_id)s);
@ -30,7 +29,8 @@ TRACKING_CODE = """
})(); })();
</script> </script>
<noscript><p><img alt="Clicky" width="1" height="1" src="//in.getclicky.com/%(site_id)sns.gif" /></p></noscript> <noscript><p><img alt="Clicky" width="1" height="1" src="//in.getclicky.com/%(site_id)sns.gif" /></p></noscript>
""" # noqa """
register = Library() register = Library()
@ -40,7 +40,7 @@ def clicky(parser, token):
""" """
Clicky tracking template tag. Clicky tracking template tag.
Renders JavaScript code to track page visits. You must supply Renders Javascript code to track page visits. You must supply
your Clicky Site ID (as a string) in the ``CLICKY_SITE_ID`` your Clicky Site ID (as a string) in the ``CLICKY_SITE_ID``
setting. setting.
""" """
@ -49,12 +49,10 @@ def clicky(parser, token):
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0]) raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return ClickyNode() return ClickyNode()
class ClickyNode(Node): class ClickyNode(Node):
def __init__(self): def __init__(self):
self.site_id = get_required_setting( self.site_id = get_required_setting('CLICKY_SITE_ID', SITE_ID_RE,
'CLICKY_SITE_ID', SITE_ID_RE, 'must be a (string containing) a number' "must be a (string containing) a number")
)
def render(self, context): def render(self, context):
custom = {} custom = {}
@ -67,10 +65,8 @@ class ClickyNode(Node):
if identity is not None: if identity is not None:
custom.setdefault('session', {})['username'] = identity custom.setdefault('session', {})['username'] = identity
html = TRACKING_CODE % { html = TRACKING_CODE % {'site_id': self.site_id,
'site_id': self.site_id, 'custom': json.dumps(custom, sort_keys=True)}
'custom': json.dumps(custom, sort_keys=True),
}
if is_internal_ip(context, 'CLICKY'): if is_internal_ip(context, 'CLICKY'):
html = disable_html(html, 'Clicky') html = disable_html(html, 'Clicky')
return html return html

View file

@ -2,17 +2,17 @@
Crazy Egg template tags and filters. Crazy Egg template tags and filters.
""" """
from __future__ import absolute_import
import re import re
from django.template import Library, Node, TemplateSyntaxError from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import disable_html, get_required_setting, is_internal_ip from analytical.utils import is_internal_ip, disable_html, get_required_setting
ACCOUNT_NUMBER_RE = re.compile(r'^\d+$') ACCOUNT_NUMBER_RE = re.compile(r'^\d+$')
SETUP_CODE = '<script src="{placeholder_url}"></script>'.format( SETUP_CODE = """<script type="text/javascript" src="//dnn506yrbagrg.cloudfront.net/pages/scripts/%(account_nr_1)s/%(account_nr_2)s.js"></script>"""
placeholder_url='//dnn506yrbagrg.cloudfront.net/pages/scripts/'
'%(account_nr_1)s/%(account_nr_2)s.js'
)
USERVAR_CODE = "CE2.set(%(varnr)d, '%(value)s');" USERVAR_CODE = "CE2.set(%(varnr)d, '%(value)s');"
@ -24,7 +24,7 @@ def crazy_egg(parser, token):
""" """
Crazy Egg tracking template tag. Crazy Egg tracking template tag.
Renders JavaScript code to track page clicks. You must supply Renders Javascript code to track page clicks. You must supply
your Crazy Egg account number (as a string) in the your Crazy Egg account number (as a string) in the
``CRAZY_EGG_ACCOUNT_NUMBER`` setting. ``CRAZY_EGG_ACCOUNT_NUMBER`` setting.
""" """
@ -33,32 +33,21 @@ def crazy_egg(parser, token):
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0]) raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return CrazyEggNode() return CrazyEggNode()
class CrazyEggNode(Node): class CrazyEggNode(Node):
def __init__(self): def __init__(self):
self.account_nr = get_required_setting( self.account_nr = get_required_setting('CRAZY_EGG_ACCOUNT_NUMBER',
'CRAZY_EGG_ACCOUNT_NUMBER', ACCOUNT_NUMBER_RE, "must be (a string containing) a number")
ACCOUNT_NUMBER_RE,
'must be (a string containing) a number',
)
def render(self, context): def render(self, context):
html = SETUP_CODE % { html = SETUP_CODE % {'account_nr_1': self.account_nr[:4],
'account_nr_1': self.account_nr[:4], 'account_nr_2': self.account_nr[4:]}
'account_nr_2': self.account_nr[4:],
}
values = (context.get('crazy_egg_var%d' % i) for i in range(1, 6)) values = (context.get('crazy_egg_var%d' % i) for i in range(1, 6))
params = [(i, v) for i, v in enumerate(values, 1) if v is not None] vars = [(i, v) for i, v in enumerate(values, 1) if v is not None]
if params: if vars:
js = ' '.join( js = " ".join(USERVAR_CODE % {'varnr': varnr, 'value': value}
USERVAR_CODE for (varnr, value) in vars)
% { html = '%s\n<script type="text/javascript">%s</script>' \
'varnr': varnr, % (html, js)
'value': value,
}
for (varnr, value) in params
)
html = '%s\n<script>%s</script>' % (html, js)
if is_internal_ip(context, 'CRAZY_EGG'): if is_internal_ip(context, 'CRAZY_EGG'):
html = disable_html(html, 'Crazy Egg') html = disable_html(html, 'Crazy Egg')
return html return html

View file

@ -1,96 +0,0 @@
"""
Facebook Pixel template tags and filters.
"""
import re
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import disable_html, get_required_setting, is_internal_ip
FACEBOOK_PIXEL_HEAD_CODE = """\
<script>
!function(f,b,e,v,n,t,s)
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];
s.parentNode.insertBefore(t,s)}(window, document,'script',
'https://connect.facebook.net/en_US/fbevents.js');
fbq('init', '%(FACEBOOK_PIXEL_ID)s');
fbq('track', 'PageView');
</script>
"""
FACEBOOK_PIXEL_BODY_CODE = """\
<noscript><img height="1" width="1" style="display:none"
src="https://www.facebook.com/tr?id=%(FACEBOOK_PIXEL_ID)s&ev=PageView&noscript=1"
/></noscript>
"""
register = Library()
def _validate_no_args(token):
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
@register.tag
def facebook_pixel_head(parser, token):
"""
Facebook Pixel head template tag.
"""
_validate_no_args(token)
return FacebookPixelHeadNode()
@register.tag
def facebook_pixel_body(parser, token):
"""
Facebook Pixel body template tag.
"""
_validate_no_args(token)
return FacebookPixelBodyNode()
class _FacebookPixelNode(Node):
"""
Base class: override and provide code_template.
"""
def __init__(self):
self.pixel_id = get_required_setting(
'FACEBOOK_PIXEL_ID',
re.compile(r'^\d+$'),
'must be (a string containing) a number',
)
def render(self, context):
html = self.code_template % {'FACEBOOK_PIXEL_ID': self.pixel_id}
if is_internal_ip(context, 'FACEBOOK_PIXEL'):
return disable_html(html, 'Facebook Pixel')
else:
return html
@property
def code_template(self):
raise NotImplementedError # pragma: no cover
class FacebookPixelHeadNode(_FacebookPixelNode):
code_template = FACEBOOK_PIXEL_HEAD_CODE
class FacebookPixelBodyNode(_FacebookPixelNode):
code_template = FACEBOOK_PIXEL_BODY_CODE
def contribute_to_analytical(add_node):
# ensure properly configured
FacebookPixelHeadNode()
FacebookPixelBodyNode()
add_node('head_bottom', FacebookPixelHeadNode)
add_node('body_bottom', FacebookPixelBodyNode)

View file

@ -2,15 +2,17 @@
Gaug.es template tags and filters. Gaug.es template tags and filters.
""" """
from __future__ import absolute_import
import re import re
from django.template import Library, Node, TemplateSyntaxError from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import disable_html, get_required_setting, is_internal_ip from analytical.utils import is_internal_ip, disable_html, get_required_setting
SITE_ID_RE = re.compile(r'[\da-f]+$') SITE_ID_RE = re.compile(r'[\da-f]+$')
TRACKING_CODE = """ TRACKING_CODE = """
<script> <script type="text/javascript">
var _gauges = _gauges || []; var _gauges = _gauges || [];
(function() { (function() {
var t = document.createElement('script'); var t = document.createElement('script');
@ -33,7 +35,7 @@ def gauges(parser, token):
""" """
Gaug.es template tag. Gaug.es template tag.
Renders JavaScript code to gaug.es testing. You must supply Renders Javascript code to gaug.es testing. You must supply
your Site ID account number in the ``GAUGES_SITE_ID`` your Site ID account number in the ``GAUGES_SITE_ID``
setting. setting.
""" """
@ -46,8 +48,8 @@ def gauges(parser, token):
class GaugesNode(Node): class GaugesNode(Node):
def __init__(self): def __init__(self):
self.site_id = get_required_setting( self.site_id = get_required_setting(
'GAUGES_SITE_ID', SITE_ID_RE, "must be a string looking like 'XXXXXXX'" 'GAUGES_SITE_ID', SITE_ID_RE,
) "must be a string looking like 'XXXXXXX'")
def render(self, context): def render(self, context):
html = TRACKING_CODE % {'site_id': self.site_id} html = TRACKING_CODE % {'site_id': self.site_id}

View file

@ -1,22 +1,24 @@
""" """
Google Analytics template tags and filters. Google Analytics template tags and filters.
DEPRECATED
""" """
import decimal from __future__ import absolute_import
import re import re
from django.conf import settings from django.conf import settings
from django.template import Library, Node, TemplateSyntaxError from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import ( from analytical.utils import is_internal_ip, disable_html, \
AnalyticalException, get_required_setting, get_domain, AnalyticalException
disable_html,
get_domain, def enumerate(sequence, start=0):
get_required_setting, """Copy of the Python 2.6 `enumerate` builtin for compatibility."""
is_internal_ip, n = start
) for elem in sequence:
yield n, elem
n += 1
TRACK_SINGLE_DOMAIN = 1 TRACK_SINGLE_DOMAIN = 1
TRACK_MULTIPLE_SUBDOMAINS = 2 TRACK_MULTIPLE_SUBDOMAINS = 2
@ -28,10 +30,11 @@ SCOPE_PAGE = 3
PROPERTY_ID_RE = re.compile(r'^UA-\d+-\d+$') PROPERTY_ID_RE = re.compile(r'^UA-\d+-\d+$')
SETUP_CODE = """ SETUP_CODE = """
<script> <script type="text/javascript">
var _gaq = _gaq || []; var _gaq = _gaq || [];
_gaq.push(['_setAccount', '%(property_id)s']); _gaq.push(['_setAccount', '%(property_id)s']);
_gaq.push(['_trackPageview']);
%(commands)s %(commands)s
(function() { (function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
@ -43,35 +46,22 @@ SETUP_CODE = """
""" """
DOMAIN_CODE = "_gaq.push(['_setDomainName', '%s']);" DOMAIN_CODE = "_gaq.push(['_setDomainName', '%s']);"
NO_ALLOW_HASH_CODE = "_gaq.push(['_setAllowHash', false]);" NO_ALLOW_HASH_CODE = "_gaq.push(['_setAllowHash', false]);"
TRACK_PAGE_VIEW = "_gaq.push(['_trackPageview']);"
ALLOW_LINKER_CODE = "_gaq.push(['_setAllowLinker', true]);" ALLOW_LINKER_CODE = "_gaq.push(['_setAllowLinker', true]);"
CUSTOM_VAR_CODE = ( CUSTOM_VAR_CODE = "_gaq.push(['_setCustomVar', %(index)s, '%(name)s', " \
"_gaq.push(['_setCustomVar', %(index)s, '%(name)s', '%(value)s', %(scope)s]);" "'%(value)s', %(scope)s]);"
)
SITE_SPEED_CODE = "_gaq.push(['_trackPageLoadTime']);" SITE_SPEED_CODE = "_gaq.push(['_trackPageLoadTime']);"
ANONYMIZE_IP_CODE = "_gaq.push(['_gat._anonymizeIp']);" ANONYMIZE_IP_CODE = "_gaq.push (['_gat._anonymizeIp']);"
SAMPLE_RATE_CODE = "_gaq.push(['_setSampleRate', '%s']);"
SITE_SPEED_SAMPLE_RATE_CODE = "_gaq.push(['_setSiteSpeedSampleRate', '%s']);"
SESSION_COOKIE_TIMEOUT_CODE = "_gaq.push(['_setSessionCookieTimeout', '%s']);"
VISITOR_COOKIE_TIMEOUT_CODE = "_gaq.push(['_setVisitorCookieTimeout', '%s']);"
DEFAULT_SOURCE = ("'https://ssl' : 'http://www'", "'.google-analytics.com/ga.js'") DEFAULT_SOURCE = ("'https://ssl' : 'http://www'", "'.google-analytics.com/ga.js'")
DISPLAY_ADVERTISING_SOURCE = ( DISPLAY_ADVERTISING_SOURCE = ("'https://' : 'http://'", "'stats.g.doubleclick.net/dc.js'")
"'https://' : 'http://'",
"'stats.g.doubleclick.net/dc.js'",
)
ZEROPLACES = decimal.Decimal('0')
TWOPLACES = decimal.Decimal('0.01')
register = Library() register = Library()
@register.tag @register.tag
def google_analytics(parser, token): def google_analytics(parser, token):
""" """
Google Analytics tracking template tag. Google Analytics tracking template tag.
Renders JavaScript code to track page visits. You must supply Renders Javascript code to track page visits. You must supply
your website property ID (as a string) in the your website property ID (as a string) in the
``GOOGLE_ANALYTICS_PROPERTY_ID`` setting. ``GOOGLE_ANALYTICS_PROPERTY_ID`` setting.
""" """
@ -80,47 +70,39 @@ def google_analytics(parser, token):
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0]) raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return GoogleAnalyticsNode() return GoogleAnalyticsNode()
class GoogleAnalyticsNode(Node): class GoogleAnalyticsNode(Node):
def __init__(self): def __init__(self):
self.property_id = get_required_setting( self.property_id = get_required_setting(
'GOOGLE_ANALYTICS_PROPERTY_ID', 'GOOGLE_ANALYTICS_PROPERTY_ID', PROPERTY_ID_RE,
PROPERTY_ID_RE, "must be a string looking like 'UA-XXXXXX-Y'")
"must be a string looking like 'UA-XXXXXX-Y'",
)
def render(self, context): def render(self, context):
commands = self._get_domain_commands(context) commands = self._get_domain_commands(context)
commands.extend(self._get_custom_var_commands(context)) commands.extend(self._get_custom_var_commands(context))
commands.extend(self._get_other_commands(context)) commands.extend(self._get_other_commands(context))
commands.append(TRACK_PAGE_VIEW)
if getattr(settings, 'GOOGLE_ANALYTICS_DISPLAY_ADVERTISING', False): if getattr(settings, 'GOOGLE_ANALYTICS_DISPLAY_ADVERTISING', False):
source = DISPLAY_ADVERTISING_SOURCE source = DISPLAY_ADVERTISING_SOURCE
else: else:
source = DEFAULT_SOURCE source = DEFAULT_SOURCE
html = SETUP_CODE % { html = SETUP_CODE % {'property_id': self.property_id,
'property_id': self.property_id, 'commands': " ".join(commands),
'commands': ' '.join(commands), 'source_scheme': source[0],
'source_scheme': source[0], 'source_url': source[1]}
'source_url': source[1],
}
if is_internal_ip(context, 'GOOGLE_ANALYTICS'): if is_internal_ip(context, 'GOOGLE_ANALYTICS'):
html = disable_html(html, 'Google Analytics') html = disable_html(html, 'Google Analytics')
return html return html
def _get_domain_commands(self, context): def _get_domain_commands(self, context):
commands = [] commands = []
tracking_type = getattr( tracking_type = getattr(settings, 'GOOGLE_ANALYTICS_TRACKING_STYLE',
settings, 'GOOGLE_ANALYTICS_TRACKING_STYLE', TRACK_SINGLE_DOMAIN TRACK_SINGLE_DOMAIN)
)
if tracking_type == TRACK_SINGLE_DOMAIN: if tracking_type == TRACK_SINGLE_DOMAIN:
pass pass
else: else:
domain = get_domain(context, 'google_analytics') domain = get_domain(context, 'google_analytics')
if domain is None: if domain is None:
raise AnalyticalException( raise AnalyticalException("tracking multiple domains with"
'tracking multiple domains with Google Analytics requires a domain name' " Google Analytics requires a domain name")
)
commands.append(DOMAIN_CODE % domain) commands.append(DOMAIN_CODE % domain)
commands.append(NO_ALLOW_HASH_CODE) commands.append(NO_ALLOW_HASH_CODE)
if tracking_type == TRACK_MULTIPLE_DOMAINS: if tracking_type == TRACK_MULTIPLE_DOMAINS:
@ -128,79 +110,28 @@ class GoogleAnalyticsNode(Node):
return commands return commands
def _get_custom_var_commands(self, context): def _get_custom_var_commands(self, context):
values = (context.get('google_analytics_var%s' % i) for i in range(1, 6)) values = (context.get('google_analytics_var%s' % i)
params = [(i, v) for i, v in enumerate(values, 1) if v is not None] for i in range(1, 6))
vars = [(i, v) for i, v in enumerate(values, 1) if v is not None]
commands = [] commands = []
for index, var in params: for index, var in vars:
name = var[0] name = var[0]
value = var[1] value = var[1]
try: try:
scope = var[2] scope = var[2]
except IndexError: except IndexError:
scope = SCOPE_PAGE scope = SCOPE_PAGE
commands.append( commands.append(CUSTOM_VAR_CODE % locals())
CUSTOM_VAR_CODE
% {
'index': index,
'name': name,
'value': value,
'scope': scope,
}
)
return commands return commands
def _get_other_commands(self, context): def _get_other_commands(self, context):
commands = [] commands = []
if getattr(settings, 'GOOGLE_ANALYTICS_SITE_SPEED', False): if getattr(settings, 'GOOGLE_ANALYTICS_SITE_SPEED', False):
commands.append(SITE_SPEED_CODE) commands.append(SITE_SPEED_CODE)
if getattr(settings, 'GOOGLE_ANALYTICS_ANONYMIZE_IP', False): if getattr(settings, 'GOOGLE_ANALYTICS_ANONYMIZE_IP', False):
commands.append(ANONYMIZE_IP_CODE) commands.append(ANONYMIZE_IP_CODE)
sampleRate = getattr(settings, 'GOOGLE_ANALYTICS_SAMPLE_RATE', False)
if sampleRate is not False:
value = decimal.Decimal(sampleRate)
if not 0 <= value <= 100:
raise AnalyticalException(
"'GOOGLE_ANALYTICS_SAMPLE_RATE' must be >= 0 and <= 100"
)
commands.append(SAMPLE_RATE_CODE % value.quantize(TWOPLACES))
siteSpeedSampleRate = getattr(
settings, 'GOOGLE_ANALYTICS_SITE_SPEED_SAMPLE_RATE', False
)
if siteSpeedSampleRate is not False:
value = decimal.Decimal(siteSpeedSampleRate)
if not 0 <= value <= 100:
raise AnalyticalException(
"'GOOGLE_ANALYTICS_SITE_SPEED_SAMPLE_RATE' must be >= 0 and <= 100"
)
commands.append(SITE_SPEED_SAMPLE_RATE_CODE % value.quantize(TWOPLACES))
sessionCookieTimeout = getattr(
settings, 'GOOGLE_ANALYTICS_SESSION_COOKIE_TIMEOUT', False
)
if sessionCookieTimeout is not False:
value = decimal.Decimal(sessionCookieTimeout)
if value < 0:
raise AnalyticalException(
"'GOOGLE_ANALYTICS_SESSION_COOKIE_TIMEOUT' must be >= 0"
)
commands.append(SESSION_COOKIE_TIMEOUT_CODE % value.quantize(ZEROPLACES))
visitorCookieTimeout = getattr(
settings, 'GOOGLE_ANALYTICS_VISITOR_COOKIE_TIMEOUT', False
)
if visitorCookieTimeout is not False:
value = decimal.Decimal(visitorCookieTimeout)
if value < 0:
raise AnalyticalException(
"'GOOGLE_ANALYTICS_VISITOR_COOKIE_TIMEOUT' must be >= 0"
)
commands.append(VISITOR_COOKIE_TIMEOUT_CODE % value.quantize(ZEROPLACES))
return commands return commands
def contribute_to_analytical(add_node): def contribute_to_analytical(add_node):
GoogleAnalyticsNode() # ensure properly configured GoogleAnalyticsNode() # ensure properly configured
add_node('head_bottom', GoogleAnalyticsNode) add_node('head_bottom', GoogleAnalyticsNode)

View file

@ -1,78 +0,0 @@
"""
Google Analytics template tags and filters, using the new gtag.js library.
https://developers.google.com/tag-platform/gtagjs/reference
"""
import json
import re
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import (
disable_html,
get_identity,
get_required_setting,
is_internal_ip,
)
PROPERTY_ID_RE = re.compile(
r'^UA-\d+-\d+$|^G-[a-zA-Z0-9]+$|^AW-[a-zA-Z0-9]+$|^DC-[a-zA-Z0-9]+$'
)
SETUP_CODE = """
<script async src="https://www.googletagmanager.com/gtag/js?id={property_id}"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){{dataLayer.push(arguments);}}
gtag('js', new Date());
gtag('config', '{property_id}', {custom_dimensions});
</script>
"""
register = Library()
@register.tag
def google_analytics_gtag(parser, token):
"""
Google Analytics tracking template tag.
Renders JavaScript code to track page visits. You must supply
your website property ID (as a string) in the
``GOOGLE_ANALYTICS_GTAG_PROPERTY_ID`` setting.
"""
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return GoogleAnalyticsGTagNode()
class GoogleAnalyticsGTagNode(Node):
def __init__(self):
self.property_id = get_required_setting(
'GOOGLE_ANALYTICS_GTAG_PROPERTY_ID',
PROPERTY_ID_RE,
"""must be a string looking like one of these patterns
('UA-XXXXXX-Y' , 'AW-XXXXXXXXXX',
'G-XXXXXXXX', 'DC-XXXXXXXX')""",
)
def render(self, context):
custom_dimensions = context.get('google_analytics_custom_dimensions', {})
identity = get_identity(context, prefix='google_analytics_gtag')
if identity is not None:
custom_dimensions['user_id'] = identity
html = SETUP_CODE.format(
property_id=self.property_id,
custom_dimensions=json.dumps(custom_dimensions),
)
if is_internal_ip(context, 'GOOGLE_ANALYTICS'):
html = disable_html(html, 'Google Analytics')
return html
def contribute_to_analytical(add_node):
GoogleAnalyticsGTagNode() # ensure properly configured
add_node('head_top', GoogleAnalyticsGTagNode)

View file

@ -1,176 +0,0 @@
"""
Google Analytics template tags and filters, using the new analytics.js library.
"""
import decimal
import re
from django.conf import settings
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import (
AnalyticalException,
disable_html,
get_domain,
get_required_setting,
is_internal_ip,
)
TRACK_SINGLE_DOMAIN = 1
TRACK_MULTIPLE_SUBDOMAINS = 2
TRACK_MULTIPLE_DOMAINS = 3
PROPERTY_ID_RE = re.compile(r'^UA-\d+-\d+$')
SETUP_CODE = """
<script>
(function(i,s,o,g,r,a,m){{i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){{
(i[r].q=i[r].q||[]).push(arguments)}},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
}})(window,document,'script','{js_source}','ga');
ga('create', '{property_id}', 'auto', {create_fields});
{commands}ga('send', 'pageview');
</script>
"""
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()
@register.tag
def google_analytics_js(parser, token):
"""
Google Analytics tracking template tag.
Renders JavaScript code to track page visits. You must supply
your website property ID (as a string) in the
``GOOGLE_ANALYTICS_JS_PROPERTY_ID`` setting.
"""
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return GoogleAnalyticsJsNode()
class GoogleAnalyticsJsNode(Node):
def __init__(self):
self.property_id = get_required_setting(
'GOOGLE_ANALYTICS_JS_PROPERTY_ID',
PROPERTY_ID_RE,
"must be a string looking like 'UA-XXXXXX-Y'",
)
def render(self, context):
import json
create_fields = self._get_domain_fields(context)
create_fields.update(self._get_other_create_fields(context))
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)
js_source = getattr(
settings,
'GOOGLE_ANALYTICS_JS_SOURCE',
'https://www.google-analytics.com/analytics.js',
)
html = SETUP_CODE.format(
property_id=self.property_id,
create_fields=json.dumps(create_fields),
commands=''.join(commands),
js_source=js_source,
)
if is_internal_ip(context, 'GOOGLE_ANALYTICS'):
html = disable_html(html, 'Google Analytics')
return html
def _get_domain_fields(self, context):
domain_fields = {}
tracking_type = getattr(
settings, 'GOOGLE_ANALYTICS_TRACKING_STYLE', TRACK_SINGLE_DOMAIN
)
if tracking_type == TRACK_SINGLE_DOMAIN:
pass
else:
domain = get_domain(context, 'google_analytics')
if domain is None:
raise AnalyticalException(
'tracking multiple domains with Google Analytics requires a domain name'
)
domain_fields['legacyCookieDomain'] = domain
if tracking_type == TRACK_MULTIPLE_DOMAINS:
domain_fields['allowLinker'] = True
return domain_fields
def _get_other_create_fields(self, context):
other_fields = {}
site_speed_sample_rate = getattr(
settings, 'GOOGLE_ANALYTICS_SITE_SPEED_SAMPLE_RATE', False
)
if site_speed_sample_rate is not False:
value = int(decimal.Decimal(site_speed_sample_rate))
if not 0 <= value <= 100:
raise AnalyticalException(
"'GOOGLE_ANALYTICS_SITE_SPEED_SAMPLE_RATE' must be >= 0 and <= 100"
)
other_fields['siteSpeedSampleRate'] = value
sample_rate = getattr(settings, 'GOOGLE_ANALYTICS_SAMPLE_RATE', False)
if sample_rate is not False:
value = int(decimal.Decimal(sample_rate))
if not 0 <= value <= 100:
raise AnalyticalException(
"'GOOGLE_ANALYTICS_SAMPLE_RATE' must be >= 0 and <= 100"
)
other_fields['sampleRate'] = value
cookie_expires = getattr(settings, 'GOOGLE_ANALYTICS_COOKIE_EXPIRATION', False)
if cookie_expires is not False:
value = int(decimal.Decimal(cookie_expires))
if value < 0:
raise AnalyticalException(
"'GOOGLE_ANALYTICS_COOKIE_EXPIRATION' must be >= 0"
)
other_fields['cookieExpires'] = value
return other_fields
def _get_custom_var_commands(self, context):
values = (context.get('google_analytics_var%s' % i) for i in range(1, 6))
params = [(i, v) for i, v in enumerate(values, 1) if v is not None]
commands = []
for _, var in params:
name = var[0]
value = var[1]
try:
float(value)
except ValueError:
value = f"'{value}'"
commands.append(
CUSTOM_VAR_CODE.format(
name=name,
value=value,
)
)
return commands
def _get_other_commands(self, context):
commands = []
if getattr(settings, 'GOOGLE_ANALYTICS_ANONYMIZE_IP', False):
commands.append(ANONYMIZE_IP_CODE)
return commands
def contribute_to_analytical(add_node):
GoogleAnalyticsJsNode() # ensure properly configured
add_node('head_bottom', GoogleAnalyticsJsNode)

View file

@ -2,20 +2,20 @@
GoSquared template tags and filters. GoSquared template tags and filters.
""" """
from __future__ import absolute_import
import re import re
from django.conf import settings
from django.template import Library, Node, TemplateSyntaxError from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import ( from analytical.utils import get_identity, get_user_from_context, \
disable_html, is_internal_ip, disable_html, get_required_setting
get_identity,
get_required_setting,
is_internal_ip,
)
TOKEN_RE = re.compile(r'^\S+-\S+-\S+$') TOKEN_RE = re.compile(r'^\S+-\S+-\S+$')
TRACKING_CODE = """ TRACKING_CODE = """
<script> <script type="text/javascript">
var GoSquared={}; var GoSquared={};
%(config)s %(config)s
(function(w){ (function(w){
@ -27,7 +27,7 @@ TRACKING_CODE = """
w.addEventListener?w.addEventListener("load",gs,false):w.attachEvent("onload",gs); w.addEventListener?w.addEventListener("load",gs,false):w.attachEvent("onload",gs);
})(window); })(window);
</script> </script>
""" # noqa """
TOKEN_CODE = 'GoSquared.acct = "%s";' TOKEN_CODE = 'GoSquared.acct = "%s";'
IDENTIFY_CODE = 'GoSquared.UserName = "%s";' IDENTIFY_CODE = 'GoSquared.UserName = "%s";'
@ -40,7 +40,7 @@ def gosquared(parser, token):
""" """
GoSquared tracking template tag. GoSquared tracking template tag.
Renders JavaScript code to track page visits. You must supply Renders Javascript code to track page visits. You must supply
your GoSquared site token in the ``GOSQUARED_SITE_TOKEN`` setting. your GoSquared site token in the ``GOSQUARED_SITE_TOKEN`` setting.
""" """
bits = token.split_contents() bits = token.split_contents()
@ -48,14 +48,10 @@ def gosquared(parser, token):
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0]) raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return GoSquaredNode() return GoSquaredNode()
class GoSquaredNode(Node): class GoSquaredNode(Node):
def __init__(self): def __init__(self):
self.site_token = get_required_setting( self.site_token = get_required_setting('GOSQUARED_SITE_TOKEN', TOKEN_RE,
'GOSQUARED_SITE_TOKEN', "must be a string looking like XXX-XXXXXX-X")
TOKEN_RE,
'must be a string looking like XXX-XXXXXX-X',
)
def render(self, context): def render(self, context):
configs = [TOKEN_CODE % self.site_token] configs = [TOKEN_CODE % self.site_token]

View file

@ -1,57 +0,0 @@
"""
Heap template tags and filters.
"""
import re
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import disable_html, get_required_setting, is_internal_ip
HEAP_TRACKER_ID_RE = re.compile(r'^\d+$')
TRACKING_CODE = """
<script>
window.heap=window.heap||[],heap.load=function(e,t){window.heap.appid=e,window.heap.config=t=t||{};var r=document.createElement("script");r.type="text/javascript",r.async=!0,r.src="https://cdn.heapanalytics.com/js/heap-"+e+".js";var a=document.getElementsByTagName("script")[0];a.parentNode.insertBefore(r,a);for(var n=function(e){return function(){heap.push([e].concat(Array.prototype.slice.call(arguments,0)))}},p=["addEventProperties","addUserProperties","clearEventProperties","identify","resetIdentity","removeEventProperty","setEventProperties","track","unsetEventProperty"],o=0;o<p.length;o++)heap[p[o]]=n(p[o])};
heap.load("%(tracker_id)s");
</script>
""" # noqa
register = Library()
def _validate_no_args(token):
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
@register.tag
def heap(parser, token):
"""
Heap tracker template tag.
Renders JavaScript code to track page visits. You must supply
your heap tracker ID (as a string) in the ``HEAP_TRACKER_ID``
setting.
"""
_validate_no_args(token)
return HeapNode()
class HeapNode(Node):
def __init__(self):
self.tracker_id = get_required_setting(
'HEAP_TRACKER_ID', HEAP_TRACKER_ID_RE, 'must be an numeric string'
)
def render(self, context):
html = TRACKING_CODE % {'tracker_id': self.tracker_id}
if is_internal_ip(context, 'HEAP'):
html = disable_html(html, 'Heap')
return html
def contribute_to_analytical(add_node):
HeapNode() # ensure properly configured
add_node('head_bottom', HeapNode)

View file

@ -1,62 +0,0 @@
"""
Hotjar template tags and filters.
"""
import re
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import disable_html, get_required_setting, is_internal_ip
HOTJAR_TRACKING_CODE = """\
<script>
(function(h,o,t,j,a,r){
h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)};
h._hjSettings={hjid:%(HOTJAR_SITE_ID)s,hjsv:6};
a=o.getElementsByTagName('head')[0];
r=o.createElement('script');r.async=1;
r.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv;
a.appendChild(r);
})(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv=');
</script>
"""
register = Library()
def _validate_no_args(token):
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
@register.tag
def hotjar(parser, token):
"""
Hotjar template tag.
"""
_validate_no_args(token)
return HotjarNode()
class HotjarNode(Node):
def __init__(self):
self.site_id = get_required_setting(
'HOTJAR_SITE_ID',
re.compile(r'^\d+$'),
'must be (a string containing) a number',
)
def render(self, context):
html = HOTJAR_TRACKING_CODE % {'HOTJAR_SITE_ID': self.site_id}
if is_internal_ip(context, 'HOTJAR'):
return disable_html(html, 'Hotjar')
else:
return html
def contribute_to_analytical(add_node):
# ensure properly configured
HotjarNode()
add_node('head_bottom', HotjarNode)

View file

@ -2,16 +2,19 @@
HubSpot template tags and filters. HubSpot template tags and filters.
""" """
from __future__ import absolute_import
import re import re
from django.template import Library, Node, TemplateSyntaxError from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import disable_html, get_required_setting, is_internal_ip from analytical.utils import is_internal_ip, disable_html, get_required_setting
PORTAL_ID_RE = re.compile(r'^\d+$') PORTAL_ID_RE = re.compile(r'^\d+$')
TRACKING_CODE = """ TRACKING_CODE = """
<!-- Start of Async HubSpot Analytics Code --> <!-- Start of Async HubSpot Analytics Code -->
<script> <script type="text/javascript">
(function(d,s,i,r) { (function(d,s,i,r) {
if (d.getElementById(i)){return;} if (d.getElementById(i)){return;}
var n=d.createElement(s),e=d.getElementsByTagName(s)[0]; var n=d.createElement(s),e=d.getElementsByTagName(s)[0];
@ -20,7 +23,7 @@ TRACKING_CODE = """
})(document,"script","hs-analytics",300000); })(document,"script","hs-analytics",300000);
</script> </script>
<!-- End of Async HubSpot Analytics Code --> <!-- End of Async HubSpot Analytics Code -->
""" # noqa """
register = Library() register = Library()
@ -30,7 +33,7 @@ def hubspot(parser, token):
""" """
HubSpot tracking template tag. HubSpot tracking template tag.
Renders JavaScript code to track page visits. You must supply Renders Javascript code to track page visits. You must supply
your portal ID (as a string) in the ``HUBSPOT_PORTAL_ID`` setting. your portal ID (as a string) in the ``HUBSPOT_PORTAL_ID`` setting.
""" """
bits = token.split_contents() bits = token.split_contents()
@ -41,9 +44,8 @@ def hubspot(parser, token):
class HubSpotNode(Node): class HubSpotNode(Node):
def __init__(self): def __init__(self):
self.portal_id = get_required_setting( self.portal_id = get_required_setting('HUBSPOT_PORTAL_ID',
'HUBSPOT_PORTAL_ID', PORTAL_ID_RE, 'must be a (string containing a) number' PORTAL_ID_RE, "must be a (string containing a) number")
)
def render(self, context): def render(self, context):
html = TRACKING_CODE % {'portal_id': self.portal_id} html = TRACKING_CODE % {'portal_id': self.portal_id}

View file

@ -2,68 +2,33 @@
intercom.io template tags and filters. intercom.io template tags and filters.
""" """
import hashlib from __future__ import absolute_import
import hmac
import json import json
import time
import re import re
from django.conf import settings
from django.template import Library, Node, TemplateSyntaxError from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import ( from analytical.utils import disable_html, get_required_setting, is_internal_ip,\
disable_html, get_user_from_context, get_identity
get_identity,
get_required_setting,
get_user_from_context,
get_user_is_authenticated,
is_internal_ip,
)
APP_ID_RE = re.compile(r'[\da-z]+$') APP_ID_RE = re.compile(r'[\da-f]+$')
TRACKING_CODE = """ TRACKING_CODE = """
<script id="IntercomSettingsScriptTag"> <script id="IntercomSettingsScriptTag">
window.intercomSettings = %(settings_json)s; window.intercomSettings = %(settings_json)s;
</script> </script>
<script>(function(){var w=window;var ic=w.Intercom;if(typeof ic==="function"){ic('reattach_activator');ic('update',intercomSettings);}else{var d=document;var i=function(){i.c(arguments)};i.q=[];i.c=function(args){i.q.push(args)};w.Intercom=i;function l(){var s=d.createElement('script');s.type='text/javascript';s.async=true;s.src='https://static.intercomcdn.com/intercom.v1.js';var x=d.getElementsByTagName('script')[0];x.parentNode.insertBefore(s,x);}if(w.attachEvent){w.attachEvent('onload',l);}else{w.addEventListener('load',l,false);}}})()</script> <script>(function(){var w=window;var ic=w.Intercom;if(typeof ic==="function"){ic('reattach_activator');ic('update',intercomSettings);}else{var d=document;var i=function(){i.c(arguments)};i.q=[];i.c=function(args){i.q.push(args)};w.Intercom=i;function l(){var s=d.createElement('script');s.type='text/javascript';s.async=true;s.src='https://static.intercomcdn.com/intercom.v1.js';var x=d.getElementsByTagName('script')[0];x.parentNode.insertBefore(s,x);}if(w.attachEvent){w.attachEvent('onload',l);}else{w.addEventListener('load',l,false);}}})()</script>
""" # noqa """
register = Library() register = Library()
def _hashable_bytes(data):
"""
Coerce strings to hashable bytes.
"""
if isinstance(data, bytes):
return data
elif isinstance(data, str):
return data.encode('ascii') # Fail on anything non-ASCII.
else:
raise TypeError(data)
def intercom_user_hash(data):
"""
Return a SHA-256 HMAC `user_hash` as expected by Intercom, if configured.
Return None if the `INTERCOM_HMAC_SECRET_KEY` setting is not configured.
"""
if getattr(settings, 'INTERCOM_HMAC_SECRET_KEY', None):
return hmac.new(
key=_hashable_bytes(settings.INTERCOM_HMAC_SECRET_KEY),
msg=_hashable_bytes(data),
digestmod=hashlib.sha256,
).hexdigest()
else:
return None
@register.tag @register.tag
def intercom(parser, token): def intercom(parser, token):
""" """
Intercom.io template tag. Intercom.io template tag.
Renders JavaScript code to intercom.io testing. You must supply Renders Javascript code to intercom.io testing. You must supply
your APP ID account number in the ``INTERCOM_APP_ID`` your APP ID account number in the ``INTERCOM_APP_ID``
setting. setting.
""" """
@ -76,8 +41,8 @@ def intercom(parser, token):
class IntercomNode(Node): class IntercomNode(Node):
def __init__(self): def __init__(self):
self.app_id = get_required_setting( self.app_id = get_required_setting(
'INTERCOM_APP_ID', APP_ID_RE, "must be a string looking like 'XXXXXXX'" 'INTERCOM_APP_ID', APP_ID_RE,
) "must be a string looking like 'XXXXXXX'")
def _identify(self, user): def _identify(self, user):
name = user.get_full_name() name = user.get_full_name()
@ -86,43 +51,34 @@ class IntercomNode(Node):
return name return name
def _get_custom_attrs(self, context): def _get_custom_attrs(self, context):
params = {} vars = {}
for dict_ in context: for dict_ in context:
for var, val in dict_.items(): for var, val in dict_.items():
if var.startswith('intercom_'): if var.startswith('intercom_'):
params[var[9:]] = val vars[var[9:]] = val
user = get_user_from_context(context) user = get_user_from_context(context)
if user is not None and get_user_is_authenticated(user): if user is not None and user.is_authenticated():
if 'name' not in params: if 'name' not in vars:
params['name'] = get_identity(context, 'intercom', self._identify, user) vars['name'] = get_identity(context, 'intercom', self._identify, user)
if 'email' not in params and user.email: if 'email' not in vars and user.email:
params['email'] = user.email vars['email'] = user.email
params.setdefault('user_id', user.pk) vars['created_at'] = int(time.mktime(user.date_joined.timetuple()))
params['created_at'] = int(user.date_joined.timestamp())
else: else:
params['created_at'] = None vars['created_at'] = None
# Generate a user_hash HMAC to verify the user's identity, if configured. return vars
# (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'))
if user_hash_data:
user_hash = intercom_user_hash(str(user_hash_data))
if user_hash is not None:
params.setdefault('user_hash', user_hash)
return params
def render(self, context): def render(self, context):
params = self._get_custom_attrs(context) html = ""
params['app_id'] = self.app_id user = get_user_from_context(context)
html = TRACKING_CODE % {'settings_json': json.dumps(params, sort_keys=True)} vars = self._get_custom_attrs(context)
vars["app_id"] = self.app_id
html = TRACKING_CODE % {"settings_json": json.dumps(vars, sort_keys=True)}
if is_internal_ip(context, 'INTERCOM'): if is_internal_ip(context, 'INTERCOM') or not user or not user.is_authenticated():
# Intercom is disabled for non-logged in users.
html = disable_html(html, 'Intercom') html = disable_html(html, 'Intercom')
return html return html

View file

@ -2,18 +2,21 @@
KISSinsights template tags. KISSinsights template tags.
""" """
from __future__ import absolute_import
import re import re
from django.template import Library, Node, TemplateSyntaxError from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import get_identity, get_required_setting from analytical.utils import get_identity, get_required_setting
ACCOUNT_NUMBER_RE = re.compile(r'^\d+$') ACCOUNT_NUMBER_RE = re.compile(r'^\d+$')
SITE_CODE_RE = re.compile(r'^[\w]+$') SITE_CODE_RE = re.compile(r'^[\w]+$')
SETUP_CODE = """ SETUP_CODE = """
<script>var _kiq = _kiq || []; %(commands)s</script> <script type="text/javascript">var _kiq = _kiq || []; %(commands)s</script>
<script src="//s3.amazonaws.com/ki.js/%(account_number)s/%(site_code)s.js" async="true"></script> <script type="text/javascript" src="//s3.amazonaws.com/ki.js/%(account_number)s/%(site_code)s.js" async="true"></script>
""" # noqa """
IDENTIFY_CODE = "_kiq.push(['identify', '%s']);" IDENTIFY_CODE = "_kiq.push(['identify', '%s']);"
SHOW_SURVEY_CODE = "_kiq.push(['showSurvey', %s]);" SHOW_SURVEY_CODE = "_kiq.push(['showSurvey', %s]);"
SHOW_SURVEY_CONTEXT_KEY = 'kiss_insights_show_survey' SHOW_SURVEY_CONTEXT_KEY = 'kiss_insights_show_survey'
@ -27,7 +30,7 @@ def kiss_insights(parser, token):
""" """
KISSinsights set-up template tag. KISSinsights set-up template tag.
Renders JavaScript code to set-up surveys. You must supply Renders Javascript code to set-up surveys. You must supply
your account number and site code in the your account number and site code in the
``KISS_INSIGHTS_ACCOUNT_NUMBER`` and ``KISS_INSIGHTS_SITE_CODE`` ``KISS_INSIGHTS_ACCOUNT_NUMBER`` and ``KISS_INSIGHTS_SITE_CODE``
settings. settings.
@ -37,19 +40,13 @@ def kiss_insights(parser, token):
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0]) raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return KissInsightsNode() return KissInsightsNode()
class KissInsightsNode(Node): class KissInsightsNode(Node):
def __init__(self): def __init__(self):
self.account_number = get_required_setting( self.account_number = get_required_setting(
'KISS_INSIGHTS_ACCOUNT_NUMBER', 'KISS_INSIGHTS_ACCOUNT_NUMBER', ACCOUNT_NUMBER_RE,
ACCOUNT_NUMBER_RE, "must be (a string containing) a number")
'must be (a string containing) a number', self.site_code = get_required_setting('KISS_INSIGHTS_SITE_CODE',
) SITE_CODE_RE, "must be a string containing three characters")
self.site_code = get_required_setting(
'KISS_INSIGHTS_SITE_CODE',
SITE_CODE_RE,
'must be a string containing three characters',
)
def render(self, context): def render(self, context):
commands = [] commands = []
@ -57,14 +54,12 @@ class KissInsightsNode(Node):
if identity is not None: if identity is not None:
commands.append(IDENTIFY_CODE % identity) commands.append(IDENTIFY_CODE % identity)
try: try:
commands.append(SHOW_SURVEY_CODE % context[SHOW_SURVEY_CONTEXT_KEY]) commands.append(SHOW_SURVEY_CODE
% context[SHOW_SURVEY_CONTEXT_KEY])
except KeyError: except KeyError:
pass pass
html = SETUP_CODE % { html = SETUP_CODE % {'account_number': self.account_number,
'account_number': self.account_number, 'site_code': self.site_code, 'commands': " ".join(commands)}
'site_code': self.site_code,
'commands': ' '.join(commands),
}
return html return html

View file

@ -2,21 +2,20 @@
KISSmetrics template tags. KISSmetrics template tags.
""" """
from __future__ import absolute_import
import json import json
import re import re
from django.template import Library, Node, TemplateSyntaxError from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import ( from analytical.utils import is_internal_ip, disable_html, get_identity, \
disable_html, get_required_setting
get_identity,
get_required_setting,
is_internal_ip,
)
API_KEY_RE = re.compile(r'^[0-9a-f]{40}$') API_KEY_RE = re.compile(r'^[0-9a-f]{40}$')
TRACKING_CODE = """ TRACKING_CODE = """
<script> <script type="text/javascript">
var _kmq = _kmq || []; var _kmq = _kmq || [];
%(commands)s %(commands)s
function _kms(u){ function _kms(u){
@ -50,7 +49,7 @@ def kiss_metrics(parser, token):
""" """
KISSinsights tracking template tag. KISSinsights tracking template tag.
Renders JavaScript code to track page visits. You must supply Renders Javascript code to track page visits. You must supply
your KISSmetrics API key in the ``KISS_METRICS_API_KEY`` your KISSmetrics API key in the ``KISS_METRICS_API_KEY``
setting. setting.
""" """
@ -59,14 +58,11 @@ def kiss_metrics(parser, token):
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0]) raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return KissMetricsNode() return KissMetricsNode()
class KissMetricsNode(Node): class KissMetricsNode(Node):
def __init__(self): def __init__(self):
self.api_key = get_required_setting( self.api_key = get_required_setting('KISS_METRICS_API_KEY',
'KISS_METRICS_API_KEY', API_KEY_RE,
API_KEY_RE, "must be a string containing a 40-digit hexadecimal number")
'must be a string containing a 40-digit hexadecimal number',
)
def render(self, context): def render(self, context):
commands = [] commands = []
@ -76,34 +72,23 @@ class KissMetricsNode(Node):
try: try:
properties = context[ALIAS_CONTEXT_KEY] properties = context[ALIAS_CONTEXT_KEY]
key, value = properties.popitem() key, value = properties.popitem()
commands.append(ALIAS_CODE % (key, value)) commands.append(ALIAS_CODE % (key,value))
except KeyError: except KeyError:
pass pass
try: try:
name, properties = context[EVENT_CONTEXT_KEY] name, properties = context[EVENT_CONTEXT_KEY]
commands.append( commands.append(EVENT_CODE % {'name': name,
EVENT_CODE 'properties': json.dumps(properties, sort_keys=True)})
% {
'name': name,
'properties': json.dumps(properties, sort_keys=True),
}
)
except KeyError: except KeyError:
pass pass
try: try:
properties = context[PROPERTY_CONTEXT_KEY] properties = context[PROPERTY_CONTEXT_KEY]
commands.append( commands.append(PROPERTY_CODE % {
PROPERTY_CODE 'properties': json.dumps(properties, sort_keys=True)})
% {
'properties': json.dumps(properties, sort_keys=True),
}
)
except KeyError: except KeyError:
pass pass
html = TRACKING_CODE % { html = TRACKING_CODE % {'api_key': self.api_key,
'api_key': self.api_key, 'commands': " ".join(commands)}
'commands': ' '.join(commands),
}
if is_internal_ip(context, 'KISS_METRICS'): if is_internal_ip(context, 'KISS_METRICS'):
html = disable_html(html, 'KISSmetrics') html = disable_html(html, 'KISSmetrics')
return html return html

View file

@ -1,60 +0,0 @@
"""
Lucky Orange template tags and filters.
"""
import re
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import disable_html, get_required_setting, is_internal_ip
LUCKYORANGE_TRACKING_CODE = """\
<script type='text/javascript'>
window.__lo_site_id = %(LUCKYORANGE_SITE_ID)s;
(function() {
var wa = document.createElement('script'); wa.type = 'text/javascript'; wa.async = true;
wa.src = 'https://d10lpsik1i8c69.cloudfront.net/w.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(wa, s);
})();
</script>
"""
register = Library()
def _validate_no_args(token):
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
@register.tag
def luckyorange(parser, token):
"""
Lucky Orange template tag.
"""
_validate_no_args(token)
return LuckyOrangeNode()
class LuckyOrangeNode(Node):
def __init__(self):
self.site_id = get_required_setting(
'LUCKYORANGE_SITE_ID',
re.compile(r'^\d+$'),
'must be (a string containing) a number',
)
def render(self, context):
html = LUCKYORANGE_TRACKING_CODE % {'LUCKYORANGE_SITE_ID': self.site_id}
if is_internal_ip(context, 'LUCKYORANGE'):
return disable_html(html, 'Lucky Orange')
else:
return html
def contribute_to_analytical(add_node):
# ensure properly configured
LuckyOrangeNode()
add_node('head_bottom', LuckyOrangeNode)

View file

@ -1,152 +0,0 @@
"""
Matomo template tags and filters.
"""
import re
from collections import namedtuple
from itertools import chain
from django.conf import settings
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import (
disable_html,
get_identity,
get_required_setting,
is_internal_ip,
)
# 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>
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/matomo.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']);"
GIVE_CONSENT_CLASS = 'matomo_give_consent'
REMOVE_CONSENT_CLASS = 'matomo_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'
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)
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, (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

@ -2,30 +2,28 @@
Mixpanel template tags and filters. Mixpanel template tags and filters.
""" """
from __future__ import absolute_import
import json import json
import re import re
from django.template import Library, Node, TemplateSyntaxError from django.template import Library, Node, TemplateSyntaxError
from django.utils.safestring import mark_safe
from analytical.utils import ( from analytical.utils import is_internal_ip, disable_html, get_identity, \
disable_html, get_required_setting
get_identity,
get_required_setting,
is_internal_ip,
)
MIXPANEL_API_TOKEN_RE = re.compile(r'^[0-9a-f]{32}$') MIXPANEL_API_TOKEN_RE = re.compile(r'^[0-9a-f]{32}$')
TRACKING_CODE = """ TRACKING_CODE = """
<script>(function(e,b){if(!b.__SV){var a,f,i,g;window.mixpanel=b;a=e.createElement("script");a.type="text/javascript";a.async=!0;a.src=("https:"===e.location.protocol?"https:":"http:")+'//cdn.mxpnl.com/libs/mixpanel-2.2.min.js';f=e.getElementsByTagName("script")[0];f.parentNode.insertBefore(a,f);b._i=[];b.init=function(a,e,d){function f(b,h){var a=h.split(".");2==a.length&&(b=b[a[0]],h=a[1]);b[h]=function(){b.push([h].concat(Array.prototype.slice.call(arguments,0)))}}var c=b;"undefined"!== <script type="text/javascript">(function(e,b){if(!b.__SV){var a,f,i,g;window.mixpanel=b;a=e.createElement("script");a.type="text/javascript";a.async=!0;a.src=("https:"===e.location.protocol?"https:":"http:")+'//cdn.mxpnl.com/libs/mixpanel-2.2.min.js';f=e.getElementsByTagName("script")[0];f.parentNode.insertBefore(a,f);b._i=[];b.init=function(a,e,d){function f(b,h){var a=h.split(".");2==a.length&&(b=b[a[0]],h=a[1]);b[h]=function(){b.push([h].concat(Array.prototype.slice.call(arguments,0)))}}var c=b;"undefined"!==
typeof d?c=b[d]=[]:d="mixpanel";c.people=c.people||[];c.toString=function(b){var a="mixpanel";"mixpanel"!==d&&(a+="."+d);b||(a+=" (stub)");return a};c.people.toString=function(){return c.toString(1)+".people (stub)"};i="disable track track_pageview track_links track_forms register register_once alias unregister identify name_tag set_config people.set people.increment people.append people.track_charge people.clear_charges people.delete_user".split(" ");for(g=0;g<i.length;g++)f(c,i[g]);b._i.push([a, typeof d?c=b[d]=[]:d="mixpanel";c.people=c.people||[];c.toString=function(b){var a="mixpanel";"mixpanel"!==d&&(a+="."+d);b||(a+=" (stub)");return a};c.people.toString=function(){return c.toString(1)+".people (stub)"};i="disable track track_pageview track_links track_forms register register_once alias unregister identify name_tag set_config people.set people.increment people.append people.track_charge people.clear_charges people.delete_user".split(" ");for(g=0;g<i.length;g++)f(c,i[g]);b._i.push([a,
e,d])};b.__SV=1.2}})(document,window.mixpanel||[]); e,d])};b.__SV=1.2}})(document,window.mixpanel||[]);
mixpanel.init('%(token)s'); mixpanel.init('%(token)s');
%(commands)s %(commands)s
</script> </script>
""" # noqa """
IDENTIFY_CODE = "mixpanel.identify('%s');" IDENTIFY_CODE = "mixpanel.identify('%s');"
IDENTIFY_PROPERTIES = 'mixpanel.people.set(%s);' IDENTIFY_PROPERTIES = "mixpanel.people.set(%s);"
EVENT_CODE = "mixpanel.track('%(name)s', %(properties)s);" EVENT_CODE = "mixpanel.track('%(name)s', %(properties)s);"
EVENT_CONTEXT_KEY = 'mixpanel_event' EVENT_CONTEXT_KEY = 'mixpanel_event'
@ -37,7 +35,7 @@ def mixpanel(parser, token):
""" """
Mixpanel tracking template tag. Mixpanel tracking template tag.
Renders JavaScript code to track page visits. You must supply Renders Javascript code to track page visits. You must supply
your Mixpanel token in the ``MIXPANEL_API_TOKEN`` setting. your Mixpanel token in the ``MIXPANEL_API_TOKEN`` setting.
""" """
bits = token.split_contents() bits = token.split_contents()
@ -48,43 +46,30 @@ def mixpanel(parser, token):
class MixpanelNode(Node): class MixpanelNode(Node):
def __init__(self): def __init__(self):
self._token = get_required_setting( self.token = get_required_setting(
'MIXPANEL_API_TOKEN', 'MIXPANEL_API_TOKEN', MIXPANEL_API_TOKEN_RE,
MIXPANEL_API_TOKEN_RE, "must be a string containing a 32-digit hexadecimal number")
'must be a string containing a 32-digit hexadecimal number',
)
def render(self, context): def render(self, context):
commands = [] commands = []
identity = get_identity(context, 'mixpanel') identity = get_identity(context, 'mixpanel')
if identity is not None: if identity is not None:
if isinstance(identity, dict): if isinstance(identity, dict):
commands.append( commands.append(IDENTIFY_CODE % identity.get('id', identity.get('username')))
IDENTIFY_CODE % identity.get('id', identity.get('username')) commands.append(IDENTIFY_PROPERTIES % json.dumps(identity, sort_keys=True))
)
commands.append(
IDENTIFY_PROPERTIES % json.dumps(identity, sort_keys=True)
)
else: else:
commands.append(IDENTIFY_CODE % identity) commands.append(IDENTIFY_CODE % identity)
try: try:
name, properties = context[EVENT_CONTEXT_KEY] name, properties = context[EVENT_CONTEXT_KEY]
commands.append( commands.append(EVENT_CODE % {'name': name,
EVENT_CODE 'properties': json.dumps(properties, sort_keys=True)})
% {
'name': name,
'properties': json.dumps(properties, sort_keys=True),
}
)
except KeyError: except KeyError:
pass pass
html = TRACKING_CODE % { html = TRACKING_CODE % {'token': self.token,
'token': self._token, 'commands': " ".join(commands)}
'commands': ' '.join(commands),
}
if is_internal_ip(context, 'MIXPANEL'): if is_internal_ip(context, 'MIXPANEL'):
html = disable_html(html, 'Mixpanel') html = disable_html(html, 'Mixpanel')
return mark_safe(html) return html
def contribute_to_analytical(add_node): def contribute_to_analytical(add_node):

View file

@ -2,6 +2,8 @@
Olark template tags. Olark template tags.
""" """
from __future__ import absolute_import
import json import json
import re import re
@ -9,45 +11,31 @@ from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import get_identity, get_required_setting from analytical.utils import get_identity, get_required_setting
SITE_ID_RE = re.compile(r'^\d+-\d+-\d+-\d+$') SITE_ID_RE = re.compile(r'^\d+-\d+-\d+-\d+$')
SETUP_CODE = """ SETUP_CODE = """
<script type='text/javascript'> <script type='text/javascript'>
/*{literal}<![CDATA[*/ window.olark||(function(k){var g=window,j=document,a=g.location.protocol=="https:"?"https:":"http:",i=k.name,b="load",h="addEventListener";(function(){g[i]=function(){(c.s=c.s||[]).push(arguments)};var c=g[i]._={},f=k.methods.length;while(f--){(function(l){g[i][l]=function(){g[i]("call",l,arguments)}})(k.methods[f])}c.l=k.loader;c.i=arguments.callee;c.p={0:+new Date};c.P=function(l){c.p[l]=new Date-c.p[0]};function e(){c.P(b);g[i](b)}g[h]?g[h](b,e,false):g.attachEvent("on"+b,e);c.P(1);var d=j.createElement("script"),m=document.getElementsByTagName("script")[0];d.type="text/javascript";d.async=true;d.src=a+"//"+c.l;m.parentNode.insertBefore(d,m);c.P(2)})()})({loader:(function(a){return "static.olark.com/jsclient/loader1.js?ts="+(a?a[1]:(+new Date))})(document.cookie.match(/olarkld=([0-9]+)/)),name:"olark",methods:["configure","extend","declare","identify"]}); olark.identify('%(site_id)s');/*]]>{/literal}*/ /*{literal}<![CDATA[*/ window.olark||(function(k){var g=window,j=document,a=g.location.protocol=="https:"?"https:":"http:",i=k.name,b="load",h="addEventListener";(function(){g[i]=function(){(c.s=c.s||[]).push(arguments)};var c=g[i]._={},f=k.methods.length;while(f--){(function(l){g[i][l]=function(){g[i]("call",l,arguments)}})(k.methods[f])}c.l=k.loader;c.i=arguments.callee;c.p={0:+new Date};c.P=function(l){c.p[l]=new Date-c.p[0]};function e(){c.P(b);g[i](b)}g[h]?g[h](b,e,false):g.attachEvent("on"+b,e);c.P(1);var d=j.createElement("script"),m=document.getElementsByTagName("script")[0];d.type="text/javascript";d.async=true;d.src=a+"//"+c.l;m.parentNode.insertBefore(d,m);c.P(2)})()})({loader:(function(a){return "static.olark.com/jsclient/loader1.js?ts="+(a?a[1]:(+new Date))})(document.cookie.match(/olarkld=([0-9]+)/)),name:"olark",methods:["configure","extend","declare","identify"]}); olark.identify('%(site_id)s');/*]]>{/literal}*/
%(extra_code)s %(extra_code)s
</script> </script>
""" # noqa """
NICKNAME_CODE = "olark('api.chat.updateVisitorNickname', {snippet: '%s'});" NICKNAME_CODE = "olark('api.chat.updateVisitorNickname', {snippet: '%s'});"
NICKNAME_CONTEXT_KEY = 'olark_nickname' NICKNAME_CONTEXT_KEY = 'olark_nickname'
FULLNAME_CODE = "olark('api.visitor.updateFullName', {{fullName: '{0}'}});" FULLNAME_CODE = u"olark('api.visitor.updateFullName', {{fullName: '{0}'}});"
FULLNAME_CONTEXT_KEY = 'olark_fullname' FULLNAME_CONTEXT_KEY = 'olark_fullname'
EMAIL_CODE = "olark('api.visitor.updateEmailAddress', {{emailAddress: '{0}'}});" EMAIL_CODE = "olark('api.visitor.updateEmailAddress', {{emailAddress: '{0}'}});"
EMAIL_CONTEXT_KEY = 'olark_email' EMAIL_CONTEXT_KEY = 'olark_email'
STATUS_CODE = "olark('api.chat.updateVisitorStatus', {snippet: %s});" STATUS_CODE = "olark('api.chat.updateVisitorStatus', {snippet: %s});"
STATUS_CONTEXT_KEY = 'olark_status' STATUS_CONTEXT_KEY = 'olark_status'
MESSAGE_CODE = 'olark.configure(\'locale.%(key)s\', "%(msg)s");' MESSAGE_CODE = "olark.configure('locale.%(key)s', \"%(msg)s\");"
MESSAGE_KEYS = { MESSAGE_KEYS = set(["welcome_title", "chatting_title", "unavailable_title",
'welcome_title', "busy_title", "away_message", "loading_title", "welcome_message",
'chatting_title', "busy_message", "chat_input_text", "name_input_text",
'unavailable_title', "email_input_text", "offline_note_message", "send_button_text",
'busy_title', "offline_note_thankyou_text", "offline_note_error_text",
'away_message', "offline_note_sending_text", "operator_is_typing_text",
'loading_title', "operator_has_stopped_typing_text", "introduction_error_text",
'welcome_message', "introduction_messages", "introduction_submit_button_text"])
'busy_message',
'chat_input_text',
'name_input_text',
'email_input_text',
'offline_note_message',
'send_button_text',
'offline_note_thankyou_text',
'offline_note_error_text',
'offline_note_sending_text',
'operator_is_typing_text',
'operator_has_stopped_typing_text',
'introduction_error_text',
'introduction_messages',
'introduction_submit_button_text',
}
register = Library() register = Library()
@ -57,7 +45,7 @@ def olark(parser, token):
""" """
Olark set-up template tag. Olark set-up template tag.
Renders JavaScript code to set-up Olark chat. You must supply Renders Javascript code to set-up Olark chat. You must supply
your site ID in the ``OLARK_SITE_ID`` setting. your site ID in the ``OLARK_SITE_ID`` setting.
""" """
bits = token.split_contents() bits = token.split_contents()
@ -65,14 +53,10 @@ def olark(parser, token):
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0]) raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return OlarkNode() return OlarkNode()
class OlarkNode(Node): class OlarkNode(Node):
def __init__(self): def __init__(self):
self.site_id = get_required_setting( self.site_id = get_required_setting('OLARK_SITE_ID', SITE_ID_RE,
'OLARK_SITE_ID', "must be a string looking like 'XXXX-XXX-XX-XXXX'")
SITE_ID_RE,
"must be a string looking like 'XXXX-XXX-XX-XXXX'",
)
def render(self, context): def render(self, context):
extra_code = [] extra_code = []
@ -91,22 +75,19 @@ class OlarkNode(Node):
except KeyError: except KeyError:
pass pass
try: try:
extra_code.append( extra_code.append(STATUS_CODE %
STATUS_CODE % json.dumps(context[STATUS_CONTEXT_KEY], sort_keys=True) json.dumps(context[STATUS_CONTEXT_KEY], sort_keys=True))
)
except KeyError: except KeyError:
pass pass
extra_code.extend(self._get_configuration(context)) extra_code.extend(self._get_configuration(context))
html = SETUP_CODE % { html = SETUP_CODE % {'site_id': self.site_id,
'site_id': self.site_id, 'extra_code': " ".join(extra_code)}
'extra_code': ' '.join(extra_code),
}
return html return html
def _get_nickname(self, user): def _get_nickname(self, user):
name = user.get_full_name() name = user.get_full_name()
if name: if name:
return '%s (%s)' % (name, user.username) return "%s (%s)" % (name, user.username)
else: else:
return user.username return user.username

View file

@ -2,11 +2,14 @@
Optimizely template tags and filters. Optimizely template tags and filters.
""" """
from __future__ import absolute_import
import re import re
from django.template import Library, Node, TemplateSyntaxError from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import disable_html, get_required_setting, is_internal_ip from analytical.utils import is_internal_ip, disable_html, get_required_setting
ACCOUNT_NUMBER_RE = re.compile(r'^\d+$') ACCOUNT_NUMBER_RE = re.compile(r'^\d+$')
SETUP_CODE = """<script src="//cdn.optimizely.com/js/%(account_number)s.js"></script>""" SETUP_CODE = """<script src="//cdn.optimizely.com/js/%(account_number)s.js"></script>"""
@ -20,7 +23,7 @@ def optimizely(parser, token):
""" """
Optimizely template tag. Optimizely template tag.
Renders JavaScript code to set-up A/B testing. You must supply Renders Javascript code to set-up A/B testing. You must supply
your Optimizely account number in the ``OPTIMIZELY_ACCOUNT_NUMBER`` your Optimizely account number in the ``OPTIMIZELY_ACCOUNT_NUMBER``
setting. setting.
""" """
@ -33,10 +36,8 @@ def optimizely(parser, token):
class OptimizelyNode(Node): class OptimizelyNode(Node):
def __init__(self): def __init__(self):
self.account_number = get_required_setting( self.account_number = get_required_setting(
'OPTIMIZELY_ACCOUNT_NUMBER', 'OPTIMIZELY_ACCOUNT_NUMBER', ACCOUNT_NUMBER_RE,
ACCOUNT_NUMBER_RE, "must be a string looking like 'XXXXXXX'")
"must be a string looking like 'XXXXXXX'",
)
def render(self, context): def render(self, context):
html = SETUP_CODE % {'account_number': self.account_number} html = SETUP_CODE % {'account_number': self.account_number}

View file

@ -2,38 +2,36 @@
Performable template tags and filters. Performable template tags and filters.
""" """
from __future__ import absolute_import
import re import re
from django.template import Library, Node, TemplateSyntaxError from django.template import Library, Node, TemplateSyntaxError
from django.utils.safestring import mark_safe
from analytical.utils import ( from analytical.utils import is_internal_ip, disable_html, get_identity, \
disable_html, get_required_setting
get_identity,
get_required_setting,
is_internal_ip,
)
API_KEY_RE = re.compile(r'^\w+$') API_KEY_RE = re.compile(r'^\w+$')
SETUP_CODE = """ SETUP_CODE = """
<script src="//d1nu2rn22elx8m.cloudfront.net/performable/pax/%(api_key)s.js"></script> <script src="//d1nu2rn22elx8m.cloudfront.net/performable/pax/%(api_key)s.js" type="text/javascript"></script>
""" # noqa """
IDENTIFY_CODE = """ IDENTIFY_CODE = """
<script> <script type="text/javascript">
var _paq = _paq || []; var _paq = _paq || [];
_paq.push(["identify", {identity: "%s"}]); _paq.push(["identify", {identity: "%s"}]);
</script> </script>
""" """
EMBED_CODE = """ EMBED_CODE = """
<script src="//d1nu2rn22elx8m.cloudfront.net/performable/embed/page.js"></script> <script type="text/javascript" src="//d1nu2rn22elx8m.cloudfront.net/performable/embed/page.js"></script>
<script> <script type="text/javascript">
(function() { (function() {
var $f = new PerformableEmbed(); var $f = new PerformableEmbed();
$f.initialize({'host': '%(hostname)s', 'page': '%(page_id)s'}); $f.initialize({'host': '%(hostname)s', 'page': '%(page_id)s'});
$f.write(); $f.write();
})() })()
</script> </script>
""" # noqa """
register = Library() register = Library()
@ -43,7 +41,7 @@ def performable(parser, token):
""" """
Performable template tag. Performable template tag.
Renders JavaScript code to set-up Performable tracking. You must Renders Javascript code to set-up Performable tracking. You must
supply your Performable API key in the ``PERFORMABLE_API_KEY`` supply your Performable API key in the ``PERFORMABLE_API_KEY``
setting. setting.
""" """
@ -52,18 +50,16 @@ def performable(parser, token):
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0]) raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return PerformableNode() return PerformableNode()
class PerformableNode(Node): class PerformableNode(Node):
def __init__(self): def __init__(self):
self.api_key = get_required_setting( self.api_key = get_required_setting('PERFORMABLE_API_KEY', API_KEY_RE,
'PERFORMABLE_API_KEY', API_KEY_RE, "must be a string looking like 'XXXXX'" "must be a string looking like 'XXXXX'")
)
def render(self, context): def render(self, context):
html = SETUP_CODE % {'api_key': self.api_key} html = SETUP_CODE % {'api_key': self.api_key}
identity = get_identity(context, 'performable') identity = get_identity(context, 'performable')
if identity is not None: if identity is not None:
html = '%s%s' % (IDENTIFY_CODE % identity, html) html = "%s%s" % (IDENTIFY_CODE % identity, html)
if is_internal_ip(context, 'PERFORMABLE'): if is_internal_ip(context, 'PERFORMABLE'):
html = disable_html(html, 'Performable') html = disable_html(html, 'Performable')
return html return html
@ -74,13 +70,7 @@ def performable_embed(hostname, page_id):
""" """
Include a Performable landing page. Include a Performable landing page.
""" """
return mark_safe( return EMBED_CODE % {'hostname': hostname, 'page_id': page_id}
EMBED_CODE
% {
'hostname': hostname,
'page_id': page_id,
}
)
def contribute_to_analytical(add_node): def contribute_to_analytical(add_node):

View file

@ -0,0 +1,110 @@
"""
Piwik template tags and filters.
"""
from __future__ import absolute_import
from collections import namedtuple
from itertools import chain
import re
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 URI path, no slash
DOMAINPATH_RE = re.compile(r'^(([^./?#@:]+\.)+[^./?#@:]+)+(/[^/?#@:]+)*$')
# numeric ID
SITEID_RE = re.compile(r'^\d+$')
TRACKING_CODE = """
<script type="text/javascript">
var _paq = _paq || [];
%(variables)s
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u=(("https:" == document.location.protocol) ? "https" : "http") + "://%(url)s/";
_paq.push(['setTrackerUrl', u+'piwik.php']);
_paq.push(['setSiteId', %(siteid)s]);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0]; g.type='text/javascript';
g.defer=true; g.async=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s);
})();
</script>
<noscript><p><img src="http://%(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"]);'
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)
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)
}
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)

View file

@ -1,67 +0,0 @@
"""
Rating@Mail.ru template tags and filters.
"""
import re
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import disable_html, get_required_setting, is_internal_ip
COUNTER_ID_RE = re.compile(r'^\d{7}$')
COUNTER_CODE = """
<script>
var _tmr = window._tmr || (window._tmr = []);
_tmr.push({id: "%(counter_id)s", type: "pageView", start: (new Date()).getTime()});
(function (d, w, id) {
if (d.getElementById(id)) return;
var ts = d.createElement("script"); ts.type = "text/javascript"; ts.async = true; ts.id = id;
ts.src = (d.location.protocol == "https:" ? "https:" : "http:") + "//top-fwz1.mail.ru/js/code.js";
var f = function () {var s = d.getElementsByTagName("script")[0]; s.parentNode.insertBefore(ts, s);};
if (w.opera == "[object Opera]") { d.addEventListener("DOMContentLoaded", f, false); } else { f(); }
})(document, window, "topmailru-code");
</script>
<noscript><div style="position:absolute;left:-10000px;">
<img src="//top-fwz1.mail.ru/counter?id=%(counter_id)s;js=na" style="border:0;" height="1" width="1" alt="Rating@Mail.ru" />
</div></noscript>
""" # noqa
register = Library()
@register.tag
def rating_mailru(parser, token):
"""
Rating@Mail.ru counter template tag.
Renders JavaScript code to track page visits. You must supply
your website counter ID (as a string) in the
``RATING_MAILRU_COUNTER_ID`` setting.
"""
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return RatingMailruNode()
class RatingMailruNode(Node):
def __init__(self):
self.counter_id = get_required_setting(
'RATING_MAILRU_COUNTER_ID',
COUNTER_ID_RE,
"must be (a string containing) a number'",
)
def render(self, context):
html = COUNTER_CODE % {
'counter_id': self.counter_id,
}
if is_internal_ip(context, 'RATING_MAILRU_METRICA'):
html = disable_html(html, 'Rating@Mail.ru')
return html
def contribute_to_analytical(add_node):
RatingMailruNode() # ensure properly configured
add_node('head_bottom', RatingMailruNode)

View file

@ -0,0 +1,80 @@
"""
Reinvigorate template tags and filters.
"""
from __future__ import absolute_import
import json
import re
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import get_identity, is_internal_ip, disable_html, \
get_required_setting
TRACKING_ID_RE = re.compile(r'^[\w\d]+-[\w\d]+$')
TRACKING_CODE = """
<script type="text/javascript">
document.write(unescape("%%3Cscript src='" + (("https:" == document.location.protocol) ? "https://ssl-" : "http://") + "include.reinvigorate.net/re_.js' type='text/javascript'%%3E%%3C/script%%3E"));
</script>
<script type="text/javascript">
try {
%(tags)s
reinvigorate.track("%(tracking_id)s");
} catch(err) {}
</script>
"""
register = Library()
@register.tag
def reinvigorate(parser, token):
"""
Reinvigorate tracking template tag.
Renders Javascript code to track page visits. You must supply
your Reinvigorate tracking ID (as a string) in the
``REINVIGORATE_TRACKING_ID`` setting.
"""
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return ReinvigorateNode()
class ReinvigorateNode(Node):
def __init__(self):
self.tracking_id = get_required_setting('REINVIGORATE_TRACKING_ID',
TRACKING_ID_RE,
"must be a string looking like XXXXX-XXXXXXXXXX")
def render(self, context):
re_vars = {}
for dict_ in context:
for var, val in dict_.items():
if var.startswith('reinvigorate_'):
re_vars[var[13:]] = val
if 'name' not in re_vars:
identity = get_identity(context, 'reinvigorate',
lambda u: u.get_full_name())
if identity is not None:
re_vars['name'] = identity
if 'context' not in re_vars:
email = get_identity(context, 'reinvigorate', lambda u: u.email)
if email is not None:
re_vars['context'] = email
tags = " ".join("var re_%s_tag = %s;" % (tag, json.dumps(value, sort_keys=True))
for tag, value in re_vars.items())
html = TRACKING_CODE % {'tracking_id': self.tracking_id,
'tags': tags}
if is_internal_ip(context, 'REINVIGORATE'):
html = disable_html(html, 'Reinvigorate')
return html
def contribute_to_analytical(add_node):
ReinvigorateNode() # ensure properly configured
add_node('body_bottom', ReinvigorateNode)

View file

@ -2,6 +2,8 @@
SnapEngage template tags. SnapEngage template tags.
""" """
from __future__ import absolute_import
import re import re
from django.conf import settings from django.conf import settings
@ -10,6 +12,7 @@ from django.utils import translation
from analytical.utils import get_identity, get_required_setting from analytical.utils import get_identity, get_required_setting
BUTTON_LOCATION_LEFT = 0 BUTTON_LOCATION_LEFT = 0
BUTTON_LOCATION_RIGHT = 1 BUTTON_LOCATION_RIGHT = 1
BUTTON_LOCATION_TOP = 2 BUTTON_LOCATION_TOP = 2
@ -24,21 +27,17 @@ FORM_POSITION_TOP_RIGHT = 'tr'
FORM_POSITION_BOTTOM_LEFT = 'bl' FORM_POSITION_BOTTOM_LEFT = 'bl'
FORM_POSITION_BOTTOM_RIGHT = 'br' FORM_POSITION_BOTTOM_RIGHT = 'br'
WIDGET_ID_RE = re.compile( WIDGET_ID_RE = re.compile(r'^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$')
r'^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$'
)
SETUP_CODE = """ SETUP_CODE = """
<script> <script type="text/javascript">
document.write(unescape("%%3Cscript src='" + ((document.location.protocol=="https:")?"https://snapabug.appspot.com":"http://www.snapengage.com") + "/snapabug.js' type='text/javascript'%%3E%%3C/script%%3E"));</script><script> document.write(unescape("%%3Cscript src='" + ((document.location.protocol=="https:")?"https://snapabug.appspot.com":"http://www.snapengage.com") + "/snapabug.js' type='text/javascript'%%3E%%3C/script%%3E"));</script><script type="text/javascript">
%(settings_code)s %(settings_code)s
</script> </script>
""" # noqa """
DOMAIN_CODE = 'SnapABug.setDomain("%s");' DOMAIN_CODE = 'SnapABug.setDomain("%s");'
SECURE_CONNECTION_CODE = 'SnapABug.setSecureConnexion();' SECURE_CONNECTION_CODE = 'SnapABug.setSecureConnexion();'
INIT_CODE = 'SnapABug.init("%s");' INIT_CODE = 'SnapABug.init("%s");'
ADDBUTTON_CODE = ( ADDBUTTON_CODE = 'SnapABug.addButton("%(id)s","%(location)s","%(offset)s"%(dynamic_tail)s);'
'SnapABug.addButton("%(id)s","%(location)s","%(offset)s"%(dynamic_tail)s);'
)
SETBUTTON_CODE = 'SnapABug.setButton("%s");' SETBUTTON_CODE = 'SnapABug.setButton("%s");'
SETEMAIL_CODE = 'SnapABug.setUserEmail("%s"%s);' SETEMAIL_CODE = 'SnapABug.setUserEmail("%s"%s);'
SETLOCALE_CODE = 'SnapABug.setLocale("%s");' SETLOCALE_CODE = 'SnapABug.setLocale("%s");'
@ -59,7 +58,7 @@ def snapengage(parser, token):
""" """
SnapEngage set-up template tag. SnapEngage set-up template tag.
Renders JavaScript code to set-up SnapEngage chat. You must supply Renders Javascript code to set-up SnapEngage chat. You must supply
your widget ID in the ``SNAPENGAGE_WIDGET_ID`` setting. your widget ID in the ``SNAPENGAGE_WIDGET_ID`` setting.
""" """
bits = token.split_contents() bits = token.split_contents()
@ -67,28 +66,23 @@ def snapengage(parser, token):
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0]) raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return SnapEngageNode() return SnapEngageNode()
class SnapEngageNode(Node): class SnapEngageNode(Node):
def __init__(self): def __init__(self):
self.widget_id = get_required_setting( self.widget_id = get_required_setting('SNAPENGAGE_WIDGET_ID',
'SNAPENGAGE_WIDGET_ID', WIDGET_ID_RE, "must be a string looking like this: "
WIDGET_ID_RE, "'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'")
"must be a string looking like this: 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'",
)
def render(self, context): def render(self, context):
settings_code = [] settings_code = []
domain = self._get_setting(context, 'snapengage_domain', 'SNAPENGAGE_DOMAIN') domain = self._get_setting(context, 'snapengage_domain',
'SNAPENGAGE_DOMAIN')
if domain is not None: if domain is not None:
settings_code.append(DOMAIN_CODE % domain) settings_code.append(DOMAIN_CODE % domain)
secure_connection = self._get_setting( secure_connection = self._get_setting(context,
context, 'snapengage_secure_connection', 'SNAPENGAGE_SECURE_CONNECTION',
'snapengage_secure_connection', False)
'SNAPENGAGE_SECURE_CONNECTION',
False,
)
if secure_connection: if secure_connection:
settings_code.append(SECURE_CONNECTION_CODE) settings_code.append(SECURE_CONNECTION_CODE)
@ -96,101 +90,80 @@ class SnapEngageNode(Node):
if email is None: if email is None:
email = get_identity(context, 'snapengage', lambda u: u.email) email = get_identity(context, 'snapengage', lambda u: u.email)
if email is not None: if email is not None:
if self._get_setting( if self._get_setting(context, 'snapengage_readonly_email',
context, 'snapengage_readonly_email', 'SNAPENGAGE_READONLY_EMAIL', False 'SNAPENGAGE_READONLY_EMAIL', False):
):
readonly_tail = ',true' readonly_tail = ',true'
else: else:
readonly_tail = '' readonly_tail = ''
settings_code.append(SETEMAIL_CODE % (email, readonly_tail)) settings_code.append(SETEMAIL_CODE % (email, readonly_tail))
locale = self._get_setting(context, 'snapengage_locale', 'SNAPENGAGE_LOCALE') locale = self._get_setting(context, 'snapengage_locale',
'SNAPENGAGE_LOCALE')
if locale is None: if locale is None:
locale = translation.to_locale(translation.get_language()) locale = translation.to_locale(translation.get_language())
settings_code.append(SETLOCALE_CODE % locale) settings_code.append(SETLOCALE_CODE % locale)
form_position = self._get_setting( form_position = self._get_setting(context,
context, 'snapengage_form_position', 'SNAPENGAGE_FORM_POSITION' 'snapengage_form_position', 'SNAPENGAGE_FORM_POSITION')
)
if form_position is not None: if form_position is not None:
settings_code.append(FORM_POSITION_CODE % form_position) settings_code.append(FORM_POSITION_CODE % form_position)
form_top_position = self._get_setting( form_top_position = self._get_setting(context,
context, 'snapengage_form_top_position', 'SNAPENGAGE_FORM_TOP_POSITION' 'snapengage_form_top_position', 'SNAPENGAGE_FORM_TOP_POSITION')
)
if form_top_position is not None: if form_top_position is not None:
settings_code.append(FORM_TOP_POSITION_CODE % form_top_position) settings_code.append(FORM_TOP_POSITION_CODE % form_top_position)
show_offline = self._get_setting( show_offline = self._get_setting(context, 'snapengage_show_offline',
context, 'snapengage_show_offline', 'SNAPENGAGE_SHOW_OFFLINE', True 'SNAPENGAGE_SHOW_OFFLINE', True)
)
if not show_offline: if not show_offline:
settings_code.append(DISABLE_OFFLINE_CODE) settings_code.append(DISABLE_OFFLINE_CODE)
screenshots = self._get_setting( screenshots = self._get_setting(context, 'snapengage_screenshots',
context, 'snapengage_screenshots', 'SNAPENGAGE_SCREENSHOTS', True 'SNAPENGAGE_SCREENSHOTS', True)
)
if not screenshots: if not screenshots:
settings_code.append(DISABLE_SCREENSHOT_CODE) settings_code.append(DISABLE_SCREENSHOT_CODE)
offline_screenshots = self._get_setting( offline_screenshots = self._get_setting(context,
context,
'snapengage_offline_screenshots', 'snapengage_offline_screenshots',
'SNAPENGAGE_OFFLINE_SCREENSHOTS', 'SNAPENGAGE_OFFLINE_SCREENSHOTS', True)
True,
)
if not offline_screenshots: if not offline_screenshots:
settings_code.append(DISABLE_OFFLINE_SCREENSHOT_CODE) settings_code.append(DISABLE_OFFLINE_SCREENSHOT_CODE)
if not context.get('snapengage_proactive_chat', True): if not context.get('snapengage_proactive_chat', True):
settings_code.append(DISABLE_PROACTIVE_CHAT_CODE) settings_code.append(DISABLE_PROACTIVE_CHAT_CODE)
sounds = self._get_setting( sounds = self._get_setting(context, 'snapengage_sounds',
context, 'snapengage_sounds', 'SNAPENGAGE_SOUNDS', True 'SNAPENGAGE_SOUNDS', True)
)
if not sounds: if not sounds:
settings_code.append(DISABLE_SOUNDS_CODE) settings_code.append(DISABLE_SOUNDS_CODE)
button_effect = self._get_setting( button_effect = self._get_setting(context, 'snapengage_button_effect',
context, 'snapengage_button_effect', 'SNAPENGAGE_BUTTON_EFFECT' 'SNAPENGAGE_BUTTON_EFFECT')
)
if button_effect is not None: if button_effect is not None:
settings_code.append(BUTTONEFFECT_CODE % button_effect) settings_code.append(BUTTONEFFECT_CODE % button_effect)
button = self._get_setting( button = self._get_setting(context, 'snapengage_button',
context, 'snapengage_button', 'SNAPENGAGE_BUTTON', BUTTON_STYLE_DEFAULT 'SNAPENGAGE_BUTTON', BUTTON_STYLE_DEFAULT)
)
if button == BUTTON_STYLE_NONE: if button == BUTTON_STYLE_NONE:
settings_code.append(INIT_CODE % self.widget_id) settings_code.append(INIT_CODE % self.widget_id)
else: else:
if not isinstance(button, int): if not isinstance(button, int):
# Assume button as a URL to a custom image # Assume button as a URL to a custom image
settings_code.append(SETBUTTON_CODE % button) settings_code.append(SETBUTTON_CODE % button)
button_location = self._get_setting( button_location = self._get_setting(context,
context, 'snapengage_button_location', 'SNAPENGAGE_BUTTON_LOCATION',
'snapengage_button_location', BUTTON_LOCATION_LEFT)
'SNAPENGAGE_BUTTON_LOCATION', button_offset = self._get_setting(context,
BUTTON_LOCATION_LEFT, 'snapengage_button_location_offset',
) 'SNAPENGAGE_BUTTON_LOCATION_OFFSET', '55%')
button_offset = self._get_setting( settings_code.append(ADDBUTTON_CODE % {
context, 'id': self.widget_id,
'snapengage_button_location_offset', 'location': button_location,
'SNAPENGAGE_BUTTON_LOCATION_OFFSET', 'offset': button_offset,
'55%', 'dynamic_tail': ',true' if (button == BUTTON_STYLE_LIVE) else '',
) })
settings_code.append( html = SETUP_CODE % {'widget_id': self.widget_id,
ADDBUTTON_CODE 'settings_code': " ".join(settings_code)}
% {
'id': self.widget_id,
'location': button_location,
'offset': button_offset,
'dynamic_tail': ',true' if (button == BUTTON_STYLE_LIVE) else '',
}
)
html = SETUP_CODE % {
'widget_id': self.widget_id,
'settings_code': ' '.join(settings_code),
}
return html return html
def _get_setting(self, context, context_key, setting=None, default=None): def _get_setting(self, context, context_key, setting=None, default=None):

View file

@ -2,16 +2,15 @@
Spring Metrics template tags and filters. Spring Metrics template tags and filters.
""" """
from __future__ import absolute_import
import re import re
from django.template import Library, Node, TemplateSyntaxError from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import ( from analytical.utils import get_identity, is_internal_ip, disable_html, \
disable_html, get_required_setting
get_identity,
get_required_setting,
is_internal_ip,
)
TRACKING_ID_RE = re.compile(r'^[0-9a-f]+$') TRACKING_ID_RE = re.compile(r'^[0-9a-f]+$')
TRACKING_CODE = """ TRACKING_CODE = """
@ -30,7 +29,8 @@ TRACKING_CODE = """
)(); )();
%(custom_commands)s %(custom_commands)s
</script> </script>
""" # noqa """
register = Library() register = Library()
@ -40,7 +40,7 @@ def spring_metrics(parser, token):
""" """
Spring Metrics tracking template tag. Spring Metrics tracking template tag.
Renders JavaScript code to track page visits. You must supply Renders Javascript code to track page visits. You must supply
your Spring Metrics Tracking ID in the your Spring Metrics Tracking ID in the
``SPRING_METRICS_TRACKING_ID`` setting. ``SPRING_METRICS_TRACKING_ID`` setting.
""" """
@ -49,12 +49,10 @@ def spring_metrics(parser, token):
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0]) raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return SpringMetricsNode() return SpringMetricsNode()
class SpringMetricsNode(Node): class SpringMetricsNode(Node):
def __init__(self): def __init__(self):
self.tracking_id = get_required_setting( self.tracking_id = get_required_setting('SPRING_METRICS_TRACKING_ID',
'SPRING_METRICS_TRACKING_ID', TRACKING_ID_RE, 'must be a hexadecimal string' TRACKING_ID_RE, "must be a hexadecimal string")
)
def render(self, context): def render(self, context):
custom = {} custom = {}
@ -63,28 +61,25 @@ class SpringMetricsNode(Node):
if var.startswith('spring_metrics_'): if var.startswith('spring_metrics_'):
custom[var[15:]] = val custom[var[15:]] = val
if 'email' not in custom: if 'email' not in custom:
identity = get_identity(context, 'spring_metrics', lambda u: u.email) identity = get_identity(context, 'spring_metrics',
lambda u: u.email)
if identity is not None: if identity is not None:
custom['email'] = identity custom['email'] = identity
html = TRACKING_CODE % { html = TRACKING_CODE % {'tracking_id': self.tracking_id,
'tracking_id': self.tracking_id, 'custom_commands': self._generate_custom_javascript(custom)}
'custom_commands': self._generate_custom_javascript(custom),
}
if is_internal_ip(context, 'SPRING_METRICS'): if is_internal_ip(context, 'SPRING_METRICS'):
html = disable_html(html, 'Spring Metrics') html = disable_html(html, 'Spring Metrics')
return html return html
def _generate_custom_javascript(self, params): def _generate_custom_javascript(self, vars):
commands = [] commands = []
convert = params.pop('convert', None) convert = vars.pop('convert', None)
if convert is not None: if convert is not None:
commands.append("_springMetq.push(['convert', '%s'])" % convert) commands.append("_springMetq.push(['convert', '%s'])" % convert)
commands.extend( commands.extend("_springMetq.push(['setdata', {'%s': '%s'}]);"
"_springMetq.push(['setdata', {'%s': '%s'}]);" % (var, val) % (var, val) for var, val in vars.items())
for var, val in params.items() return " ".join(commands)
)
return ' '.join(commands)
def contribute_to_analytical(add_node): def contribute_to_analytical(add_node):

View file

@ -2,17 +2,19 @@
UserVoice template tags. UserVoice template tags.
""" """
from __future__ import absolute_import
import json import json
import re import re
from django.conf import settings from django.conf import settings
from django.template import Library, Node, TemplateSyntaxError from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import get_required_setting, get_identity
from analytical.utils import get_identity, get_required_setting
WIDGET_KEY_RE = re.compile(r'^[a-zA-Z0-9]*$') WIDGET_KEY_RE = re.compile(r'^[a-zA-Z0-9]*$')
TRACKING_CODE = """ TRACKING_CODE = """
<script> <script type="text/javascript">
UserVoice=window.UserVoice||[];(function(){ UserVoice=window.UserVoice||[];(function(){
var uv=document.createElement('script');uv.type='text/javascript'; var uv=document.createElement('script');uv.type='text/javascript';
@ -35,7 +37,7 @@ def uservoice(parser, token):
""" """
UserVoice tracking template tag. UserVoice tracking template tag.
Renders JavaScript code to track page visits. You must supply Renders Javascript code to track page visits. You must supply
your UserVoice Widget Key in the ``USERVOICE_WIDGET_KEY`` your UserVoice Widget Key in the ``USERVOICE_WIDGET_KEY``
setting or the ``uservoice_widget_key`` template context variable. setting or the ``uservoice_widget_key`` template context variable.
""" """
@ -47,9 +49,8 @@ def uservoice(parser, token):
class UserVoiceNode(Node): class UserVoiceNode(Node):
def __init__(self): def __init__(self):
self.default_widget_key = get_required_setting( self.default_widget_key = get_required_setting('USERVOICE_WIDGET_KEY',
'USERVOICE_WIDGET_KEY', WIDGET_KEY_RE, 'must be an alphanumeric string' WIDGET_KEY_RE, "must be an alphanumeric string")
)
def render(self, context): def render(self, context):
widget_key = context.get('uservoice_widget_key') widget_key = context.get('uservoice_widget_key')
@ -66,16 +67,13 @@ class UserVoiceNode(Node):
if identity: if identity:
identity = IDENTITY % {'options': json.dumps(identity, sort_keys=True)} identity = IDENTITY % {'options': json.dumps(identity, sort_keys=True)}
trigger = context.get( trigger = context.get('uservoice_add_trigger',
'uservoice_add_trigger', getattr(settings, 'USERVOICE_ADD_TRIGGER', True) getattr(settings, 'USERVOICE_ADD_TRIGGER', True))
)
html = TRACKING_CODE % { html = TRACKING_CODE % {'widget_key': widget_key,
'widget_key': widget_key, 'options': json.dumps(options, sort_keys=True),
'options': json.dumps(options, sort_keys=True), 'trigger': TRIGGER if trigger else '',
'trigger': TRIGGER if trigger else '', 'identity': identity if identity else ''}
'identity': identity if identity else '',
}
return html return html
def _identify(self, user): def _identify(self, user):
@ -84,7 +82,6 @@ class UserVoiceNode(Node):
name = user.username name = user.username
return {'name': name, 'email': user.email} return {'name': name, 'email': user.email}
def contribute_to_analytical(add_node): def contribute_to_analytical(add_node):
UserVoiceNode() # ensure properly configured UserVoiceNode() # ensure properly configured
add_node('body_bottom', UserVoiceNode) add_node('body_bottom', UserVoiceNode)

View file

@ -2,34 +2,34 @@
Woopra template tags and filters. Woopra template tags and filters.
""" """
from __future__ import absolute_import
import json import json
import re import re
from contextlib import suppress
from django.conf import settings from django.conf import settings
from django.template import Library, Node, TemplateSyntaxError from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import ( from analytical.utils import get_identity, get_user_from_context, \
AnalyticalException, is_internal_ip, disable_html, get_required_setting
disable_html,
get_identity,
get_required_setting,
get_user_from_context,
get_user_is_authenticated,
is_internal_ip,
)
DOMAIN_RE = re.compile(r'^\S+$') DOMAIN_RE = re.compile(r'^\S+$')
TRACKING_CODE = """ TRACKING_CODE = """
<script> <script type="text/javascript">
var woo_settings = %(settings)s; var woo_settings = %(settings)s;
var woo_visitor = %(visitor)s; var woo_visitor = %(visitor)s;
!function(){var a,b,c,d=window,e=document,f=arguments,g="script",h=["config","track","trackForm","trackClick","identify","visit","push","call"],i=function(){var a,b=this,c=function(a){b[a]=function(){return b._e.push([a].concat(Array.prototype.slice.call(arguments,0))),b}};for(b._e=[],a=0;a<h.length;a++)c(h[a])};for(d.__woo=d.__woo||{},a=0;a<f.length;a++)d.__woo[f[a]]=d[f[a]]=d[f[a]]||new i;b=e.createElement(g),b.async=1,b.src="//static.woopra.com/js/w.js",c=e.getElementsByTagName(g)[0],c.parentNode.insertBefore(b,c)}("woopra"); (function(){
woopra.config(woo_settings); var wsc=document.createElement('script');
woopra.identify(woo_visitor); wsc.type='text/javascript';
woopra.track(); wsc.src=document.location.protocol+'//static.woopra.com/js/woopra.js';
wsc.async=true;
var ssc = document.getElementsByTagName('script')[0];
ssc.parentNode.insertBefore(wsc, ssc);
})();
</script> </script>
""" # noqa """
register = Library() register = Library()
@ -39,7 +39,7 @@ def woopra(parser, token):
""" """
Woopra tracking template tag. Woopra tracking template tag.
Renders JavaScript code to track page visits. You must supply Renders Javascript code to track page visits. You must supply
your Woopra domain in the ``WOOPRA_DOMAIN`` setting. your Woopra domain in the ``WOOPRA_DOMAIN`` setting.
""" """
bits = token.split_contents() bits = token.split_contents()
@ -47,12 +47,10 @@ def woopra(parser, token):
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0]) raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return WoopraNode() return WoopraNode()
class WoopraNode(Node): class WoopraNode(Node):
def __init__(self): def __init__(self):
self.domain = get_required_setting( self.domain = get_required_setting('WOOPRA_DOMAIN', DOMAIN_RE,
'WOOPRA_DOMAIN', DOMAIN_RE, 'must be a domain name' "must be a domain name")
)
def render(self, context): def render(self, context):
settings = self._get_settings(context) settings = self._get_settings(context)
@ -67,58 +65,27 @@ class WoopraNode(Node):
return html return html
def _get_settings(self, context): def _get_settings(self, context):
variables = {'domain': self.domain} vars = {'domain': self.domain}
woopra_int_settings = { try:
'idle_timeout': 'WOOPRA_IDLE_TIMEOUT', vars['idle_timeout'] = str(settings.WOOPRA_IDLE_TIMEOUT)
} except AttributeError:
woopra_str_settings = { pass
'cookie_name': 'WOOPRA_COOKIE_NAME', return vars
'cookie_domain': 'WOOPRA_COOKIE_DOMAIN',
'cookie_path': 'WOOPRA_COOKIE_PATH',
'cookie_expire': 'WOOPRA_COOKIE_EXPIRE',
}
woopra_bool_settings = {
'click_tracking': 'WOOPRA_CLICK_TRACKING',
'download_tracking': 'WOOPRA_DOWNLOAD_TRACKING',
'outgoing_tracking': 'WOOPRA_OUTGOING_TRACKING',
'outgoing_ignore_subdomain': 'WOOPRA_OUTGOING_IGNORE_SUBDOMAIN',
'ignore_query_url': 'WOOPRA_IGNORE_QUERY_URL',
'hide_campaign': 'WOOPRA_HIDE_CAMPAIGN',
}
for key, name in woopra_int_settings.items():
with suppress(AttributeError):
variables[key] = getattr(settings, name)
if type(variables[key]) is not int:
raise AnalyticalException(f'{name} must be an int value')
for key, name in woopra_str_settings.items():
with suppress(AttributeError):
variables[key] = getattr(settings, name)
if type(variables[key]) is not str:
raise AnalyticalException(f'{name} must be a string value')
for key, name in woopra_bool_settings.items():
with suppress(AttributeError):
variables[key] = getattr(settings, name)
if type(variables[key]) is not bool:
raise AnalyticalException(f'{name} must be a boolean value')
return variables
def _get_visitor(self, context): def _get_visitor(self, context):
params = {} vars = {}
for dict_ in context: for dict_ in context:
for var, val in dict_.items(): for var, val in dict_.items():
if var.startswith('woopra_'): if var.startswith('woopra_'):
params[var[7:]] = val vars[var[7:]] = val
if 'name' not in params and 'email' not in params: if 'name' not in vars and 'email' not in vars:
user = get_user_from_context(context) user = get_user_from_context(context)
if user is not None and get_user_is_authenticated(user): if user is not None and user.is_authenticated():
params['name'] = get_identity(context, 'woopra', self._identify, user) vars['name'] = get_identity(context, 'woopra',
self._identify, user)
if user.email: if user.email:
params['email'] = user.email vars['email'] = user.email
return params return vars
def _identify(self, user): def _identify(self, user):
name = user.get_full_name() name = user.get_full_name()

View file

@ -1,91 +0,0 @@
"""
Yandex.Metrica template tags and filters.
"""
import json
import re
from django.conf import settings
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import disable_html, get_required_setting, is_internal_ip
COUNTER_ID_RE = re.compile(r'^\d{8}$')
COUNTER_CODE = """
<script>
(function (d, w, c) {
(w[c] = w[c] || []).push(function() {
try {
w.yaCounter%(counter_id)s = new Ya.Metrika(%(options)s);
} catch(e) { }
});
var n = d.getElementsByTagName("script")[0],
s = d.createElement("script"),
f = function () { n.parentNode.insertBefore(s, n); };
s.type = "text/javascript";
s.async = true;
s.src = "https://mc.yandex.ru/metrika/watch.js";
if (w.opera == "[object Opera]") {
d.addEventListener("DOMContentLoaded", f, false);
} else { f(); }
})(document, window, "yandex_metrika_callbacks");
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/%(counter_id)s" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
""" # noqa
register = Library()
@register.tag
def yandex_metrica(parser, token):
"""
Yandex.Metrica counter template tag.
Renders JavaScript code to track page visits. You must supply
your website counter ID (as a string) in the
``YANDEX_METRICA_COUNTER_ID`` setting.
"""
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return YandexMetricaNode()
class YandexMetricaNode(Node):
def __init__(self):
self.counter_id = get_required_setting(
'YANDEX_METRICA_COUNTER_ID',
COUNTER_ID_RE,
"must be (a string containing) a number'",
)
def render(self, context):
options = {
'id': int(self.counter_id),
'clickmap': True,
'trackLinks': True,
'accurateTrackBounce': True,
}
if getattr(settings, 'YANDEX_METRICA_WEBVISOR', False):
options['webvisor'] = True
if getattr(settings, 'YANDEX_METRICA_TRACKHASH', False):
options['trackHash'] = True
if getattr(settings, 'YANDEX_METRICA_NOINDEX', False):
options['ut'] = 'noindex'
if getattr(settings, 'YANDEX_METRICA_ECOMMERCE', False):
options['ecommerce'] = 'dataLayer'
html = COUNTER_CODE % {
'counter_id': self.counter_id,
'options': json.dumps(options),
}
if is_internal_ip(context, 'YANDEX_METRICA'):
html = disable_html(html, 'Yandex.Metrica')
return html
def contribute_to_analytical(add_node):
YandexMetricaNode() # ensure properly configured
add_node('head_bottom', YandexMetricaNode)

View file

@ -0,0 +1,27 @@
"""
Tests for django-analytical.
"""
from analytical.tests.test_tag_analytical import *
from analytical.tests.test_tag_chartbeat import *
from analytical.tests.test_tag_clickmap import *
from analytical.tests.test_tag_clicky import *
from analytical.tests.test_tag_crazy_egg import *
from analytical.tests.test_tag_gauges import *
from analytical.tests.test_tag_google_analytics import *
from analytical.tests.test_tag_gosquared import *
from analytical.tests.test_tag_hubspot import *
from analytical.tests.test_tag_intercom import *
from analytical.tests.test_tag_kiss_insights import *
from analytical.tests.test_tag_kiss_metrics import *
from analytical.tests.test_tag_mixpanel import *
from analytical.tests.test_tag_olark import *
from analytical.tests.test_tag_optimizely import *
from analytical.tests.test_tag_performable import *
from analytical.tests.test_tag_piwik import *
from analytical.tests.test_tag_reinvigorate import *
from analytical.tests.test_tag_snapengage import *
from analytical.tests.test_tag_spring_metrics import *
from analytical.tests.test_tag_uservoice import *
from analytical.tests.test_tag_woopra import *
from analytical.tests.test_utils import *

View file

@ -0,0 +1,19 @@
"""
django-analytical testing settings.
"""
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
}
}
INSTALLED_APPS = [
'analytical',
]
SECRET_KEY = 'testing'
MIDDLEWARE_CLASSES=('django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware'),

View file

@ -2,22 +2,23 @@
Dummy testing template tags and filters. Dummy testing template tags and filters.
""" """
from __future__ import absolute_import
from django.template import Library, Node, TemplateSyntaxError from django.template import Library, Node, TemplateSyntaxError
from analytical.templatetags.analytical import TAG_LOCATIONS from analytical.templatetags.analytical import TAG_LOCATIONS
register = Library() register = Library()
def _location_node(location): def _location_node(location):
class DummyNode(Node): class DummyNode(Node):
def render(self, context): def render(self, context):
return '<!-- dummy_%s -->' % location return "<!-- dummy_%s -->" % location
return DummyNode return DummyNode
_location_nodes = dict((l, _location_node(l)) for l in TAG_LOCATIONS)
_location_nodes = {loc: _location_node(loc) for loc in TAG_LOCATIONS}
def _location_tag(location): def _location_tag(location):
@ -26,10 +27,8 @@ def _location_tag(location):
if len(bits) > 1: if len(bits) > 1:
raise TemplateSyntaxError("'%s' tag takes no arguments" % bits[0]) raise TemplateSyntaxError("'%s' tag takes no arguments" % bits[0])
return _location_nodes[location] return _location_nodes[location]
return dummy_tag return dummy_tag
for loc in TAG_LOCATIONS: for loc in TAG_LOCATIONS:
register.tag('dummy_%s' % loc, _location_tag(loc)) register.tag('dummy_%s' % loc, _location_tag(loc))

View file

@ -3,9 +3,9 @@ Tests for the generic template tags and filters.
""" """
from django.template import Context, Template from django.template import Context, Template
from utils import TagTestCase
from analytical.templatetags import analytical from analytical.templatetags import analytical
from analytical.tests.utils import TagTestCase
class AnalyticsTagTestCase(TagTestCase): class AnalyticsTagTestCase(TagTestCase):
@ -14,23 +14,24 @@ class AnalyticsTagTestCase(TagTestCase):
""" """
def setUp(self): def setUp(self):
super().setUp() super(AnalyticsTagTestCase, self).setUp()
self._tag_modules = analytical.TAG_MODULES self._tag_modules = analytical.TAG_MODULES
analytical.TAG_MODULES = ['tests.testproject.dummy'] analytical.TAG_MODULES = ['analytical.tests.dummy']
analytical.template_nodes = analytical._load_template_nodes() analytical.template_nodes = analytical._load_template_nodes()
def tearDown(self): def tearDown(self):
analytical.TAG_MODULES = self._tag_modules analytical.TAG_MODULES = self._tag_modules
analytical.template_nodes = analytical._load_template_nodes() analytical.template_nodes = analytical._load_template_nodes()
super().tearDown() super(AnalyticsTagTestCase, self).tearDown()
def render_location_tag(self, location, vars=None): def render_location_tag(self, location, vars=None):
if vars is None: if vars is None:
vars = {} vars = {}
t = Template('{%% load analytical %%}{%% analytical_%s %%}' % location) t = Template("{%% load analytical %%}{%% analytical_%s %%}"
% location)
return t.render(Context(vars)) return t.render(Context(vars))
def test_location_tags(self): def test_location_tags(self):
for loc in ['head_top', 'head_bottom', 'body_top', 'body_bottom']: for l in ['head_top', 'head_bottom', 'body_top', 'body_bottom']:
r = self.render_location_tag(loc) r = self.render_location_tag(l)
assert f'dummy_{loc}' in r self.assertTrue('dummy_%s' % l in r, r)

View file

@ -4,14 +4,15 @@ Tests for the Chartbeat template tags and filters.
import re import re
import pytest from django.contrib.sites.models import Site
from django.http import HttpRequest from django.http import HttpRequest
from django.template import Context from django.template import Context
from django.test import TestCase from django.test import TestCase
from django.test.utils import override_settings from django.test.utils import override_settings
from utils import TagTestCase
from analytical.templatetags.chartbeat import ChartbeatBottomNode, ChartbeatTopNode from analytical.templatetags.chartbeat import ChartbeatTopNode, \
ChartbeatBottomNode
from analytical.tests.utils import TagTestCase
from analytical.utils import AnalyticalException from analytical.utils import AnalyticalException
@ -22,29 +23,23 @@ class ChartbeatTagTestCaseNoSites(TestCase):
self.assertTrue('var _sf_async_config={"uid": "12345"};' in r, r) self.assertTrue('var _sf_async_config={"uid": "12345"};' in r, r)
@override_settings( @override_settings(INSTALLED_APPS=('analytical', 'django.contrib.sites'))
INSTALLED_APPS=(
'analytical',
'django.contrib.sites',
'django.contrib.auth',
'django.contrib.contenttypes',
)
)
@override_settings(CHARTBEAT_USER_ID='12345') @override_settings(CHARTBEAT_USER_ID='12345')
class ChartbeatTagTestCaseWithSites(TestCase): class ChartbeatTagTestCaseWithSites(TestCase):
def setUp(self): def setUp(self):
from django.core.management import call_command from django.core.management import call_command
from django.db.models import loading
call_command('migrate', verbosity=0) loading.cache.loaded = False
call_command("syncdb", verbosity=0)
def test_rendering_setup_site(self): def test_rendering_setup_site(self):
from django.contrib.sites.models import Site site = Site.objects.create(domain="test.com", name="test")
site = Site.objects.create(domain='test.com', name='test')
with override_settings(SITE_ID=site.id): with override_settings(SITE_ID=site.id):
r = ChartbeatBottomNode().render(Context()) r = ChartbeatBottomNode().render(Context())
assert re.search('var _sf_async_config={.*"uid": "12345".*};', r) self.assertTrue(re.search(
assert re.search('var _sf_async_config={.*"domain": "test.com".*};', r) 'var _sf_async_config={.*"uid": "12345".*};', r), r)
self.assertTrue(re.search(
'var _sf_async_config={.*"domain": "test.com".*};', r), r)
@override_settings(CHARTBEAT_AUTO_DOMAIN=False) @override_settings(CHARTBEAT_AUTO_DOMAIN=False)
def test_auto_domain_false(self): def test_auto_domain_false(self):
@ -54,7 +49,7 @@ class ChartbeatTagTestCaseWithSites(TestCase):
in _sf_async_config. in _sf_async_config.
""" """
r = ChartbeatBottomNode().render(Context()) r = ChartbeatBottomNode().render(Context())
assert 'var _sf_async_config={"uid": "12345"};' in r self.assertTrue('var _sf_async_config={"uid": "12345"};' in r, r)
@override_settings(CHARTBEAT_USER_ID='12345') @override_settings(CHARTBEAT_USER_ID='12345')
@ -64,48 +59,38 @@ class ChartbeatTagTestCase(TagTestCase):
""" """
def test_top_tag(self): def test_top_tag(self):
r = self.render_tag( r = self.render_tag('chartbeat', 'chartbeat_top',
'chartbeat', 'chartbeat_top', {'chartbeat_domain': 'test.com'} {'chartbeat_domain': "test.com"})
) self.assertTrue('var _sf_startpt=(new Date()).getTime()' in r, r)
assert 'var _sf_startpt=(new Date()).getTime()' in r
def test_bottom_tag(self): def test_bottom_tag(self):
r = self.render_tag( r = self.render_tag('chartbeat', 'chartbeat_bottom',
'chartbeat', 'chartbeat_bottom', {'chartbeat_domain': 'test.com'} {'chartbeat_domain': "test.com"})
) self.assertTrue(re.search(
assert re.search('var _sf_async_config={.*"uid": "12345".*};', r) 'var _sf_async_config={.*"uid": "12345".*};', r), r)
assert re.search('var _sf_async_config={.*"domain": "test.com".*};', r) self.assertTrue(re.search(
'var _sf_async_config={.*"domain": "test.com".*};', r), r)
def test_top_node(self): def test_top_node(self):
r = ChartbeatTopNode().render( r = ChartbeatTopNode().render(
Context( Context({'chartbeat_domain': "test.com"}))
{ self.assertTrue('var _sf_startpt=(new Date()).getTime()' in r, r)
'chartbeat_domain': 'test.com',
}
)
)
assert 'var _sf_startpt=(new Date()).getTime()' in r
def test_bottom_node(self): def test_bottom_node(self):
r = ChartbeatBottomNode().render( r = ChartbeatBottomNode().render(
Context( Context({'chartbeat_domain': "test.com"}))
{ self.assertTrue(re.search(
'chartbeat_domain': 'test.com', 'var _sf_async_config={.*"uid": "12345".*};', r), r)
} self.assertTrue(re.search(
) 'var _sf_async_config={.*"domain": "test.com".*};', r), r)
)
assert re.search('var _sf_async_config={.*"uid": "12345".*};', r)
assert re.search('var _sf_async_config={.*"domain": "test.com".*};', r)
@override_settings(CHARTBEAT_USER_ID=None) @override_settings(CHARTBEAT_USER_ID=None)
def test_no_user_id(self): def test_no_user_id(self):
with pytest.raises(AnalyticalException): self.assertRaises(AnalyticalException, ChartbeatBottomNode)
ChartbeatBottomNode()
@override_settings(CHARTBEAT_USER_ID='123abc') @override_settings(CHARTBEAT_USER_ID='123abc')
def test_wrong_user_id(self): def test_wrong_user_id(self):
with pytest.raises(AnalyticalException): self.assertRaises(AnalyticalException, ChartbeatBottomNode)
ChartbeatBottomNode()
@override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1']) @override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
def test_render_internal_ip(self): def test_render_internal_ip(self):
@ -113,5 +98,6 @@ class ChartbeatTagTestCase(TagTestCase):
req.META['REMOTE_ADDR'] = '1.1.1.1' req.META['REMOTE_ADDR'] = '1.1.1.1'
context = Context({'request': req}) context = Context({'request': req})
r = ChartbeatBottomNode().render(context) r = ChartbeatBottomNode().render(context)
assert r.startswith('<!-- Chartbeat disabled on internal IP address') self.assertTrue(r.startswith(
assert r.endswith('-->') '<!-- Chartbeat disabled on internal IP address'), r)
self.assertTrue(r.endswith('-->'), r)

View file

@ -2,13 +2,12 @@
Tests for the Clickmap template tags and filters. Tests for the Clickmap template tags and filters.
""" """
import pytest
from django.http import HttpRequest from django.http import HttpRequest
from django.template import Context from django.template import Context
from django.test.utils import override_settings from django.test.utils import override_settings
from utils import TagTestCase
from analytical.templatetags.clickmap import ClickmapNode from analytical.templatetags.clickmap import ClickmapNode
from analytical.tests.utils import TagTestCase
from analytical.utils import AnalyticalException from analytical.utils import AnalyticalException
@ -20,21 +19,19 @@ class ClickmapTagTestCase(TagTestCase):
def test_tag(self): def test_tag(self):
r = self.render_tag('clickmap', 'clickmap') r = self.render_tag('clickmap', 'clickmap')
assert "tracker: '12345ABC', version:'2'};" in r self.assertTrue("tracker: '12345ABC', version:'2'};" in r, r)
def test_node(self): def test_node(self):
r = ClickmapNode().render(Context({})) r = ClickmapNode().render(Context({}))
assert "tracker: '12345ABC', version:'2'};" in r self.assertTrue("tracker: '12345ABC', version:'2'};" in r, r)
@override_settings(CLICKMAP_TRACKER_ID=None) @override_settings(CLICKMAP_TRACKER_ID=None)
def test_no_site_id(self): def test_no_site_id(self):
with pytest.raises(AnalyticalException): self.assertRaises(AnalyticalException, ClickmapNode)
ClickmapNode()
@override_settings(CLICKMAP_TRACKER_ID='ab#c') @override_settings(CLICKMAP_TRACKER_ID='ab#c')
def test_wrong_site_id(self): def test_wrong_site_id(self):
with pytest.raises(AnalyticalException): self.assertRaises(AnalyticalException, ClickmapNode)
ClickmapNode()
@override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1']) @override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
def test_render_internal_ip(self): def test_render_internal_ip(self):
@ -42,5 +39,6 @@ class ClickmapTagTestCase(TagTestCase):
req.META['REMOTE_ADDR'] = '1.1.1.1' req.META['REMOTE_ADDR'] = '1.1.1.1'
context = Context({'request': req}) context = Context({'request': req})
r = ClickmapNode().render(context) r = ClickmapNode().render(context)
assert r.startswith('<!-- Clickmap disabled on internal IP address') self.assertTrue(r.startswith(
assert r.endswith('-->') '<!-- Clickmap disabled on internal IP address'), r)
self.assertTrue(r.endswith('-->'), r)

View file

@ -4,14 +4,13 @@ Tests for the Clicky template tags and filters.
import re import re
import pytest from django.contrib.auth.models import User, AnonymousUser
from django.contrib.auth.models import AnonymousUser, User
from django.http import HttpRequest from django.http import HttpRequest
from django.template import Context from django.template import Context
from django.test.utils import override_settings from django.test.utils import override_settings
from utils import TagTestCase
from analytical.templatetags.clicky import ClickyNode from analytical.templatetags.clicky import ClickyNode
from analytical.tests.utils import TagTestCase
from analytical.utils import AnalyticalException from analytical.utils import AnalyticalException
@ -23,46 +22,41 @@ class ClickyTagTestCase(TagTestCase):
def test_tag(self): def test_tag(self):
r = self.render_tag('clicky', 'clicky') r = self.render_tag('clicky', 'clicky')
assert 'clicky_site_ids.push(12345678);' in r self.assertTrue('clicky_site_ids.push(12345678);' in r, r)
assert 'src="//in.getclicky.com/12345678ns.gif"' in r self.assertTrue('src="//in.getclicky.com/12345678ns.gif"' in r,
r)
def test_node(self): def test_node(self):
r = ClickyNode().render(Context({})) r = ClickyNode().render(Context({}))
assert 'clicky_site_ids.push(12345678);' in r self.assertTrue('clicky_site_ids.push(12345678);' in r, r)
assert 'src="//in.getclicky.com/12345678ns.gif"' in r self.assertTrue('src="//in.getclicky.com/12345678ns.gif"' in r,
r)
@override_settings(CLICKY_SITE_ID=None) @override_settings(CLICKY_SITE_ID=None)
def test_no_site_id(self): def test_no_site_id(self):
with pytest.raises(AnalyticalException): self.assertRaises(AnalyticalException, ClickyNode)
ClickyNode()
@override_settings(CLICKY_SITE_ID='123abc') @override_settings(CLICKY_SITE_ID='123abc')
def test_wrong_site_id(self): def test_wrong_site_id(self):
with pytest.raises(AnalyticalException): self.assertRaises(AnalyticalException, ClickyNode)
ClickyNode()
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True) @override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
def test_identify(self): def test_identify(self):
r = ClickyNode().render(Context({'user': User(username='test')})) r = ClickyNode().render(Context({'user': User(username='test')}))
assert 'var clicky_custom = {"session": {"username": "test"}};' in r self.assertTrue(
'var clicky_custom = {"session": {"username": "test"}};' in r,
r)
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True) @override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
def test_identify_anonymous_user(self): def test_identify_anonymous_user(self):
r = ClickyNode().render(Context({'user': AnonymousUser()})) r = ClickyNode().render(Context({'user': AnonymousUser()}))
assert 'var clicky_custom = {"session": {"username":' not in r self.assertFalse('var clicky_custom = {"session": {"username":' in r, r)
def test_custom(self): def test_custom(self):
r = ClickyNode().render( r = ClickyNode().render(Context({'clicky_var1': 'val1',
Context( 'clicky_var2': 'val2'}))
{ self.assertTrue(re.search('var clicky_custom = {.*'
'clicky_var1': 'val1', '"var1": "val1", "var2": "val2".*};', r), r)
'clicky_var2': 'val2',
}
)
)
assert re.search(
r'var clicky_custom = {.*"var1": "val1", "var2": "val2".*};', r
)
@override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1']) @override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
def test_render_internal_ip(self): def test_render_internal_ip(self):
@ -70,5 +64,6 @@ class ClickyTagTestCase(TagTestCase):
req.META['REMOTE_ADDR'] = '1.1.1.1' req.META['REMOTE_ADDR'] = '1.1.1.1'
context = Context({'request': req}) context = Context({'request': req})
r = ClickyNode().render(context) r = ClickyNode().render(context)
assert r.startswith('<!-- Clicky disabled on internal IP address') self.assertTrue(r.startswith(
assert r.endswith('-->') '<!-- Clicky disabled on internal IP address'), r)
self.assertTrue(r.endswith('-->'), r)

View file

@ -2,13 +2,12 @@
Tests for the Crazy Egg template tags and filters. Tests for the Crazy Egg template tags and filters.
""" """
import pytest
from django.http import HttpRequest from django.http import HttpRequest
from django.template import Context from django.template import Context
from django.test.utils import override_settings from django.test.utils import override_settings
from utils import TagTestCase
from analytical.templatetags.crazy_egg import CrazyEggNode from analytical.templatetags.crazy_egg import CrazyEggNode
from analytical.tests.utils import TagTestCase
from analytical.utils import AnalyticalException from analytical.utils import AnalyticalException
@ -20,27 +19,25 @@ class CrazyEggTagTestCase(TagTestCase):
def test_tag(self): def test_tag(self):
r = self.render_tag('crazy_egg', 'crazy_egg') r = self.render_tag('crazy_egg', 'crazy_egg')
assert '/1234/5678.js' in r self.assertTrue('/1234/5678.js' in r, r)
def test_node(self): def test_node(self):
r = CrazyEggNode().render(Context()) r = CrazyEggNode().render(Context())
assert '/1234/5678.js' in r self.assertTrue('/1234/5678.js' in r, r)
@override_settings(CRAZY_EGG_ACCOUNT_NUMBER=None) @override_settings(CRAZY_EGG_ACCOUNT_NUMBER=None)
def test_no_account_number(self): def test_no_account_number(self):
with pytest.raises(AnalyticalException): self.assertRaises(AnalyticalException, CrazyEggNode)
CrazyEggNode()
@override_settings(CRAZY_EGG_ACCOUNT_NUMBER='123abc') @override_settings(CRAZY_EGG_ACCOUNT_NUMBER='123abc')
def test_wrong_account_number(self): def test_wrong_account_number(self):
with pytest.raises(AnalyticalException): self.assertRaises(AnalyticalException, CrazyEggNode)
CrazyEggNode()
def test_uservars(self): def test_uservars(self):
context = Context({'crazy_egg_var1': 'foo', 'crazy_egg_var2': 'bar'}) context = Context({'crazy_egg_var1': 'foo', 'crazy_egg_var2': 'bar'})
r = CrazyEggNode().render(context) r = CrazyEggNode().render(context)
assert "CE2.set(1, 'foo');" in r self.assertTrue("CE2.set(1, 'foo');" in r, r)
assert "CE2.set(2, 'bar');" in r self.assertTrue("CE2.set(2, 'bar');" in r, r)
@override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1']) @override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
def test_render_internal_ip(self): def test_render_internal_ip(self):
@ -48,5 +45,6 @@ class CrazyEggTagTestCase(TagTestCase):
req.META['REMOTE_ADDR'] = '1.1.1.1' req.META['REMOTE_ADDR'] = '1.1.1.1'
context = Context({'request': req}) context = Context({'request': req})
r = CrazyEggNode().render(context) r = CrazyEggNode().render(context)
assert r.startswith('<!-- Crazy Egg disabled on internal IP address') self.assertTrue(r.startswith(
assert r.endswith('-->') '<!-- Crazy Egg disabled on internal IP address'), r)
self.assertTrue(r.endswith('-->'), r)

View file

@ -2,13 +2,12 @@
Tests for the Gauges template tags and filters. Tests for the Gauges template tags and filters.
""" """
import pytest
from django.http import HttpRequest from django.http import HttpRequest
from django.template import Context from django.template import Context
from django.test.utils import override_settings from django.test.utils import override_settings
from utils import TagTestCase
from analytical.templatetags.gauges import GaugesNode from analytical.templatetags.gauges import GaugesNode
from analytical.tests.utils import TagTestCase
from analytical.utils import AnalyticalException from analytical.utils import AnalyticalException
@ -19,10 +18,8 @@ class GaugesTagTestCase(TagTestCase):
""" """
def test_tag(self): def test_tag(self):
assert ( self.assertEqual("""
self.render_tag('gauges', 'gauges') <script type="text/javascript">
== """
<script>
var _gauges = _gauges || []; var _gauges = _gauges || [];
(function() { (function() {
var t = document.createElement('script'); var t = document.createElement('script');
@ -35,14 +32,13 @@ class GaugesTagTestCase(TagTestCase):
s.parentNode.insertBefore(t, s); s.parentNode.insertBefore(t, s);
})(); })();
</script> </script>
""" """,
) self.render_tag('gauges', 'gauges'))
def test_node(self): def test_node(self):
assert ( self.assertEqual(
GaugesNode().render(Context()) """
== """ <script type="text/javascript">
<script>
var _gauges = _gauges || []; var _gauges = _gauges || [];
(function() { (function() {
var t = document.createElement('script'); var t = document.createElement('script');
@ -55,13 +51,12 @@ class GaugesTagTestCase(TagTestCase):
s.parentNode.insertBefore(t, s); s.parentNode.insertBefore(t, s);
})(); })();
</script> </script>
""" """,
) GaugesNode().render(Context()))
@override_settings(GAUGES_SITE_ID=None) @override_settings(GAUGES_SITE_ID=None)
def test_no_account_number(self): def test_no_account_number(self):
with pytest.raises(AnalyticalException): self.assertRaises(AnalyticalException, GaugesNode)
GaugesNode()
@override_settings(GAUGES_SITE_ID='123abQ') @override_settings(GAUGES_SITE_ID='123abQ')
def test_wrong_account_number(self): def test_wrong_account_number(self):
@ -73,5 +68,6 @@ class GaugesTagTestCase(TagTestCase):
req.META['REMOTE_ADDR'] = '1.1.1.1' req.META['REMOTE_ADDR'] = '1.1.1.1'
context = Context({'request': req}) context = Context({'request': req})
r = GaugesNode().render(context) r = GaugesNode().render(context)
assert r.startswith('<!-- Gauges disabled on internal IP address') self.assertTrue(r.startswith(
assert r.endswith('-->') '<!-- Gauges disabled on internal IP address'), r)
self.assertTrue(r.endswith('-->'), r)

View file

@ -0,0 +1,115 @@
"""
Tests for the Google Analytics template tags and filters.
"""
from django.http import HttpRequest
from django.template import Context
from django.test.utils import override_settings
from analytical.templatetags.google_analytics import GoogleAnalyticsNode, \
TRACK_SINGLE_DOMAIN, TRACK_MULTIPLE_DOMAINS, TRACK_MULTIPLE_SUBDOMAINS,\
SCOPE_VISITOR, SCOPE_SESSION, SCOPE_PAGE
from analytical.tests.utils import TestCase, TagTestCase
from analytical.utils import AnalyticalException
@override_settings(GOOGLE_ANALYTICS_PROPERTY_ID='UA-123456-7',
GOOGLE_ANALYTICS_TRACKING_STYLE=TRACK_SINGLE_DOMAIN)
class GoogleAnalyticsTagTestCase(TagTestCase):
"""
Tests for the ``google_analytics`` template tag.
"""
def test_tag(self):
r = self.render_tag('google_analytics', 'google_analytics')
self.assertTrue("_gaq.push(['_setAccount', 'UA-123456-7']);" in r, r)
self.assertTrue("_gaq.push(['_trackPageview']);" in r, r)
def test_node(self):
r = GoogleAnalyticsNode().render(Context())
self.assertTrue("_gaq.push(['_setAccount', 'UA-123456-7']);" in r, r)
self.assertTrue("_gaq.push(['_trackPageview']);" in r, r)
@override_settings(GOOGLE_ANALYTICS_PROPERTY_ID=None)
def test_no_property_id(self):
self.assertRaises(AnalyticalException, GoogleAnalyticsNode)
@override_settings(GOOGLE_ANALYTICS_PROPERTY_ID='wrong')
def test_wrong_property_id(self):
self.assertRaises(AnalyticalException, GoogleAnalyticsNode)
@override_settings(
GOOGLE_ANALYTICS_TRACKING_STYLE=TRACK_MULTIPLE_SUBDOMAINS,
GOOGLE_ANALYTICS_DOMAIN='example.com')
def test_track_multiple_subdomains(self):
r = GoogleAnalyticsNode().render(Context())
self.assertTrue("_gaq.push(['_setDomainName', 'example.com']);" in r, r)
self.assertTrue("_gaq.push(['_setAllowHash', false]);" in r, r)
@override_settings(GOOGLE_ANALYTICS_TRACKING_STYLE=TRACK_MULTIPLE_DOMAINS,
GOOGLE_ANALYTICS_DOMAIN='example.com')
def test_track_multiple_domains(self):
r = GoogleAnalyticsNode().render(Context())
self.assertTrue("_gaq.push(['_setDomainName', 'example.com']);" in r, r)
self.assertTrue("_gaq.push(['_setAllowHash', false]);" in r, r)
self.assertTrue("_gaq.push(['_setAllowLinker', true]);" in r, r)
def test_custom_vars(self):
context = Context({
'google_analytics_var1': ('test1', 'foo'),
'google_analytics_var2': ('test2', 'bar', SCOPE_VISITOR),
'google_analytics_var4': ('test4', 'baz', SCOPE_SESSION),
'google_analytics_var5': ('test5', 'qux', SCOPE_PAGE),
})
r = GoogleAnalyticsNode().render(context)
self.assertTrue("_gaq.push(['_setCustomVar', 1, 'test1', 'foo', 3]);"
in r, r)
self.assertTrue("_gaq.push(['_setCustomVar', 2, 'test2', 'bar', 1]);"
in r, r)
self.assertTrue("_gaq.push(['_setCustomVar', 4, 'test4', 'baz', 2]);"
in r, r)
self.assertTrue("_gaq.push(['_setCustomVar', 5, 'test5', 'qux', 3]);"
in r, r)
@override_settings(GOOGLE_ANALYTICS_SITE_SPEED=True)
def test_track_page_load_time(self):
r = GoogleAnalyticsNode().render(Context())
self.assertTrue("_gaq.push(['_trackPageLoadTime']);" in r, r)
def test_display_advertising(self):
with override_settings(GOOGLE_ANALYTICS_DISPLAY_ADVERTISING=False):
r = GoogleAnalyticsNode().render(Context())
self.assertTrue("google-analytics.com/ga.js" in r, r)
with override_settings(GOOGLE_ANALYTICS_DISPLAY_ADVERTISING=True):
r = GoogleAnalyticsNode().render(Context())
self.assertTrue("stats.g.doubleclick.net/dc.js" in r, r)
@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 = GoogleAnalyticsNode().render(context)
self.assertTrue(r.startswith(
'<!-- Google Analytics disabled on internal IP address'), r)
self.assertTrue(r.endswith('-->'), r)
@override_settings(GOOGLE_ANALYTICS_ANONYMIZE_IP=True)
def test_anonymize_ip(self):
r = GoogleAnalyticsNode().render(Context())
self.assertTrue("_gaq.push (['_gat._anonymizeIp']);" in r, r)
@override_settings(GOOGLE_ANALYTICS_ANONYMIZE_IP=False)
def test_anonymize_ip_not_present(self):
r = GoogleAnalyticsNode().render(Context())
self.assertFalse("_gaq.push (['_gat._anonymizeIp']);" in r, r)
@override_settings(GOOGLE_ANALYTICS_PROPERTY_ID='UA-123456-7',
GOOGLE_ANALYTICS_TRACKING_STYLE=TRACK_MULTIPLE_DOMAINS,
GOOGLE_ANALYTICS_DOMAIN=None,
ANALYTICAL_DOMAIN=None)
class NoDomainTestCase(TestCase):
def test_exception_without_domain(self):
context = Context()
self.assertRaises(AnalyticalException, GoogleAnalyticsNode().render,
context)

View file

@ -2,14 +2,14 @@
Tests for the GoSquared template tags and filters. Tests for the GoSquared template tags and filters.
""" """
import pytest from django.contrib.auth.models import User, AnonymousUser
from django.contrib.auth.models import AnonymousUser, User
from django.http import HttpRequest from django.http import HttpRequest
from django.template import Context from django.template import Context
from django.test.utils import override_settings from django.test.utils import override_settings
from utils import TagTestCase
from analytical.templatetags.gosquared import GoSquaredNode from analytical.templatetags.gosquared import GoSquaredNode
from analytical.tests import override_settings
from analytical.tests.utils import TagTestCase
from analytical.utils import AnalyticalException from analytical.utils import AnalyticalException
@ -21,49 +21,38 @@ class GoSquaredTagTestCase(TagTestCase):
def test_tag(self): def test_tag(self):
r = self.render_tag('gosquared', 'gosquared') r = self.render_tag('gosquared', 'gosquared')
assert 'GoSquared.acct = "ABC-123456-D";' in r self.assertTrue('GoSquared.acct = "ABC-123456-D";' in r, r)
def test_node(self): def test_node(self):
r = GoSquaredNode().render(Context({})) r = GoSquaredNode().render(Context({}))
assert 'GoSquared.acct = "ABC-123456-D";' in r self.assertTrue('GoSquared.acct = "ABC-123456-D";' in r, r)
@override_settings(GOSQUARED_SITE_TOKEN=None) @override_settings(GOSQUARED_SITE_TOKEN=None)
def test_no_token(self): def test_no_token(self):
with pytest.raises(AnalyticalException): self.assertRaises(AnalyticalException, GoSquaredNode)
GoSquaredNode()
@override_settings(GOSQUARED_SITE_TOKEN='this is not a token') @override_settings(GOSQUARED_SITE_TOKEN='this is not a token')
def test_wrong_token(self): def test_wrong_token(self):
with pytest.raises(AnalyticalException): self.assertRaises(AnalyticalException, GoSquaredNode)
GoSquaredNode()
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True) @override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
def test_auto_identify(self): def test_auto_identify(self):
r = GoSquaredNode().render( r = GoSquaredNode().render(Context({'user': User(username='test',
Context( first_name='Test', last_name='User')}))
{ self.assertTrue('GoSquared.UserName = "Test User";' in r, r)
'user': User(username='test', first_name='Test', last_name='User'),
}
)
)
assert 'GoSquared.UserName = "Test User";' in r
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True) @override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
def test_manual_identify(self): def test_manual_identify(self):
r = GoSquaredNode().render( r = GoSquaredNode().render(Context({
Context( 'user': User(username='test', first_name='Test', last_name='User'),
{ 'gosquared_identity': 'test_identity',
'user': User(username='test', first_name='Test', last_name='User'), }))
'gosquared_identity': 'test_identity', self.assertTrue('GoSquared.UserName = "test_identity";' in r, r)
}
)
)
assert 'GoSquared.UserName = "test_identity";' in r
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True) @override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
def test_identify_anonymous_user(self): def test_identify_anonymous_user(self):
r = GoSquaredNode().render(Context({'user': AnonymousUser()})) r = GoSquaredNode().render(Context({'user': AnonymousUser()}))
assert 'GoSquared.UserName = ' not in r self.assertFalse('GoSquared.UserName = ' in r, r)
@override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1']) @override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
def test_render_internal_ip(self): def test_render_internal_ip(self):
@ -71,5 +60,6 @@ class GoSquaredTagTestCase(TagTestCase):
req.META['REMOTE_ADDR'] = '1.1.1.1' req.META['REMOTE_ADDR'] = '1.1.1.1'
context = Context({'request': req}) context = Context({'request': req})
r = GoSquaredNode().render(context) r = GoSquaredNode().render(context)
assert r.startswith('<!-- GoSquared disabled on internal IP address') self.assertTrue(r.startswith(
assert r.endswith('-->') '<!-- GoSquared disabled on internal IP address'), r)
self.assertTrue(r.endswith('-->'), r)

View file

@ -2,13 +2,12 @@
Tests for the HubSpot template tags and filters. Tests for the HubSpot template tags and filters.
""" """
import pytest
from django.http import HttpRequest from django.http import HttpRequest
from django.template import Context from django.template import Context
from django.test.utils import override_settings from django.test.utils import override_settings
from utils import TagTestCase
from analytical.templatetags.hubspot import HubSpotNode from analytical.templatetags.hubspot import HubSpotNode
from analytical.tests.utils import TagTestCase
from analytical.utils import AnalyticalException from analytical.utils import AnalyticalException
@ -20,27 +19,21 @@ class HubSpotTagTestCase(TagTestCase):
def test_tag(self): def test_tag(self):
r = self.render_tag('hubspot', 'hubspot') r = self.render_tag('hubspot', 'hubspot')
assert ( self.assertTrue("n.id=i;n.src='//js.hs-analytics.net/analytics/'+(Math.ceil(new Date()/r)*r)+'/1234.js';"
"n.id=i;n.src='//js.hs-analytics.net/analytics/'" in r, r)
"+(Math.ceil(new Date()/r)*r)+'/1234.js';"
) in r
def test_node(self): def test_node(self):
r = HubSpotNode().render(Context()) r = HubSpotNode().render(Context())
assert ( self.assertTrue("n.id=i;n.src='//js.hs-analytics.net/analytics/'+(Math.ceil(new Date()/r)*r)+'/1234.js';"
"n.id=i;n.src='//js.hs-analytics.net/analytics/'" in r, r)
"+(Math.ceil(new Date()/r)*r)+'/1234.js';"
) in r
@override_settings(HUBSPOT_PORTAL_ID=None) @override_settings(HUBSPOT_PORTAL_ID=None)
def test_no_portal_id(self): def test_no_portal_id(self):
with pytest.raises(AnalyticalException): self.assertRaises(AnalyticalException, HubSpotNode)
HubSpotNode()
@override_settings(HUBSPOT_PORTAL_ID='wrong') @override_settings(HUBSPOT_PORTAL_ID='wrong')
def test_wrong_portal_id(self): def test_wrong_portal_id(self):
with pytest.raises(AnalyticalException): self.assertRaises(AnalyticalException, HubSpotNode)
HubSpotNode()
@override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1']) @override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
def test_render_internal_ip(self): def test_render_internal_ip(self):
@ -48,5 +41,5 @@ class HubSpotTagTestCase(TagTestCase):
req.META['REMOTE_ADDR'] = '1.1.1.1' req.META['REMOTE_ADDR'] = '1.1.1.1'
context = Context({'request': req}) context = Context({'request': req})
r = HubSpotNode().render(context) r = HubSpotNode().render(context)
assert r.startswith('<!-- HubSpot disabled on internal IP address') self.assertTrue(r.startswith('<!-- HubSpot disabled on internal IP address'), r)
assert r.endswith('-->') self.assertTrue(r.endswith('-->'), r)

View file

@ -0,0 +1,94 @@
"""
Tests for the intercom template tags and filters.
"""
import datetime
from django.contrib.auth.models import User, AnonymousUser
from django.template import Context
from django.test.utils import override_settings
from analytical.templatetags.intercom import IntercomNode
from analytical.tests.utils import TagTestCase
from analytical.utils import AnalyticalException
@override_settings(INTERCOM_APP_ID='1234567890abcdef0123456789')
class IntercomTagTestCase(TagTestCase):
"""
Tests for the ``intercom`` template tag.
"""
def test_tag(self):
rendered_tag = self.render_tag('intercom', 'intercom')
self.assertTrue(rendered_tag.startswith('<!-- Intercom disabled on internal IP address'))
def test_node(self):
now = datetime.datetime(2014, 4, 9, 15, 15, 0)
rendered_tag = IntercomNode().render(Context({
'user': User(
username='test',
first_name='Firstname',
last_name='Lastname',
email="test@example.com",
date_joined=now)
}))
# Because the json isn't predictably ordered, we can't just test the whole thing verbatim.
self.assertEqual("""
<script id="IntercomSettingsScriptTag">
window.intercomSettings = {"app_id": "1234567890abcdef0123456789", "created_at": 1397074500, "email": "test@example.com", "name": "Firstname Lastname"};
</script>
<script>(function(){var w=window;var ic=w.Intercom;if(typeof ic==="function"){ic('reattach_activator');ic('update',intercomSettings);}else{var d=document;var i=function(){i.c(arguments)};i.q=[];i.c=function(args){i.q.push(args)};w.Intercom=i;function l(){var s=d.createElement('script');s.type='text/javascript';s.async=true;s.src='https://static.intercomcdn.com/intercom.v1.js';var x=d.getElementsByTagName('script')[0];x.parentNode.insertBefore(s,x);}if(w.attachEvent){w.attachEvent('onload',l);}else{w.addEventListener('load',l,false);}}})()</script>
""", rendered_tag)
@override_settings(INTERCOM_APP_ID=None)
def test_no_account_number(self):
self.assertRaises(AnalyticalException, IntercomNode)
@override_settings(INTERCOM_APP_ID='123abQ')
def test_wrong_account_number(self):
self.assertRaises(AnalyticalException, IntercomNode)
def test_identify_name_email_and_created_at(self):
now = datetime.datetime(2014, 4, 9, 15, 15, 0)
r = IntercomNode().render(Context({'user': User(username='test',
first_name='Firstname', last_name='Lastname',
email="test@example.com", date_joined=now)}))
self.assertTrue(
"""window.intercomSettings = {"app_id": "1234567890abcdef0123456789", "created_at": 1397074500, "email": "test@example.com", "name": "Firstname Lastname"};"""\
in r
)
def test_custom(self):
r = IntercomNode().render(Context({
'intercom_var1': 'val1',
'intercom_var2': 'val2'
}))
self.assertTrue('var1": "val1", "var2": "val2"' in r)
def test_identify_name_and_email(self):
r = IntercomNode().render(Context({
'user': User(username='test',
first_name='Firstname',
last_name='Lastname',
email="test@example.com")
}))
self.assertTrue('"email": "test@example.com", "name": "Firstname Lastname"' in r)
def test_identify_username_no_email(self):
r = IntercomNode().render(Context({'user': User(username='test')}))
self.assertTrue('"name": "test"' in r, r)
def test_no_identify_when_explicit_name(self):
r = IntercomNode().render(Context({'intercom_name': 'explicit',
'user': User(username='implicit')}))
self.assertTrue('"name": "explicit"' in r, r)
def test_no_identify_when_explicit_email(self):
r = IntercomNode().render(Context({'intercom_email': 'explicit',
'user': User(username='implicit')}))
self.assertTrue('"email": "explicit"' in r, r)
def test_disable_for_anonymous_users(self):
r = IntercomNode().render(Context({'user': AnonymousUser()}))
self.assertTrue(r.startswith('<!-- Intercom disabled on internal IP address'), r)

View file

@ -2,17 +2,17 @@
Tests for the KISSinsights template tags and filters. Tests for the KISSinsights template tags and filters.
""" """
import pytest from django.contrib.auth.models import User, AnonymousUser
from django.contrib.auth.models import AnonymousUser, User
from django.template import Context from django.template import Context
from django.test.utils import override_settings from django.test.utils import override_settings
from utils import TagTestCase
from analytical.templatetags.kiss_insights import KissInsightsNode from analytical.templatetags.kiss_insights import KissInsightsNode
from analytical.tests.utils import TagTestCase
from analytical.utils import AnalyticalException from analytical.utils import AnalyticalException
@override_settings(KISS_INSIGHTS_ACCOUNT_NUMBER='12345', KISS_INSIGHTS_SITE_CODE='abc') @override_settings(KISS_INSIGHTS_ACCOUNT_NUMBER='12345',
KISS_INSIGHTS_SITE_CODE='abc')
class KissInsightsTagTestCase(TagTestCase): class KissInsightsTagTestCase(TagTestCase):
""" """
Tests for the ``kiss_insights`` template tag. Tests for the ``kiss_insights`` template tag.
@ -20,42 +20,39 @@ class KissInsightsTagTestCase(TagTestCase):
def test_tag(self): def test_tag(self):
r = self.render_tag('kiss_insights', 'kiss_insights') r = self.render_tag('kiss_insights', 'kiss_insights')
assert '//s3.amazonaws.com/ki.js/12345/abc.js' in r self.assertTrue("//s3.amazonaws.com/ki.js/12345/abc.js" in r, r)
def test_node(self): def test_node(self):
r = KissInsightsNode().render(Context()) r = KissInsightsNode().render(Context())
assert '//s3.amazonaws.com/ki.js/12345/abc.js' in r self.assertTrue("//s3.amazonaws.com/ki.js/12345/abc.js" in r, r)
@override_settings(KISS_INSIGHTS_ACCOUNT_NUMBER=None) @override_settings(KISS_INSIGHTS_ACCOUNT_NUMBER=None)
def test_no_account_number(self): def test_no_account_number(self):
with pytest.raises(AnalyticalException): self.assertRaises(AnalyticalException, KissInsightsNode)
KissInsightsNode()
@override_settings(KISS_INSIGHTS_SITE_CODE=None) @override_settings(KISS_INSIGHTS_SITE_CODE=None)
def test_no_site_code(self): def test_no_site_code(self):
with pytest.raises(AnalyticalException): self.assertRaises(AnalyticalException, KissInsightsNode)
KissInsightsNode()
@override_settings(KISS_INSIGHTS_ACCOUNT_NUMBER='abcde') @override_settings(KISS_INSIGHTS_ACCOUNT_NUMBER='abcde')
def test_wrong_account_number(self): def test_wrong_account_number(self):
with pytest.raises(AnalyticalException): self.assertRaises(AnalyticalException, KissInsightsNode)
KissInsightsNode()
@override_settings(KISS_INSIGHTS_SITE_CODE='abc def') @override_settings(KISS_INSIGHTS_SITE_CODE='abc def')
def test_wrong_site_id(self): def test_wrong_site_id(self):
with pytest.raises(AnalyticalException): self.assertRaises(AnalyticalException, KissInsightsNode)
KissInsightsNode()
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True) @override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
def test_identify(self): def test_identify(self):
r = KissInsightsNode().render(Context({'user': User(username='test')})) r = KissInsightsNode().render(Context({'user': User(username='test')}))
assert "_kiq.push(['identify', 'test']);" in r self.assertTrue("_kiq.push(['identify', 'test']);" in r, r)
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True) @override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
def test_identify_anonymous_user(self): def test_identify_anonymous_user(self):
r = KissInsightsNode().render(Context({'user': AnonymousUser()})) r = KissInsightsNode().render(Context({'user': AnonymousUser()}))
assert "_kiq.push(['identify', " not in r self.assertFalse("_kiq.push(['identify', " in r, r)
def test_show_survey(self): def test_show_survey(self):
r = KissInsightsNode().render(Context({'kiss_insights_show_survey': 1234})) r = KissInsightsNode().render(
assert "_kiq.push(['showSurvey', 1234]);" in r Context({'kiss_insights_show_survey': 1234}))
self.assertTrue("_kiq.push(['showSurvey', 1234]);" in r, r)

View file

@ -0,0 +1,82 @@
"""
Tests for the KISSmetrics tags and filters.
"""
from django.contrib.auth.models import User, AnonymousUser
from django.http import HttpRequest
from django.template import Context
from django.test.utils import override_settings
from analytical.templatetags.kiss_metrics import KissMetricsNode
from analytical.tests.utils import TagTestCase
from analytical.utils import AnalyticalException
@override_settings(KISS_METRICS_API_KEY='0123456789abcdef0123456789abcdef'
'01234567')
class KissMetricsTagTestCase(TagTestCase):
"""
Tests for the ``kiss_metrics`` template tag.
"""
def test_tag(self):
r = self.render_tag('kiss_metrics', 'kiss_metrics')
self.assertTrue("//doug1izaerwt3.cloudfront.net/0123456789abcdef012345"
"6789abcdef01234567.1.js" in r, r)
def test_node(self):
r = KissMetricsNode().render(Context())
self.assertTrue("//doug1izaerwt3.cloudfront.net/0123456789abcdef012345"
"6789abcdef01234567.1.js" in r, r)
@override_settings(KISS_METRICS_API_KEY=None)
def test_no_api_key(self):
self.assertRaises(AnalyticalException, KissMetricsNode)
@override_settings(KISS_METRICS_API_KEY='0123456789abcdef0123456789abcdef'
'0123456')
def test_api_key_too_short(self):
self.assertRaises(AnalyticalException, KissMetricsNode)
@override_settings(KISS_METRICS_API_KEY='0123456789abcdef0123456789abcdef'
'012345678')
def test_api_key_too_long(self):
self.assertRaises(AnalyticalException, KissMetricsNode)
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
def test_identify(self):
r = KissMetricsNode().render(Context({'user': User(username='test')}))
self.assertTrue("_kmq.push(['identify', 'test']);" in r, r)
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
def test_identify_anonymous_user(self):
r = KissMetricsNode().render(Context({'user': AnonymousUser()}))
self.assertFalse("_kmq.push(['identify', " in r, r)
def test_event(self):
r = KissMetricsNode().render(Context({'kiss_metrics_event':
('test_event', {'prop1': 'val1', 'prop2': 'val2'})}))
self.assertTrue("_kmq.push(['record', 'test_event', "
'{"prop1": "val1", "prop2": "val2"}]);' in r, r)
def test_property(self):
r = KissMetricsNode().render(Context({'kiss_metrics_properties':
{'prop1': 'val1', 'prop2': 'val2'}}))
self.assertTrue("_kmq.push([\'set\', "
'{"prop1": "val1", "prop2": "val2"}]);' in r, r)
def test_alias(self):
r = KissMetricsNode().render(Context({'kiss_metrics_alias':
{'test': 'test_alias'}}))
self.assertTrue("_kmq.push(['alias', 'test', 'test_alias']);" in r,r)
@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 = KissMetricsNode().render(context)
self.assertTrue(r.startswith(
'<!-- KISSmetrics disabled on internal IP address'), r)
self.assertTrue(r.endswith('-->'), r)

View file

@ -2,14 +2,13 @@
Tests for the Mixpanel tags and filters. Tests for the Mixpanel tags and filters.
""" """
import pytest from django.contrib.auth.models import User, AnonymousUser
from django.contrib.auth.models import AnonymousUser, User
from django.http import HttpRequest from django.http import HttpRequest
from django.template import Context from django.template import Context
from django.test.utils import override_settings from django.test.utils import override_settings
from utils import TagTestCase
from analytical.templatetags.mixpanel import MixpanelNode from analytical.templatetags.mixpanel import MixpanelNode
from analytical.tests.utils import TagTestCase
from analytical.utils import AnalyticalException from analytical.utils import AnalyticalException
@ -21,50 +20,43 @@ class MixpanelTagTestCase(TagTestCase):
def test_tag(self): def test_tag(self):
r = self.render_tag('mixpanel', 'mixpanel') r = self.render_tag('mixpanel', 'mixpanel')
assert "mixpanel.init('0123456789abcdef0123456789abcdef');" in r self.assertTrue(
"mixpanel.init('0123456789abcdef0123456789abcdef');" in r,
r)
def test_node(self): def test_node(self):
r = MixpanelNode().render(Context()) r = MixpanelNode().render(Context())
assert "mixpanel.init('0123456789abcdef0123456789abcdef');" in r self.assertTrue(
"mixpanel.init('0123456789abcdef0123456789abcdef');" in r,
r)
@override_settings(MIXPANEL_API_TOKEN=None) @override_settings(MIXPANEL_API_TOKEN=None)
def test_no_token(self): def test_no_token(self):
with pytest.raises(AnalyticalException): self.assertRaises(AnalyticalException, MixpanelNode)
MixpanelNode()
@override_settings(MIXPANEL_API_TOKEN='0123456789abcdef0123456789abcdef0') @override_settings(MIXPANEL_API_TOKEN='0123456789abcdef0123456789abcdef0')
def test_token_too_long(self): def test_token_too_long(self):
with pytest.raises(AnalyticalException): self.assertRaises(AnalyticalException, MixpanelNode)
MixpanelNode()
@override_settings(MIXPANEL_API_TOKEN='0123456789abcdef0123456789abcde') @override_settings(MIXPANEL_API_TOKEN='0123456789abcdef0123456789abcde')
def test_token_too_short(self): def test_token_too_short(self):
with pytest.raises(AnalyticalException): self.assertRaises(AnalyticalException, MixpanelNode)
MixpanelNode()
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True) @override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
def test_identify(self): def test_identify(self):
r = MixpanelNode().render(Context({'user': User(username='test')})) r = MixpanelNode().render(Context({'user': User(username='test')}))
assert "mixpanel.identify('test');" in r self.assertIn("mixpanel.identify('test');", r)
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True) @override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
def test_identify_anonymous_user(self): def test_identify_anonymous_user(self):
r = MixpanelNode().render(Context({'user': AnonymousUser()})) r = MixpanelNode().render(Context({'user': AnonymousUser()}))
assert 'mixpanel.register_once({distinct_id:' not in r self.assertFalse("mixpanel.register_once({distinct_id:" in r, r)
def test_event(self): def test_event(self):
r = MixpanelNode().render( r = MixpanelNode().render(Context({'mixpanel_event':
Context( ('test_event', {'prop1': 'val1', 'prop2': 'val2'})}))
{ self.assertTrue("mixpanel.track('test_event', "
'mixpanel_event': ( '{"prop1": "val1", "prop2": "val2"});' in r, r)
'test_event',
{'prop1': 'val1', 'prop2': 'val2'},
),
}
)
)
assert "mixpanel.track('test_event', "
'{"prop1": "val1", "prop2": "val2"});' in r
@override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1']) @override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
def test_render_internal_ip(self): def test_render_internal_ip(self):
@ -72,5 +64,6 @@ class MixpanelTagTestCase(TagTestCase):
req.META['REMOTE_ADDR'] = '1.1.1.1' req.META['REMOTE_ADDR'] = '1.1.1.1'
context = Context({'request': req}) context = Context({'request': req})
r = MixpanelNode().render(context) r = MixpanelNode().render(context)
assert r.startswith('<!-- Mixpanel disabled on internal IP address') self.assertTrue(r.startswith(
assert r.endswith('-->') '<!-- Mixpanel disabled on internal IP address'), r)
self.assertTrue(r.endswith('-->'), r)

View file

@ -0,0 +1,92 @@
"""
Tests for the Olark template tags and filters.
"""
from django.contrib.auth.models import User, AnonymousUser
from django.template import Context
from django.test.utils import override_settings
from analytical.templatetags.olark import OlarkNode
from analytical.tests.utils import TagTestCase
from analytical.utils import AnalyticalException
@override_settings(OLARK_SITE_ID='1234-567-89-0123')
class OlarkTestCase(TagTestCase):
"""
Tests for the ``olark`` template tag.
"""
def test_tag(self):
r = self.render_tag('olark', 'olark')
self.assertTrue("olark.identify('1234-567-89-0123');" in r, r)
def test_node(self):
r = OlarkNode().render(Context())
self.assertTrue("olark.identify('1234-567-89-0123');" in r, r)
@override_settings(OLARK_SITE_ID=None)
def test_no_site_id(self):
self.assertRaises(AnalyticalException, OlarkNode)
@override_settings(OLARK_SITE_ID='1234-567-8901234')
def test_wrong_site_id(self):
self.assertRaises(AnalyticalException, OlarkNode)
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
def test_identify(self):
r = OlarkNode().render(Context({'user':
User(username='test', first_name='Test', last_name='User')}))
self.assertTrue("olark('api.chat.updateVisitorNickname', "
"{snippet: 'Test User (test)'});" in r, r)
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
def test_identify_anonymous_user(self):
r = OlarkNode().render(Context({'user': AnonymousUser()}))
self.assertFalse("olark('api.chat.updateVisitorNickname', " in r, r)
def test_nickname(self):
r = OlarkNode().render(Context({'olark_nickname': 'testnick'}))
self.assertTrue("olark('api.chat.updateVisitorNickname', "
"{snippet: 'testnick'});" in r, r)
def test_status_string(self):
r = OlarkNode().render(Context({'olark_status': 'teststatus'}))
self.assertTrue("olark('api.chat.updateVisitorStatus', "
'{snippet: "teststatus"});' in r, r)
def test_status_string_list(self):
r = OlarkNode().render(Context({'olark_status':
['teststatus1', 'teststatus2']}))
self.assertTrue("olark('api.chat.updateVisitorStatus', "
'{snippet: ["teststatus1", "teststatus2"]});' in r, r)
def test_messages(self):
messages = [
"welcome_title",
"chatting_title",
"unavailable_title",
"busy_title",
"away_message",
"loading_title",
"welcome_message",
"busy_message",
"chat_input_text",
"name_input_text",
"email_input_text",
"offline_note_message",
"send_button_text",
"offline_note_thankyou_text",
"offline_note_error_text",
"offline_note_sending_text",
"operator_is_typing_text",
"operator_has_stopped_typing_text",
"introduction_error_text",
"introduction_messages",
"introduction_submit_button_text",
]
vars = dict(('olark_%s' % m, m) for m in messages)
r = OlarkNode().render(Context(vars))
for m in messages:
self.assertTrue("olark.configure('locale.%s', \"%s\");" % (m, m)
in r, r)

View file

@ -2,13 +2,12 @@
Tests for the Optimizely template tags and filters. Tests for the Optimizely template tags and filters.
""" """
import pytest
from django.http import HttpRequest from django.http import HttpRequest
from django.template import Context from django.template import Context
from django.test.utils import override_settings from django.test.utils import override_settings
from utils import TagTestCase
from analytical.templatetags.optimizely import OptimizelyNode from analytical.templatetags.optimizely import OptimizelyNode
from analytical.tests.utils import TagTestCase
from analytical.utils import AnalyticalException from analytical.utils import AnalyticalException
@ -19,22 +18,22 @@ class OptimizelyTagTestCase(TagTestCase):
""" """
def test_tag(self): def test_tag(self):
expected = '<script src="//cdn.optimizely.com/js/1234567.js"></script>' self.assertEqual(
assert self.render_tag('optimizely', 'optimizely') == expected '<script src="//cdn.optimizely.com/js/1234567.js"></script>',
self.render_tag('optimizely', 'optimizely'))
def test_node(self): def test_node(self):
expected = '<script src="//cdn.optimizely.com/js/1234567.js"></script>' self.assertEqual(
assert OptimizelyNode().render(Context()) == expected '<script src="//cdn.optimizely.com/js/1234567.js"></script>',
OptimizelyNode().render(Context()))
@override_settings(OPTIMIZELY_ACCOUNT_NUMBER=None) @override_settings(OPTIMIZELY_ACCOUNT_NUMBER=None)
def test_no_account_number(self): def test_no_account_number(self):
with pytest.raises(AnalyticalException): self.assertRaises(AnalyticalException, OptimizelyNode)
OptimizelyNode()
@override_settings(OPTIMIZELY_ACCOUNT_NUMBER='123abc') @override_settings(OPTIMIZELY_ACCOUNT_NUMBER='123abc')
def test_wrong_account_number(self): def test_wrong_account_number(self):
with pytest.raises(AnalyticalException): self.assertRaises(AnalyticalException, OptimizelyNode)
OptimizelyNode()
@override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1']) @override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
def test_render_internal_ip(self): def test_render_internal_ip(self):
@ -42,5 +41,6 @@ class OptimizelyTagTestCase(TagTestCase):
req.META['REMOTE_ADDR'] = '1.1.1.1' req.META['REMOTE_ADDR'] = '1.1.1.1'
context = Context({'request': req}) context = Context({'request': req})
r = OptimizelyNode().render(context) r = OptimizelyNode().render(context)
assert r.startswith('<!-- Optimizely disabled on internal IP address') self.assertTrue(r.startswith(
assert r.endswith('-->') '<!-- Optimizely disabled on internal IP address'), r)
self.assertTrue(r.endswith('-->'), r)

View file

@ -2,14 +2,13 @@
Tests for the Performable template tags and filters. Tests for the Performable template tags and filters.
""" """
import pytest from django.contrib.auth.models import User, AnonymousUser
from django.contrib.auth.models import AnonymousUser, User
from django.http import HttpRequest from django.http import HttpRequest
from django.template import Context from django.template import Context
from django.test.utils import override_settings from django.test.utils import override_settings
from utils import TagTestCase
from analytical.templatetags.performable import PerformableNode from analytical.templatetags.performable import PerformableNode
from analytical.tests.utils import TagTestCase
from analytical.utils import AnalyticalException from analytical.utils import AnalyticalException
@ -21,21 +20,19 @@ class PerformableTagTestCase(TagTestCase):
def test_tag(self): def test_tag(self):
r = self.render_tag('performable', 'performable') r = self.render_tag('performable', 'performable')
assert '/performable/pax/123ABC.js' in r self.assertTrue('/performable/pax/123ABC.js' in r, r)
def test_node(self): def test_node(self):
r = PerformableNode().render(Context()) r = PerformableNode().render(Context())
assert '/performable/pax/123ABC.js' in r self.assertTrue('/performable/pax/123ABC.js' in r, r)
@override_settings(PERFORMABLE_API_KEY=None) @override_settings(PERFORMABLE_API_KEY=None)
def test_no_api_key(self): def test_no_api_key(self):
with pytest.raises(AnalyticalException): self.assertRaises(AnalyticalException, PerformableNode)
PerformableNode()
@override_settings(PERFORMABLE_API_KEY='123 ABC') @override_settings(PERFORMABLE_API_KEY='123 ABC')
def test_wrong_account_number(self): def test_wrong_account_number(self):
with pytest.raises(AnalyticalException): self.assertRaises(AnalyticalException, PerformableNode)
PerformableNode()
@override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1']) @override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
def test_render_internal_ip(self): def test_render_internal_ip(self):
@ -43,18 +40,19 @@ class PerformableTagTestCase(TagTestCase):
req.META['REMOTE_ADDR'] = '1.1.1.1' req.META['REMOTE_ADDR'] = '1.1.1.1'
context = Context({'request': req}) context = Context({'request': req})
r = PerformableNode().render(context) r = PerformableNode().render(context)
assert r.startswith('<!-- Performable disabled on internal IP address') self.assertTrue(r.startswith(
assert r.endswith('-->') '<!-- Performable disabled on internal IP address'), r)
self.assertTrue(r.endswith('-->'), r)
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True) @override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
def test_identify(self): def test_identify(self):
r = PerformableNode().render(Context({'user': User(username='test')})) r = PerformableNode().render(Context({'user': User(username='test')}))
assert '_paq.push(["identify", {identity: "test"}]);' in r self.assertTrue('_paq.push(["identify", {identity: "test"}]);' in r, r)
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True) @override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
def test_identify_anonymous_user(self): def test_identify_anonymous_user(self):
r = PerformableNode().render(Context({'user': AnonymousUser()})) r = PerformableNode().render(Context({'user': AnonymousUser()}))
assert '_paq.push(["identify", ' not in r self.assertFalse('_paq.push(["identify", ' in r, r)
class PerformableEmbedTagTestCase(TagTestCase): class PerformableEmbedTagTestCase(TagTestCase):
@ -65,5 +63,8 @@ class PerformableEmbedTagTestCase(TagTestCase):
def test_tag(self): def test_tag(self):
domain = 'example.com' domain = 'example.com'
page = 'test' page = 'test'
tag = self.render_tag('performable', f'performable_embed "{domain}" "{page}"') r = self.render_tag('performable', 'performable_embed "%s" "%s"'
assert "$f.initialize({'host': 'example.com', 'page': 'test'});" in tag % (domain, page))
self.assertTrue(
"$f.initialize({'host': 'example.com', 'page': 'test'});" in r,
r)

View file

@ -0,0 +1,120 @@
"""
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(' ? "https" : "http") + "://example.com/";' in r, r)
self.assertTrue("_paq.push(['setSiteId', 345]);" in r, r)
self.assertTrue('img src="http://example.com/piwik.php?idsite=345"'
in r, r)
def test_node(self):
r = PiwikNode().render(Context({}))
self.assertTrue(' ? "https" : "http") + "://example.com/";' in r, r)
self.assertTrue("_paq.push(['setSiteId', 345]);" in r, r)
self.assertTrue('img src="http://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(' ? "https" : "http") + "://example.com/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(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(
'<!-- Piwik disabled on internal IP address'), r)
self.assertTrue(r.endswith('-->'), 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))

View file

@ -0,0 +1,67 @@
"""
Tests for the Reinvigorate template tags and filters.
"""
import re
from django.contrib.auth.models import User, AnonymousUser
from django.http import HttpRequest
from django.template import Context
from django.test.utils import override_settings
from analytical.templatetags.reinvigorate import ReinvigorateNode
from analytical.tests.utils import TagTestCase
from analytical.utils import AnalyticalException
@override_settings(REINVIGORATE_TRACKING_ID='12345-abcdefghij')
class ReinvigorateTagTestCase(TagTestCase):
"""
Tests for the ``reinvigorate`` template tag.
"""
def test_tag(self):
r = self.render_tag('reinvigorate', 'reinvigorate')
self.assertTrue('reinvigorate.track("12345-abcdefghij");' in r, r)
def test_node(self):
r = ReinvigorateNode().render(Context({}))
self.assertTrue('reinvigorate.track("12345-abcdefghij");' in r, r)
@override_settings(REINVIGORATE_TRACKING_ID=None)
def test_no_tracking_id(self):
self.assertRaises(AnalyticalException, ReinvigorateNode)
@override_settings(REINVIGORATE_TRACKING_ID='123abc')
def test_wrong_tracking_id(self):
self.assertRaises(AnalyticalException, ReinvigorateNode)
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
def test_identify(self):
r = ReinvigorateNode().render(Context({'user':
User(username='test', first_name='Test', last_name='User',
email='test@example.com')}))
self.assertTrue('var re_name_tag = "Test User";' in r, r)
self.assertTrue('var re_context_tag = "test@example.com";' in r, r)
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
def test_identify_anonymous_user(self):
r = ReinvigorateNode().render(Context({'user': AnonymousUser()}))
self.assertFalse('var re_name_tag = ' in r, r)
self.assertFalse('var re_context_tag = ' in r, r)
def test_tags(self):
r = ReinvigorateNode().render(Context({'reinvigorate_var1': 'val1',
'reinvigorate_var2': 2}))
self.assertTrue(re.search('var re_var1_tag = "val1";', r), r)
self.assertTrue(re.search('var re_var2_tag = 2;', r), r)
@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 = ReinvigorateNode().render(context)
self.assertTrue(r.startswith(
'<!-- Reinvigorate disabled on internal IP address'), r)
self.assertTrue(r.endswith('-->'), r)

View file

@ -0,0 +1,256 @@
"""
Tests for the SnapEngage template tags and filters.
"""
from django.contrib.auth.models import User, AnonymousUser
from django.template import Context
from django.test.utils import override_settings
from django.utils import translation
from analytical.templatetags.snapengage import SnapEngageNode, \
BUTTON_STYLE_LIVE, BUTTON_STYLE_DEFAULT, BUTTON_STYLE_NONE, \
BUTTON_LOCATION_LEFT, BUTTON_LOCATION_RIGHT, BUTTON_LOCATION_TOP, \
BUTTON_LOCATION_BOTTOM, FORM_POSITION_TOP_LEFT
from analytical.tests.utils import TagTestCase
from analytical.utils import AnalyticalException
WIDGET_ID = 'ec329c69-0bf0-4db8-9b77-3f8150fb977e'
@override_settings(
SNAPENGAGE_WIDGET_ID=WIDGET_ID,
SNAPENGAGE_BUTTON=BUTTON_STYLE_DEFAULT,
SNAPENGAGE_BUTTON_LOCATION=BUTTON_LOCATION_LEFT,
SNAPENGAGE_BUTTON_OFFSET="55%",
)
class SnapEngageTestCase(TagTestCase):
"""
Tests for the ``snapengage`` template tag.
"""
def test_tag(self):
r = self.render_tag('snapengage', 'snapengage')
self.assertTrue(
'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","0",'
'"55%");' in r, r)
def test_node(self):
r = SnapEngageNode().render(Context())
self.assertTrue(
'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","0",'
'"55%");' in r, r)
@override_settings(SNAPENGAGE_WIDGET_ID=None)
def test_no_site_id(self):
self.assertRaises(AnalyticalException, SnapEngageNode)
@override_settings(SNAPENGAGE_WIDGET_ID='abc')
def test_wrong_site_id(self):
self.assertRaises(AnalyticalException, SnapEngageNode)
def test_no_button(self):
r = SnapEngageNode().render(Context({'snapengage_button': BUTTON_STYLE_NONE}))
self.assertTrue('SnapABug.init("ec329c69-0bf0-4db8-9b77-3f8150fb977e")'
in r, r)
with override_settings(SNAPENGAGE_BUTTON=BUTTON_STYLE_NONE):
r = SnapEngageNode().render(Context())
self.assertTrue(
'SnapABug.init("ec329c69-0bf0-4db8-9b77-3f8150fb977e")' in r, r)
def test_live_button(self):
r = SnapEngageNode().render(Context({'snapengage_button': BUTTON_STYLE_LIVE}))
self.assertTrue(
'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","0",'
'"55%",true);' in r, r)
with override_settings(SNAPENGAGE_BUTTON=BUTTON_STYLE_LIVE):
r = SnapEngageNode().render(Context())
self.assertTrue(
'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","0",'
'"55%",true);' in r, r)
def test_custom_button(self):
r = SnapEngageNode().render(Context({
'snapengage_button': "http://www.example.com/button.png"}))
self.assertTrue(
'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","0",'
'"55%");' in r, r)
self.assertTrue(
'SnapABug.setButton("http://www.example.com/button.png");' in r, r)
with override_settings(
SNAPENGAGE_BUTTON="http://www.example.com/button.png"):
r = SnapEngageNode().render(Context())
self.assertTrue(
'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","0",'
'"55%");' in r, r)
self.assertTrue(
'SnapABug.setButton("http://www.example.com/button.png");' in r,
r)
def test_button_location_right(self):
r = SnapEngageNode().render(Context({
'snapengage_button_location': BUTTON_LOCATION_RIGHT}))
self.assertTrue(
'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","1",'
'"55%");' in r, r)
with override_settings(
SNAPENGAGE_BUTTON_LOCATION=BUTTON_LOCATION_RIGHT):
r = SnapEngageNode().render(Context())
self.assertTrue(
'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","1",'
'"55%");' in r, r)
def test_button_location_top(self):
r = SnapEngageNode().render(Context({
'snapengage_button_location': BUTTON_LOCATION_TOP}))
self.assertTrue(
'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","2",'
'"55%");' in r, r)
with override_settings(SNAPENGAGE_BUTTON_LOCATION=BUTTON_LOCATION_TOP):
r = SnapEngageNode().render(Context())
self.assertTrue(
'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","2",'
'"55%");' in r, r)
def test_button_location_bottom(self):
r = SnapEngageNode().render(Context({
'snapengage_button_location': BUTTON_LOCATION_BOTTOM}))
self.assertTrue(
'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","3",'
'"55%");' in r, r)
with override_settings(
SNAPENGAGE_BUTTON_LOCATION=BUTTON_LOCATION_BOTTOM):
r = SnapEngageNode().render(Context())
self.assertTrue(
'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","3",'
'"55%");' in r, r)
def test_button_offset(self):
r = SnapEngageNode().render(Context({
'snapengage_button_location_offset': "30%"}))
self.assertTrue(
'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","0",'
'"30%");' in r, r)
with override_settings(SNAPENGAGE_BUTTON_LOCATION_OFFSET="30%"):
r = SnapEngageNode().render(Context())
self.assertTrue(
'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","0",'
'"30%");' in r, r)
def test_button_effect(self):
r = SnapEngageNode().render(Context({
'snapengage_button_effect': "-4px"}))
self.assertTrue('SnapABug.setButtonEffect("-4px");' in r, r)
with override_settings(SNAPENGAGE_BUTTON_EFFECT="-4px"):
r = SnapEngageNode().render(Context())
self.assertTrue('SnapABug.setButtonEffect("-4px");' in r, r)
def test_form_position(self):
r = SnapEngageNode().render(Context({
'snapengage_form_position': FORM_POSITION_TOP_LEFT}))
self.assertTrue('SnapABug.setChatFormPosition("tl");' in r, r)
with override_settings(SNAPENGAGE_FORM_POSITION=FORM_POSITION_TOP_LEFT):
r = SnapEngageNode().render(Context())
self.assertTrue('SnapABug.setChatFormPosition("tl");' in r, r)
def test_form_top_position(self):
r = SnapEngageNode().render(Context({
'snapengage_form_top_position': 40}))
self.assertTrue('SnapABug.setFormTopPosition(40);' in r, r)
with override_settings(SNAPENGAGE_FORM_TOP_POSITION=40):
r = SnapEngageNode().render(Context())
self.assertTrue('SnapABug.setFormTopPosition(40);' in r, r)
def test_domain(self):
r = SnapEngageNode().render(Context({
'snapengage_domain': "example.com"}))
self.assertTrue('SnapABug.setDomain("example.com");' in r, r)
with override_settings(SNAPENGAGE_DOMAIN="example.com"):
r = SnapEngageNode().render(Context())
self.assertTrue('SnapABug.setDomain("example.com");' in r, r)
def test_secure_connection(self):
r = SnapEngageNode().render(Context({
'snapengage_secure_connection': True}))
self.assertTrue('SnapABug.setSecureConnexion();' in r, r)
with override_settings(SNAPENGAGE_SECURE_CONNECTION=True):
r = SnapEngageNode().render(Context())
self.assertTrue('SnapABug.setSecureConnexion();' in r, r)
def test_show_offline(self):
r = SnapEngageNode().render(Context({'snapengage_show_offline': False}))
self.assertTrue('SnapABug.allowOffline(false);' in r, r)
with override_settings(SNAPENGAGE_SHOW_OFFLINE=False):
r = SnapEngageNode().render(Context())
self.assertTrue('SnapABug.allowOffline(false);' in r, r)
def test_proactive_chat(self):
r = SnapEngageNode().render(Context({
'snapengage_proactive_chat': False}))
self.assertTrue('SnapABug.allowProactiveChat(false);' in r, r)
def test_screenshot(self):
r = SnapEngageNode().render(Context({'snapengage_screenshots': False}))
self.assertTrue('SnapABug.allowScreenshot(false);' in r, r)
with override_settings(SNAPENGAGE_SCREENSHOTS=False):
r = SnapEngageNode().render(Context())
self.assertTrue('SnapABug.allowScreenshot(false);' in r, r)
def test_offline_screenshots(self):
r = SnapEngageNode().render(Context(
{'snapengage_offline_screenshots': False}))
self.assertTrue('SnapABug.showScreenshotOption(false);' in r, r)
with override_settings(SNAPENGAGE_OFFLINE_SCREENSHOTS=False):
r = SnapEngageNode().render(Context())
self.assertTrue('SnapABug.showScreenshotOption(false);' in r, r)
def test_sounds(self):
r = SnapEngageNode().render(Context({'snapengage_sounds': False}))
self.assertTrue('SnapABug.allowChatSound(false);' in r, r)
with override_settings(SNAPENGAGE_SOUNDS=False):
r = SnapEngageNode().render(Context())
self.assertTrue('SnapABug.allowChatSound(false);' in r, r)
@override_settings(SNAPENGAGE_READONLY_EMAIL=False)
def test_email(self):
r = SnapEngageNode().render(Context({'snapengage_email':
'test@example.com'}))
self.assertTrue('SnapABug.setUserEmail("test@example.com");' in r, r)
def test_email_readonly(self):
r = SnapEngageNode().render(Context({'snapengage_email':
'test@example.com', 'snapengage_readonly_email': True}))
self.assertTrue('SnapABug.setUserEmail("test@example.com",true);' in r,
r)
with override_settings(SNAPENGAGE_READONLY_EMAIL=True):
r = SnapEngageNode().render(Context({'snapengage_email':
'test@example.com'}))
self.assertTrue('SnapABug.setUserEmail("test@example.com",true);'
in r, r)
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
def test_identify(self):
r = SnapEngageNode().render(Context({'user':
User(username='test', email='test@example.com')}))
self.assertTrue('SnapABug.setUserEmail("test@example.com");' in r, r)
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
def test_identify_anonymous_user(self):
r = SnapEngageNode().render(Context({'user': AnonymousUser()}))
self.assertFalse('SnapABug.setUserEmail(' in r, r)
def test_language(self):
r = SnapEngageNode().render(Context({'snapengage_locale': 'fr'}))
self.assertTrue('SnapABug.setLocale("fr");' in r, r)
with override_settings(SNAPENGAGE_LOCALE='fr'):
r = SnapEngageNode().render(Context())
self.assertTrue('SnapABug.setLocale("fr");' in r, r)
def test_automatic_language(self):
real_get_language = translation.get_language
try:
translation.get_language = lambda: 'fr-ca'
r = SnapEngageNode().render(Context())
self.assertTrue('SnapABug.setLocale("fr_CA");' in r, r)
finally:
translation.get_language = real_get_language

View file

@ -2,14 +2,15 @@
Tests for the Spring Metrics template tags and filters. Tests for the Spring Metrics template tags and filters.
""" """
import pytest import re
from django.contrib.auth.models import AnonymousUser, User
from django.contrib.auth.models import User, AnonymousUser
from django.http import HttpRequest from django.http import HttpRequest
from django.template import Context from django.template import Context
from django.test.utils import override_settings from django.test.utils import override_settings
from utils import TagTestCase
from analytical.templatetags.spring_metrics import SpringMetricsNode from analytical.templatetags.spring_metrics import SpringMetricsNode
from analytical.tests.utils import TagTestCase
from analytical.utils import AnalyticalException from analytical.utils import AnalyticalException
@ -21,49 +22,39 @@ class SpringMetricsTagTestCase(TagTestCase):
def test_tag(self): def test_tag(self):
r = self.render_tag('spring_metrics', 'spring_metrics') r = self.render_tag('spring_metrics', 'spring_metrics')
assert "_springMetq.push(['id', '12345678']);" in r self.assertTrue("_springMetq.push(['id', '12345678']);" in r, r)
def test_node(self): def test_node(self):
r = SpringMetricsNode().render(Context({})) r = SpringMetricsNode().render(Context({}))
assert "_springMetq.push(['id', '12345678']);" in r self.assertTrue("_springMetq.push(['id', '12345678']);" in r, r)
@override_settings(SPRING_METRICS_TRACKING_ID=None) @override_settings(SPRING_METRICS_TRACKING_ID=None)
def test_no_site_id(self): def test_no_site_id(self):
with pytest.raises(AnalyticalException): self.assertRaises(AnalyticalException, SpringMetricsNode)
SpringMetricsNode()
@override_settings(SPRING_METRICS_TRACKING_ID='123xyz') @override_settings(SPRING_METRICS_TRACKING_ID='123xyz')
def test_wrong_site_id(self): def test_wrong_site_id(self):
with pytest.raises(AnalyticalException): self.assertRaises(AnalyticalException, SpringMetricsNode)
SpringMetricsNode()
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True) @override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
def test_identify(self): def test_identify(self):
r = SpringMetricsNode().render( r = SpringMetricsNode().render(Context({'user':
Context( User(email='test@test.com')}))
{ self.assertTrue("_springMetq.push(['setdata', "
'user': User(email='test@test.com'), "{'email': 'test@test.com'}]);" in r, r)
}
)
)
assert "_springMetq.push(['setdata', {'email': 'test@test.com'}]);" in r
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True) @override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
def test_identify_anonymous_user(self): def test_identify_anonymous_user(self):
r = SpringMetricsNode().render(Context({'user': AnonymousUser()})) r = SpringMetricsNode().render(Context({'user': AnonymousUser()}))
assert "_springMetq.push(['setdata', {'email':" not in r self.assertFalse("_springMetq.push(['setdata', {'email':" in r, r)
def test_custom(self): def test_custom(self):
r = SpringMetricsNode().render( r = SpringMetricsNode().render(Context({'spring_metrics_var1': 'val1',
Context( 'spring_metrics_var2': 'val2'}))
{ self.assertTrue("_springMetq.push(['setdata', {'var1': 'val1'}]);" in r,
'spring_metrics_var1': 'val1', r)
'spring_metrics_var2': 'val2', self.assertTrue("_springMetq.push(['setdata', {'var2': 'val2'}]);" in r,
} r)
)
)
assert "_springMetq.push(['setdata', {'var1': 'val1'}]);" in r
assert "_springMetq.push(['setdata', {'var2': 'val2'}]);" in r
@override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1']) @override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
def test_render_internal_ip(self): def test_render_internal_ip(self):
@ -71,5 +62,6 @@ class SpringMetricsTagTestCase(TagTestCase):
req.META['REMOTE_ADDR'] = '1.1.1.1' req.META['REMOTE_ADDR'] = '1.1.1.1'
context = Context({'request': req}) context = Context({'request': req})
r = SpringMetricsNode().render(context) r = SpringMetricsNode().render(context)
assert r.startswith('<!-- Spring Metrics disabled on internal IP address') self.assertTrue(r.startswith(
assert r.endswith('-->') '<!-- Spring Metrics disabled on internal IP address'), r)
self.assertTrue(r.endswith('-->'), r)

View file

@ -2,12 +2,11 @@
Tests for the UserVoice tags and filters. Tests for the UserVoice tags and filters.
""" """
import pytest
from django.template import Context from django.template import Context
from django.test.utils import override_settings from django.test.utils import override_settings
from utils import TagTestCase
from analytical.templatetags.uservoice import UserVoiceNode from analytical.templatetags.uservoice import UserVoiceNode
from analytical.tests.utils import TagTestCase
from analytical.utils import AnalyticalException from analytical.utils import AnalyticalException
@ -17,55 +16,65 @@ class UserVoiceTagTestCase(TagTestCase):
Tests for the ``uservoice`` template tag. Tests for the ``uservoice`` template tag.
""" """
def assertIn(self, element, container):
try:
super(TagTestCase, self).assertIn(element, container)
except AttributeError:
self.assertTrue(element in container)
def test_node(self): def test_node(self):
r = UserVoiceNode().render(Context()) r = UserVoiceNode().render(Context())
assert 'widget.uservoice.com/abcdefghijklmnopqrst.js' in r self.assertIn("widget.uservoice.com/abcdefghijklmnopqrst.js", r)
def test_tag(self): def test_tag(self):
r = self.render_tag('uservoice', 'uservoice') r = self.render_tag('uservoice', 'uservoice')
assert 'widget.uservoice.com/abcdefghijklmnopqrst.js' in r self.assertIn("widget.uservoice.com/abcdefghijklmnopqrst.js", r)
@override_settings(USERVOICE_WIDGET_KEY=None) @override_settings(USERVOICE_WIDGET_KEY=None)
def test_no_key(self): def test_no_key(self):
with pytest.raises(AnalyticalException): self.assertRaises(AnalyticalException, UserVoiceNode)
UserVoiceNode()
@override_settings(USERVOICE_WIDGET_KEY='abcdefgh ijklmnopqrst') @override_settings(USERVOICE_WIDGET_KEY='abcdefgh ijklmnopqrst')
def test_invalid_key(self): def test_invalid_key(self):
with pytest.raises(AnalyticalException): self.assertRaises(AnalyticalException, UserVoiceNode)
UserVoiceNode()
@override_settings(USERVOICE_WIDGET_KEY='') @override_settings(USERVOICE_WIDGET_KEY='')
def test_empty_key(self): def test_empty_key(self):
with pytest.raises(AnalyticalException): r = UserVoiceNode().render(Context())
UserVoiceNode() self.assertEqual(r, "")
@override_settings(USERVOICE_WIDGET_KEY='')
def test_overridden_empty_key(self):
vars = {'uservoice_widget_key': 'bcdefghijklmnopqrstu'}
r = UserVoiceNode().render(Context(vars))
self.assertIn("widget.uservoice.com/bcdefghijklmnopqrstu.js", r)
def test_overridden_key(self): def test_overridden_key(self):
vars = {'uservoice_widget_key': 'defghijklmnopqrstuvw'} vars = {'uservoice_widget_key': 'defghijklmnopqrstuvw'}
r = UserVoiceNode().render(Context(vars)) r = UserVoiceNode().render(Context(vars))
assert 'widget.uservoice.com/defghijklmnopqrstuvw.js' in r self.assertIn("widget.uservoice.com/defghijklmnopqrstuvw.js", r)
@override_settings(USERVOICE_WIDGET_OPTIONS={'key1': 'val1'}) @override_settings(USERVOICE_WIDGET_OPTIONS={'key1': 'val1'})
def test_options(self): def test_options(self):
r = UserVoiceNode().render(Context()) r = UserVoiceNode().render(Context())
assert """UserVoice.push(['set', {"key1": "val1"}]);""" in r self.assertIn("""UserVoice.push(['set', {"key1": "val1"}]);""", r)
@override_settings(USERVOICE_WIDGET_OPTIONS={'key1': 'val1'}) @override_settings(USERVOICE_WIDGET_OPTIONS={'key1': 'val1'})
def test_override_options(self): def test_override_options(self):
data = {'uservoice_widget_options': {'key1': 'val2'}} data = {'uservoice_widget_options': {'key1': 'val2'}}
r = UserVoiceNode().render(Context(data)) r = UserVoiceNode().render(Context(data))
assert """UserVoice.push(['set', {"key1": "val2"}]);""" in r self.assertIn("""UserVoice.push(['set', {"key1": "val2"}]);""", r)
def test_auto_trigger_default(self): def test_auto_trigger(self):
r = UserVoiceNode().render(Context()) r = UserVoiceNode().render(Context())
assert "UserVoice.push(['addTrigger', {}]);" in r self.assertTrue("UserVoice.push(['addTrigger', {}]);" in r, r)
@override_settings(USERVOICE_ADD_TRIGGER=False) @override_settings(USERVOICE_ADD_TRIGGER=False)
def test_auto_trigger(self): def test_auto_trigger(self):
r = UserVoiceNode().render(Context()) r = UserVoiceNode().render(Context())
assert "UserVoice.push(['addTrigger', {}]);" not in r self.assertFalse("UserVoice.push(['addTrigger', {}]);" in r, r)
@override_settings(USERVOICE_ADD_TRIGGER=False) @override_settings(USERVOICE_ADD_TRIGGER=False)
def test_auto_trigger_custom_win(self): def test_auto_trigger_custom_win(self):
r = UserVoiceNode().render(Context({'uservoice_add_trigger': True})) r = UserVoiceNode().render(Context({'uservoice_add_trigger': True}))
assert "UserVoice.push(['addTrigger', {}]);" in r self.assertTrue("UserVoice.push(['addTrigger', {}]);" in r, r)

View file

@ -0,0 +1,87 @@
"""
Tests for the Woopra template tags and filters.
"""
from django.contrib.auth.models import User, AnonymousUser
from django.http import HttpRequest
from django.template import Context
from django.test.utils import override_settings
from analytical.templatetags.woopra import WoopraNode
from analytical.tests.utils import TagTestCase
from analytical.utils import AnalyticalException
@override_settings(WOOPRA_DOMAIN='example.com')
class WoopraTagTestCase(TagTestCase):
"""
Tests for the ``woopra`` template tag.
"""
def test_tag(self):
r = self.render_tag('woopra', 'woopra')
self.assertTrue('var woo_settings = {"domain": "example.com"};' in r, r)
def test_node(self):
r = WoopraNode().render(Context({}))
self.assertTrue('var woo_settings = {"domain": "example.com"};' in r, r)
@override_settings(WOOPRA_DOMAIN=None)
def test_no_domain(self):
self.assertRaises(AnalyticalException, WoopraNode)
@override_settings(WOOPRA_DOMAIN='this is not a domain')
def test_wrong_domain(self):
self.assertRaises(AnalyticalException, WoopraNode)
@override_settings(WOOPRA_IDLE_TIMEOUT=1234)
def test_idle_timeout(self):
r = WoopraNode().render(Context({}))
self.assertTrue('var woo_settings = {"domain": "example.com", '
'"idle_timeout": "1234"};' in r, r)
def test_custom(self):
r = WoopraNode().render(Context({'woopra_var1': 'val1',
'woopra_var2': 'val2'}))
self.assertTrue('var woo_visitor = {"var1": "val1", "var2": "val2"};'
in r, r)
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
def test_identify_name_and_email(self):
r = WoopraNode().render(Context({'user': User(username='test',
first_name='Firstname', last_name='Lastname',
email="test@example.com")}))
self.assertTrue('var woo_visitor = {"email": "test@example.com", '
'"name": "Firstname Lastname"};' in r, r)
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
def test_identify_username_no_email(self):
r = WoopraNode().render(Context({'user': User(username='test')}))
self.assertTrue('var woo_visitor = {"name": "test"};' in r, r)
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
def test_no_identify_when_explicit_name(self):
r = WoopraNode().render(Context({'woopra_name': 'explicit',
'user': User(username='implicit')}))
self.assertTrue('var woo_visitor = {"name": "explicit"};' in r, r)
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
def test_no_identify_when_explicit_email(self):
r = WoopraNode().render(Context({'woopra_email': 'explicit',
'user': User(username='implicit')}))
self.assertTrue('var woo_visitor = {"email": "explicit"};' in r, r)
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
def test_identify_anonymous_user(self):
r = WoopraNode().render(Context({'user': AnonymousUser()}))
self.assertTrue('var woo_visitor = {};' in r, r)
@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 = WoopraNode().render(context)
self.assertTrue(r.startswith(
'<!-- Woopra disabled on internal IP address'), r)
self.assertTrue(r.endswith('-->'), r)

View file

@ -0,0 +1,107 @@
"""
Tests for the analytical.utils module.
"""
from django.conf import settings
from django.http import HttpRequest
from django.template import Context
from django.test.utils import override_settings
from analytical.utils import (
get_domain, is_internal_ip, get_required_setting, AnalyticalException)
from analytical.tests.utils import TestCase
class SettingDeletedTestCase(TestCase):
@override_settings(USER_ID=None)
def test_get_required_setting(self):
"""
Make sure using get_required_setting fails in the right place.
"""
# available in python >= 3.2
if hasattr(self, 'assertRaisesRegex'):
with self.assertRaisesRegex(AnalyticalException, "^USER_ID setting is set to None$"):
user_id = get_required_setting("USER_ID", "\d+", "invalid USER_ID")
# available in python >= 2.7, deprecated in 3.2
elif hasattr(self, 'assertRaisesRegexp'):
with self.assertRaisesRegexp(AnalyticalException, "^USER_ID setting is set to None$"):
user_id = get_required_setting("USER_ID", "\d+", "invalid USER_ID")
else:
self.assertRaises(AnalyticalException,
get_required_setting, "USER_ID", "\d+", "invalid USER_ID")
@override_settings(ANALYTICAL_DOMAIN="example.org")
class GetDomainTestCase(TestCase):
def test_get_service_domain_from_context(self):
context = Context({'test_domain': 'example.com'})
self.assertEqual(get_domain(context, 'test'), 'example.com')
def test_get_analytical_domain_from_context(self):
context = Context({'analytical_domain': 'example.com'})
self.assertEqual(get_domain(context, 'test'), 'example.com')
@override_settings(TEST_DOMAIN="example.net")
def test_get_service_domain_from_settings(self):
context = Context()
self.assertEqual(get_domain(context, 'test'), 'example.net')
def test_get_analytical_domain_from_settings(self):
context = Context()
self.assertEqual(get_domain(context, 'test'), 'example.org')
# FIXME: enable Django apps dynamically and enable test again
#@with_apps('django.contrib.sites')
#@override_settings(TEST_DOMAIN=SETTING_DELETED,
# ANALYTICAL_DOMAIN=SETTING_DELETED)
#class GetDomainTestCaseWithSites(TestCase):
# def test_get_domain_from_site(self):
# site = Site.objects.create(domain="example.com", name="test")
# with override_settings(SITE_ID=site.id):
# context = Context()
# self.assertEqual(get_domain(context, 'test'), 'example.com')
class InternalIpTestCase(TestCase):
@override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
def test_render_no_internal_ip(self):
context = Context()
self.assertFalse(is_internal_ip(context))
@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})
self.assertTrue(is_internal_ip(context))
@override_settings(TEST_INTERNAL_IPS=['1.1.1.1'])
def test_render_prefix_internal_ip(self):
req = HttpRequest()
req.META['REMOTE_ADDR'] = '1.1.1.1'
context = Context({'request': req})
self.assertTrue(is_internal_ip(context, 'TEST'))
@override_settings(INTERNAL_IPS=['1.1.1.1'])
def test_render_internal_ip_fallback(self):
req = HttpRequest()
req.META['REMOTE_ADDR'] = '1.1.1.1'
context = Context({'request': req})
self.assertTrue(is_internal_ip(context))
@override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
def test_render_internal_ip_forwarded_for(self):
req = HttpRequest()
req.META['HTTP_X_FORWARDED_FOR'] = '1.1.1.1'
context = Context({'request': req})
self.assertTrue(is_internal_ip(context))
@override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
def test_render_different_internal_ip(self):
req = HttpRequest()
req.META['REMOTE_ADDR'] = '2.2.2.2'
context = Context({'request': req})
self.assertFalse(is_internal_ip(context))

View file

@ -2,9 +2,30 @@
Testing utilities. Testing utilities.
""" """
from django.template import Context, RequestContext, Template from __future__ import with_statement
from django.template import Template, Context, RequestContext
from django.test.testcases import TestCase from django.test.testcases import TestCase
def run_tests():
"""
Use the Django test runner to run the tests.
Sets the return code to the number of failed tests.
"""
import sys
import django
try:
django.setup()
except AttributeError:
pass
try:
from django.test.runner import DiscoverRunner as TestRunner
except ImportError:
from django.test.simple import DjangoTestSuiteRunner as TestRunner
runner = TestRunner()
sys.exit(runner.run_tests(["analytical"]))
class TagTestCase(TestCase): class TagTestCase(TestCase):
""" """
@ -16,7 +37,7 @@ class TagTestCase(TestCase):
def render_tag(self, library, tag, vars=None, request=None): def render_tag(self, library, tag, vars=None, request=None):
if vars is None: if vars is None:
vars = {} vars = {}
t = Template('{%% load %s %%}{%% %s %%}' % (library, tag)) t = Template("{%% load %s %%}{%% %s %%}" % (library, tag))
if request is not None: if request is not None:
context = RequestContext(request, vars) context = RequestContext(request, vars)
else: else:

View file

@ -3,9 +3,12 @@ Utility function for django-analytical.
""" """
from django.conf import settings from django.conf import settings
from django.contrib.sites.models import Site
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
HTML_COMMENT = '<!-- %(service)s disabled on internal IP address\n%(html)s\n-->'
HTML_COMMENT = "<!-- %(service)s disabled on internal IP " \
"address\n%(html)s\n-->"
def get_required_setting(setting, value_re, invalid_msg): def get_required_setting(setting, value_re, invalid_msg):
@ -18,14 +21,13 @@ def get_required_setting(setting, value_re, invalid_msg):
try: try:
value = getattr(settings, setting) value = getattr(settings, setting)
except AttributeError: except AttributeError:
raise AnalyticalException('%s setting: not found' % setting) raise AnalyticalException("%s setting: not found" % setting)
if not value: if value is None:
raise AnalyticalException('%s setting is not set' % setting) raise AnalyticalException("%s setting is set to None" % setting)
value = str(value) value = str(value)
if not value_re.search(value): if not value_re.search(value):
raise AnalyticalException( raise AnalyticalException("%s setting: %s: '%s'"
"%s setting: %s: '%s'" % (setting, invalid_msg, value) % (setting, invalid_msg, value))
)
return value return value
@ -48,19 +50,6 @@ def get_user_from_context(context):
return None return None
def get_user_is_authenticated(user):
"""Check if the user is authenticated.
This is a compatibility function needed to support both Django 1.x and 2.x;
Django 2.x turns the function into a proper boolean so function calls will
fail.
"""
if callable(user.is_authenticated):
return user.is_authenticated()
else:
return user.is_authenticated
def get_identity(context, prefix=None, identity_func=None, user=None): def get_identity(context, prefix=None, identity_func=None, user=None):
""" """
Get the identity of a logged in user from a template context. Get the identity of a logged in user from a template context.
@ -83,11 +72,11 @@ def get_identity(context, prefix=None, identity_func=None, user=None):
try: try:
if user is None: if user is None:
user = get_user_from_context(context) user = get_user_from_context(context)
if get_user_is_authenticated(user): if user.is_authenticated():
if identity_func is not None: if identity_func is not None:
return identity_func(user) return identity_func(user)
else: else:
return user.get_username() return user.username
except (KeyError, AttributeError): except (KeyError, AttributeError):
pass pass
return None return None
@ -111,8 +100,6 @@ def get_domain(context, prefix):
domain = getattr(settings, 'ANALYTICAL_DOMAIN', None) domain = getattr(settings, 'ANALYTICAL_DOMAIN', None)
if domain is None: if domain is None:
if 'django.contrib.sites' in settings.INSTALLED_APPS: if 'django.contrib.sites' in settings.INSTALLED_APPS:
from django.contrib.sites.models import Site
try: try:
domain = Site.objects.get_current().domain domain = Site.objects.get_current().domain
except (ImproperlyConfigured, Site.DoesNotExist): except (ImproperlyConfigured, Site.DoesNotExist):
@ -136,15 +123,15 @@ def is_internal_ip(context, prefix=None):
if not remote_ip: if not remote_ip:
return False return False
internal_ips = None internal_ips = ''
if prefix is not None: if prefix is not None:
internal_ips = getattr(settings, '%s_INTERNAL_IPS' % prefix, None) internal_ips = getattr(settings, '%s_INTERNAL_IPS' % prefix, '')
if internal_ips is None: if not internal_ips:
internal_ips = getattr(settings, 'ANALYTICAL_INTERNAL_IPS', None) internal_ips = getattr(settings, 'ANALYTICAL_INTERNAL_IPS', '')
if internal_ips is None: if not internal_ips:
internal_ips = getattr(settings, 'INTERNAL_IPS', None) internal_ips = getattr(settings, 'INTERNAL_IPS', '')
return remote_ip in (internal_ips or []) return remote_ip in internal_ips
except (KeyError, AttributeError): except (KeyError, AttributeError):
return False return False
@ -163,5 +150,4 @@ class AnalyticalException(Exception):
Raised when an exception occurs in any django-analytical code that should Raised when an exception occurs in any django-analytical code that should
be silenced in templates. be silenced in templates.
""" """
silent_variable_failure = True silent_variable_failure = True

View file

@ -1,21 +1,26 @@
def setup(app): def setup(app):
app.add_crossref_type( app.add_crossref_type(
directivename='setting', directivename="setting",
rolename='setting', rolename="setting",
indextemplate='pair: %s; setting', indextemplate="pair: %s; setting",
) )
app.add_crossref_type( app.add_crossref_type(
directivename='templatetag', directivename="templatetag",
rolename='ttag', rolename="ttag",
indextemplate='pair: %s; template tag', indextemplate="pair: %s; template tag"
) )
app.add_crossref_type( app.add_crossref_type(
directivename='templatefilter', directivename="templatefilter",
rolename='tfilter', rolename="tfilter",
indextemplate='pair: %s; template filter', indextemplate="pair: %s; template filter"
) )
app.add_crossref_type( app.add_crossref_type(
directivename='fieldlookup', directivename="fieldlookup",
rolename='lookup', rolename="lookup",
indextemplate='pair: %s; field lookup type', indextemplate="pair: %s; field lookup type",
)
app.add_description_unit(
directivename="decorator",
rolename="dec",
indextemplate="pair: %s; function decorator",
) )

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
# #
# This file is execfile()d with the current directory set to its containing # This file is execfile()d with the current directory set to its containing
# directory. # directory.
@ -8,12 +9,13 @@ import sys
sys.path.append(os.path.join(os.path.abspath('.'), '_ext')) sys.path.append(os.path.join(os.path.abspath('.'), '_ext'))
sys.path.append(os.path.dirname(os.path.abspath('.'))) sys.path.append(os.path.dirname(os.path.abspath('.')))
import analytical # noqa import analytical
# -- General configuration -------------------------------------------------- # -- General configuration --------------------------------------------------
project = 'django-analytical' project = u'django-analytical'
copyright = '2011, Joost Cassee <joost@cassee.net>' copyright = u'2011, Joost Cassee <joost@cassee.net>'
release = analytical.__version__ release = analytical.__version__
# The short X.Y version. # The short X.Y version.
@ -21,15 +23,16 @@ version = release.rsplit('.', 1)[0]
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'local'] extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'local']
templates_path = ['_templates'] templates_path = ['_templates']
source_suffix = {'.rst': 'restructuredtext'} source_suffix = '.rst'
master_doc = 'index' master_doc = 'index'
add_function_parentheses = True add_function_parentheses = True
pygments_style = 'sphinx' pygments_style = 'sphinx'
intersphinx_mapping = { intersphinx_mapping = {
'python': ('https://docs.python.org/3.13', None), 'http://docs.python.org/2.7': None,
'django': ('https://docs.djangoproject.com/en/stable', None), 'http://docs.djangoproject.com/en/1.8':
'http://docs.djangoproject.com/en/1.8/_objects/',
} }
@ -42,11 +45,6 @@ htmlhelp_basename = 'analyticaldoc'
# -- Options for LaTeX output ----------------------------------------------- # -- Options for LaTeX output -----------------------------------------------
latex_documents = [ latex_documents = [
( ('index', 'django-analytical.tex', u'Documentation for django-analytical',
'index', u'Joost Cassee', 'manual'),
'django-analytical.tex',
'Documentation for django-analytical',
'Joost Cassee',
'manual',
),
] ]

View file

@ -18,9 +18,7 @@ initialization code if the client IP address is detected as one from the
:data:`ANALYTICAL_INTERNAL_IPS` setting. The default value for this :data:`ANALYTICAL_INTERNAL_IPS` setting. The default value for this
setting is :data:`INTERNAL_IPS`. setting is :data:`INTERNAL_IPS`.
Example: Example::
.. code-block:: python
ANALYTICAL_INTERNAL_IPS = ['192.168.1.45', '192.168.1.57'] ANALYTICAL_INTERNAL_IPS = ['192.168.1.45', '192.168.1.57']
@ -47,9 +45,7 @@ logged in through the standard Django authentication system and the
current user is accessible in the template context, the username can be current user is accessible in the template context, the username can be
passed to the analytics services that support identifying users. This passed to the analytics services that support identifying users. This
feature is configured by the :data:`ANALYTICAL_AUTO_IDENTIFY` setting feature is configured by the :data:`ANALYTICAL_AUTO_IDENTIFY` setting
and is enabled by default. To disable: and is enabled by default. To disable::
.. code-block:: python
ANALYTICAL_AUTO_IDENTIFY = False ANALYTICAL_AUTO_IDENTIFY = False
@ -68,52 +64,3 @@ and is enabled by default. To disable:
Alternatively, add one of the variables to the context yourself Alternatively, add one of the variables to the context yourself
when you render the template. when you render the template.
Changing the identity
*********************
If you want to override the identity of the logged-in user that the various
providers send you can do it by setting the ``analytical_identity`` context
variable in your view code:
.. code-block:: python
context = RequestContext({'analytical_identity': user.uuid})
return some_template.render(context)
or in the template:
.. code-block:: django
{% with analytical_identity=request.user.uuid|default:None %}
{% analytical_head_top %}
{% endwith %}
or by implementing a context processor, e.g.
.. code-block:: python
# FILE: myproject/context_processors.py
from django.conf import settings
def get_identity(request):
return {
'analytical_identity': 'some-value-here',
}
# FILE: myproject/settings.py
TEMPLATES = [
{
'OPTIONS': {
'context_processors': [
'myproject.context_processors.get_identity',
],
},
},
]
That allows you as a developer to leave your view code untouched and
make sure that the variable is injected for all templates.
If you want to change the identity only for specific provider use the
``*_identity`` context variable, where the ``*`` prefix is the module name
of the specific provider.

View file

@ -10,11 +10,6 @@ version numbers. Patch-level increments indicate bug fixes, minor
version increments indicate new functionality and major version version increments indicate new functionality and major version
increments indicate backwards incompatible changes. increments indicate backwards incompatible changes.
Version 1.0.0 is the last to support Django < 1.7. Users of older Django
versions should stick to 1.0.0, and are encouraged to upgrade their setups.
Starting with 2.0.0, dropping support for obsolete Django versions is not
considered to be a backward-incompatible change.
.. _`Semantic Versioning`: http://semver.org/ .. _`Semantic Versioning`: http://semver.org/
.. include:: ../CHANGELOG.rst .. include:: ../CHANGELOG.rst
@ -23,21 +18,8 @@ considered to be a backward-incompatible change.
Credits Credits
======= =======
The django-analytical package was originally written by `Joost Cassee`_ .. include:: ../AUTHORS.rst
and is now maintained by `Peter Bittner`_ and the `Jazzband community`_.
All known contributors are listed as ``authors`` in the `project metadata`_.
Included JavaScript code snippets for integration of the analytics services
were written by the respective service providers.
The application was inspired by and uses ideas from Analytical_, Joshua
Krall's all-purpose analytics front-end for Rails.
.. _`Joost Cassee`: https://github.com/jcassee
.. _`Peter Bittner`: https://github.com/bittner
.. _`Jazzband community`: https://jazzband.co/
.. _`project metadata`: https://github.com/jazzband/django-analytical/blob/main/pyproject.toml#L15-L60
.. _`Analytical`: https://github.com/jkrall/analytical
.. _helping-out: .. _helping-out:
@ -45,6 +27,4 @@ Helping out
=========== ===========
.. include:: ../README.rst .. include:: ../README.rst
:start-after: .. start contribute include :start-after: GitHub`_.
:end-before: .. end contribute include

View file

@ -5,10 +5,10 @@ django-analytical
The django-analytical application integrates analytics services into a The django-analytical application integrates analytics services into a
Django_ project. Django_ project.
.. _Django: https://www.djangoproject.com/ .. _Django: http://www.djangoproject.com/
:Package: https://pypi.org/project/django-analytical/ :Package: http://pypi.python.org/pypi/django-analytical/
:Source: https://github.com/jazzband/django-analytical :Source: http://github.com/jcassee/django-analytical
Overview Overview

View file

@ -20,29 +20,23 @@ Installing the Python package
To install django-analytical the ``analytical`` package must be added to To install django-analytical the ``analytical`` package must be added to
the Python path. You can install it directly from PyPI using the Python path. You can install it directly from PyPI using
``easy_install``: ``easy_install``::
.. code-block:: bash $ easy_install django-analytical
$ easy_install django-analytical
You can also install directly from source. Download either the latest You can also install directly from source. Download either the latest
stable version from PyPI_ or any release from GitHub_, or use Git to stable version from PyPI_ or any release from GitHub_, or use Git to
get the development code: get the development code::
.. code-block:: bash $ git clone https://github.com/jcassee/django-analytical.git
$ git clone https://github.com/jazzband/django-analytical.git .. _PyPI: http://pypi.python.org/pypi/django-analytical/
.. _GitHub: http://github.com/jcassee/django-analytical
.. _PyPI: https://pypi.org/project/django-analytical/ Then install the package by running the setup script::
.. _GitHub: http://github.com/jazzband/django-analytical
Then install the package by running the setup script: $ cd django-analytical
$ python setup.py install
.. code-block:: bash
$ cd django-analytical
$ python setup.py install
.. _installing-the-application: .. _installing-the-application:
@ -52,15 +46,13 @@ Installing the Django application
After you installed django-analytical, add the ``analytical`` Django After you installed django-analytical, add the ``analytical`` Django
application to the list of installed applications in the ``settings.py`` application to the list of installed applications in the ``settings.py``
file of your project: file of your project::
.. code-block:: python INSTALLED_APPS = [
...
INSTALLED_APPS = [ 'analytical',
... ...
'analytical', ]
...
]
.. _adding-the-template-tags: .. _adding-the-template-tags:
@ -68,32 +60,30 @@ file of your project:
Adding the template tags to the base template Adding the template tags to the base template
============================================= =============================================
Because every analytics service uses own specific JavaScript code that Because every analytics service uses own specific Javascript code that
should be added to the top or bottom of either the head or body of the should be added to the top or bottom of either the head or body of the
HTML page, django-analytical provides four general-purpose template tags HTML page, django-analytical provides four general-purpose template tags
that will render the code needed for the services you are using. Your that will render the code needed for the services you are using. Your
base template should look like this: base template should look like this::
.. code-block:: django {% load analytical %}
<!DOCTYPE ... >
<html>
<head>
{% analytical_head_top %}
{% load analytical %} ...
<!DOCTYPE ... >
<html>
<head>
{% analytical_head_top %}
... {% analytical_head_bottom %}
</head>
<body>
{% analytical_body_top %}
{% analytical_head_bottom %} ...
</head>
<body>
{% analytical_body_top %}
... {% analytical_body_bottom %}
</body>
{% analytical_body_bottom %} </html>
</body>
</html>
Instead of using the generic tags, you can also just use tags specific Instead of using the generic tags, you can also just use tags specific
for the analytics service(s) you are using. See :ref:`services` for for the analytics service(s) you are using. See :ref:`services` for
@ -111,39 +101,27 @@ settings required to enable each service are listed here:
* :doc:`Chartbeat <services/chartbeat>`:: * :doc:`Chartbeat <services/chartbeat>`::
CHARTBEAT_USER_ID = '12345' CHARTBEAT_USER_ID = '12345'
* :doc:`Clickmap <services/clickmap>`:: * :doc:`Clickmap <services/clickmap>`::
CLICKMAP_TRACKER_CODE = '12345678....912' CLICKMAP_TRACKER_CODE = '12345678....912'
* :doc:`Clicky <services/clicky>`:: * :doc:`Clicky <services/clicky>`::
CLICKY_SITE_ID = '12345678' CLICKY_SITE_ID = '12345678'
* :doc:`Crazy Egg <services/crazy_egg>`:: * :doc:`Crazy Egg <services/crazy_egg>`::
CRAZY_EGG_ACCOUNT_NUMBER = '12345678' CRAZY_EGG_ACCOUNT_NUMBER = '12345678'
* :doc:`Facebook Pixel <services/facebook_pixel>`::
FACEBOOK_PIXEL_ID = '1234567890'
* :doc:`Gaug.es <services/gauges>`:: * :doc:`Gaug.es <services/gauges>`::
GAUGES_SITE_ID = '0123456789abcdef0123456789abcdef' GAUGES_SITE_ID = '0123456789abcdef0123456789abcdef'
* :doc:`Google Analytics (legacy) <services/google_analytics>`:: * :doc:`Google Analytics <services/google_analytics>`::
GOOGLE_ANALYTICS_PROPERTY_ID = 'UA-1234567-8' GOOGLE_ANALYTICS_PROPERTY_ID = 'UA-1234567-8'
* :doc:`Google Analytics (gtag.js) <services/google_analytics_gtag>`::
GOOGLE_ANALYTICS_GTAG_PROPERTY_ID = 'UA-1234567-8'
* :doc:`Google Analytics (analytics.js) <services/google_analytics_js>`::
GOOGLE_ANALYTICS_JS_PROPERTY_ID = 'UA-12345678-9'
* :doc:`HubSpot <services/hubspot>`:: * :doc:`HubSpot <services/hubspot>`::
@ -156,25 +134,16 @@ settings required to enable each service are listed here:
* :doc:`KISSinsights <services/kiss_insights>`:: * :doc:`KISSinsights <services/kiss_insights>`::
KISS_INSIGHTS_ACCOUNT_NUMBER = '12345' KISS_INSIGHTS_ACCOUNT_NUMBER = '12345'
KISS_INSIGHTS_SITE_CODE = 'abc' KISS_INSIGHTS_SITE_CODE = 'abc'
* :doc:`KISSmetrics <services/kiss_metrics>`:: * :doc:`KISSmetrics <services/kiss_metrics>`::
KISS_METRICS_API_KEY = '0123456789abcdef0123456789abcdef01234567' KISS_METRICS_API_KEY = '0123456789abcdef0123456789abcdef01234567'
* :doc:`Lucky Orange <services/luckyorange>`::
LUCKYORANGE_SITE_ID = '123456'
* :doc:`Matomo (formerly Piwik) <services/matomo>`::
MATOMO_DOMAIN_PATH = 'your.matomo.server/optional/path'
MATOMO_SITE_ID = '123'
* :doc:`Mixpanel <services/mixpanel>`:: * :doc:`Mixpanel <services/mixpanel>`::
MIXPANEL_API_TOKEN = '0123456789abcdef0123456789abcdef' MIXPANEL_API_TOKEN = '0123456789abcdef0123456789abcdef'
* :doc:`Olark <services/olark>`:: * :doc:`Olark <services/olark>`::
@ -182,27 +151,25 @@ settings required to enable each service are listed here:
* :doc:`Optimizely <services/optimizely>`:: * :doc:`Optimizely <services/optimizely>`::
OPTIMIZELY_ACCOUNT_NUMBER = '1234567' OPTIMIZELY_ACCOUNT_NUMBER = '1234567'
* :doc:`Performable <services/performable>`:: * :doc:`Performable <services/performable>`::
PERFORMABLE_API_KEY = '123abc' PERFORMABLE_API_KEY = '123abc'
* :doc:`Rating\@Mail.ru <services/rating_mailru>`:: * :doc:`Piwik <services/piwik>`::
RATING_MAILRU_COUNTER_ID = '1234567' PIWIK_DOMAIN_PATH = 'your.piwik.server/optional/path'
PIWIK_SITE_ID = '123'
* :doc:`SnapEngage <services/snapengage>`:: * :doc:`Reinvigorate <services/reinvigorate>`::
SNAPENGAGE_WIDGET_ID = 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' REINVIGORATE_TRACKING_ID = '12345-abcdefghij'
* :doc:`Woopra <services/woopra>`:: * :doc:`Woopra <services/woopra>`::
WOOPRA_DOMAIN = 'abcde.com' WOOPRA_DOMAIN = 'abcde.com'
* :doc:`Yandex.Metrica <services/yandex_metrica>`::
YANDEX_METRICA_COUNTER_ID = '12345678'
---- ----

View file

@ -13,13 +13,13 @@ If you would like to have another analytics service supported by
django-analytical, please create an issue on the project django-analytical, please create an issue on the project
`issue tracker`_. See also :ref:`helping-out`. `issue tracker`_. See also :ref:`helping-out`.
.. _`issue tracker`: http://github.com/jazzband/django-analytical/issues .. _`issue tracker`: http://github.com/jcassee/django-analytical/issues
Currently supported services: Currently supported services:
.. toctree:: .. toctree::
:maxdepth: 1 :maxdepth: 1
:glob: :glob:
services/* services/*

View file

@ -70,7 +70,7 @@ contains a line that looks like this::
Here, ``XXXXX`` is your User ID. Set :const:`CHARTBEAT_USER_ID` in the Here, ``XXXXX`` is your User ID. Set :const:`CHARTBEAT_USER_ID` in the
project :file:`settings.py` file:: project :file:`settings.py` file::
CHARTBEAT_USER_ID = 'XXXXX' CHARTBEAT_SITE_ID = 'XXXXX'
If you do not set a User ID, the tracking code will not be rendered. If you do not set a User ID, the tracking code will not be rendered.
@ -96,7 +96,7 @@ important information about detecting the visitor IP address.
Setting the domain Setting the domain
------------------ ------------------
The JavaScript tracking code can send the website domain to Chartbeat. The Javascript tracking code can send the website domain to Chartbeat.
If you use multiple subdomains this enables you to treat them as one If you use multiple subdomains this enables you to treat them as one
website in Chartbeat. If your project uses the sites framework, the website in Chartbeat. If your project uses the sites framework, the
domain name of the current :class:`~django.contrib.sites.models.Site` domain name of the current :class:`~django.contrib.sites.models.Site`

View file

@ -2,10 +2,9 @@
Clickmap -- visual click tracking Clickmap -- visual click tracking
================================== ==================================
`Clickmap`_ is a real-time heatmap tool to track mouse clicks and scroll paths of your website visitors. `Clickmap`_ is a real-time heatmap tool to track mouse clicks and scroll paths of your website visitors. Gain intelligence about what's hot and what's not, and optimize your conversion with Clickmap.
Gain intelligence about what's hot and what's not, and optimize your conversion with Clickmap.
.. _`Clickmap`: http://www.clickmap.ch/ .. _`Clickmap`: http://www.getclickmap.com/
.. clickmap-installation: .. clickmap-installation:
@ -23,7 +22,7 @@ This step is only needed if you are not using the generic
:ttag:`analytical.*` tags. If you are, skip to :ttag:`analytical.*` tags. If you are, skip to
:ref:`clickmap-configuration`. :ref:`clickmap-configuration`.
The Clickmap JavaScript code is inserted into templates using a template The Clickmap Javascript code is inserted into templates using a template
tag. Load the :mod:`clickmap` template tag library and insert the tag. Load the :mod:`clickmap` template tag library and insert the
:ttag:`clickmap` tag. Because every page that you want to track must :ttag:`clickmap` tag. Because every page that you want to track must
have the tag, it is useful to add it to your base template. Insert have the tag, it is useful to add it to your base template. Insert
@ -45,7 +44,7 @@ Before you can use the Clickmap integration, you must first set your
Clickmap Tracker ID. If you don't have a Clickmap account yet, Clickmap Tracker ID. If you don't have a Clickmap account yet,
`sign up`_ to get your Tracker ID. `sign up`_ to get your Tracker ID.
.. _`sign up`: http://www.clickmap.ch/ .. _`sign up`: http://www.getclickmap.com/
.. _clickmap-tracker-id: .. _clickmap-tracker-id:
@ -54,7 +53,7 @@ Setting the Tracker ID
---------------------- ----------------------
Clickmap gives you a unique Tracker ID, and the :ttag:`clickmap` Clickmap gives you a unique Tracker ID, and the :ttag:`clickmap`
tag will include it in the rendered JavaScript code. You can find your tag will include it in the rendered Javascript code. You can find your
Tracker ID clicking the link named "Tracker" in the dashboard Tracker ID clicking the link named "Tracker" in the dashboard
of your Clickmap account. Set :const:`CLICKMAP_TRACKER_ID` in the project of your Clickmap account. Set :const:`CLICKMAP_TRACKER_ID` in the project
:file:`settings.py` file:: :file:`settings.py` file::

View file

@ -53,7 +53,7 @@ Setting the Site ID
------------------- -------------------
Every website you track with Clicky gets its own Site ID, and the Every website you track with Clicky gets its own Site ID, and the
:ttag:`clicky` tag will include it in the rendered JavaScript code. :ttag:`clicky` tag will include it in the rendered Javascript code.
You can find the Site ID in the *Info* tab of the website *Preferences* You can find the Site ID in the *Info* tab of the website *Preferences*
page, in your Clicky account. Set :const:`CLICKY_SITE_ID` in the page, in your Clicky account. Set :const:`CLICKY_SITE_ID` in the
project :file:`settings.py` file:: project :file:`settings.py` file::
@ -84,7 +84,7 @@ Custom data
As described in the Clicky `customized tracking`_ documentation page, As described in the Clicky `customized tracking`_ documentation page,
the data that is tracked by Clicky can be customized by setting the the data that is tracked by Clicky can be customized by setting the
:data:`clicky_custom` JavaScript variable before loading the tracking :data:`clicky_custom` Javascript variable before loading the tracking
code. Using template context variables, you can let the :ttag:`clicky` code. Using template context variables, you can let the :ttag:`clicky`
tag pass custom data to Clicky automatically. You can set the context tag pass custom data to Clicky automatically. You can set the context
variables in your view when you render a template containing the variables in your view when you render a template containing the

View file

@ -53,7 +53,7 @@ Setting the account number
-------------------------- --------------------------
Crazy Egg gives you a unique account number, and the :ttag:`crazy egg` Crazy Egg gives you a unique account number, and the :ttag:`crazy egg`
tag will include it in the rendered JavaScript code. You can find your tag will include it in the rendered Javascript code. You can find your
account number by clicking the link named "What's my code?" in the account number by clicking the link named "What's my code?" in the
dashboard of your Crazy Egg account. Set dashboard of your Crazy Egg account. Set
:const:`CRAZY_EGG_ACCOUNT_NUMBER` in the project :file:`settings.py` :const:`CRAZY_EGG_ACCOUNT_NUMBER` in the project :file:`settings.py`

View file

@ -1,84 +0,0 @@
=======================================
Facebook Pixel -- advertising analytics
=======================================
`Facebook Pixel`_ is Facebook's tool for conversion tracking, optimisation and remarketing.
.. _`Facebook Pixel`: https://developers.facebook.com/docs/facebook-pixel/
.. facebook-pixel-installation:
Installation
============
To start using the Facebook Pixel 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 Facebook Pixel 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:`facebook-pixel-configuration`.
The Facebook Pixel code is inserted into templates using template tags.
Because every page that you want to track must have the tag,
it is useful to add it to your base template.
At the top of the template, load the :mod:`facebook_pixel` template tag library.
Then insert the :ttag:`facebook_pixel_head` tag at the bottom of the head section,
and optionally insert the :ttag:`facebook_pixel_body` tag at the bottom of the body section::
{% load facebook_pixel %}
<html>
<head>
...
{% facebook_pixel_head %}
</head>
<body>
...
{% facebook_pixel_body %}
</body>
</html>
.. note::
The :ttag:`facebook_pixel_body` tag code will only be used for browsers with JavaScript disabled.
It can be omitted if you don't need to support them.
.. _facebook-pixel-configuration:
Configuration
=============
Before you can use the Facebook Pixel integration,
you must first set your Pixel ID.
.. _facebook-pixel-id:
Setting the Pixel ID
--------------------
Each Facebook Adverts account you have can have a Pixel ID,
and the :mod:`facebook_pixel` tags will include it in the rendered page.
You can find the Pixel ID on the "Pixels" section of your Facebook Adverts account.
Set :const:`FACEBOOK_PIXEL_ID` in the project :file:`settings.py` file::
FACEBOOK_PIXEL_ID = 'XXXXXXXXXX'
If you do not set a Pixel ID, the code will not be rendered.
.. _facebook-pixel-internal-ips:
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:`FACEBOOK_PIXEL_INTERNAL_IPS`
setting, the tracking code is commented out. It takes the value of
:const:`ANALYTICAL_INTERNAL_IPS` by default (which in turn is
:const:`INTERNAL_IPS` by default). See :ref:`identifying-visitors` for
important information about detecting the visitor IP address.

View file

@ -23,7 +23,7 @@ This step is only needed if you are not using the generic
:ttag:`analytical.*` tags. If you are, skip to :ttag:`analytical.*` tags. If you are, skip to
:ref:`gauges-configuration`. :ref:`gauges-configuration`.
The Gaug.es JavaScript code is inserted into templates using a The Gaug.es Javascript code is inserted into templates using a
template tag. Load the :mod:`gauges` template tag library and template tag. Load the :mod:`gauges` template tag library and
insert the :ttag:`gauges` tag. Because every page that you want to insert the :ttag:`gauges` tag. Because every page that you want to
track must have the tag, it is useful to add it to your base template. track must have the tag, it is useful to add it to your base template.
@ -51,12 +51,12 @@ Setting the site id
-------------------------- --------------------------
Gaug.es gives you a unique site id, and the :ttag:`gauges` Gaug.es gives you a unique site id, and the :ttag:`gauges`
tag will include it in the rendered JavaScript code. You can find your tag will include it in the rendered Javascript code. You can find your
site id by clicking the *Tracking Code* link when logged into site id by clicking the *Tracking Code* link when logged into
the on the gaug.es website. A page will display containing the on the gaug.es website. A page will display containing
HTML code looking like this:: HTML code looking like this::
<script> <script type="text/javascript">
var _gauges = _gauges || []; var _gauges = _gauges || [];
(function() { (function() {
var t = document.createElement('script'); var t = document.createElement('script');
@ -76,7 +76,7 @@ file::
GAUGES_SITE_ID = 'XXXXXXXXXXXXXXXXXXXXXXX' GAUGES_SITE_ID = 'XXXXXXXXXXXXXXXXXXXXXXX'
If you do not set an site id, the JavaScript code will not be If you do not set an site id, the Javascript code will not be
rendered. rendered.

View file

@ -1,6 +1,6 @@
============================================== ====================================
Google Analytics (legacy) -- traffic analysis Google Analytics -- traffic analysis
============================================== ====================================
`Google Analytics`_ is the well-known web analytics service from `Google Analytics`_ is the well-known web analytics service from
Google. The product is aimed more at marketers than webmasters or Google. The product is aimed more at marketers than webmasters or
@ -15,7 +15,7 @@ features.
Installation Installation
============ ============
To start using the Google Analytics (legacy) integration, you must have installed To start using the Google Analytics integration, you must have installed
the django-analytical package and have added the ``analytical`` the django-analytical package and have added the ``analytical``
application to :const:`INSTALLED_APPS` in your project application to :const:`INSTALLED_APPS` in your project
:file:`settings.py` file. See :doc:`../install` for details. :file:`settings.py` file. See :doc:`../install` for details.
@ -58,7 +58,7 @@ Setting the property ID
Every website you track with Google Analytics gets its own property ID, Every website you track with Google Analytics gets its own property ID,
and the :ttag:`google_analytics` tag will include it in the rendered and the :ttag:`google_analytics` tag will include it in the rendered
JavaScript code. You can find the web property ID on the overview page Javascript code. You can find the web property ID on the overview page
of your account. Set :const:`GOOGLE_ANALYTICS_PROPERTY_ID` in the of your account. Set :const:`GOOGLE_ANALYTICS_PROPERTY_ID` in the
project :file:`settings.py` file:: project :file:`settings.py` file::
@ -72,7 +72,7 @@ Tracking multiple domains
The default code is suitable for tracking a single domain. If you track The default code is suitable for tracking a single domain. If you track
multiple domains, set the :const:`GOOGLE_ANALYTICS_TRACKING_STYLE` multiple domains, set the :const:`GOOGLE_ANALYTICS_TRACKING_STYLE`
setting to one of the :const:`analytical.templatetags.google_analytics.TRACK_*` setting to one of the :const:`analytical.google_analytics.SCOPE_*`
constants: constants:
============================= ===== ============================================= ============================= ===== =============================================
@ -87,7 +87,7 @@ Constant Value Description
============================= ===== ============================================= ============================= ===== =============================================
As noted, the default tracking style is As noted, the default tracking style is
:const:`~analytical.templatetags.google_analytics.TRACK_SINGLE_DOMAIN`. :const:`~analytical.google_analytics.TRACK_SINGLE_DOMAIN`.
When you track multiple (sub)domains, django-analytical needs to know When you track multiple (sub)domains, django-analytical needs to know
what domain name to pass to Google Analytics. If you use the contrib what domain name to pass to Google Analytics. If you use the contrib
@ -159,7 +159,7 @@ when your render a template containing the tracking code::
The value of the context variable is a tuple *(name, value, [scope])*. The value of the context variable is a tuple *(name, value, [scope])*.
The scope parameter is one of the The scope parameter is one of the
:const:`analytical.templatetags.google_analytics.SCOPE_*` constants: :const:`analytical.google_analytics.SCOPE_*` constants:
================= ====== ============================================= ================= ====== =============================================
Constant Value Description Constant Value Description
@ -171,7 +171,7 @@ Constant Value Description
``SCOPE_PAGE`` 3 Defines page-level activity. ``SCOPE_PAGE`` 3 Defines page-level activity.
================= ====== ============================================= ================= ====== =============================================
The default scope is :const:`~analytical.templatetags.google_analytics.SCOPE_PAGE`. The default scope is :const:`~analytical.google_analytics.SCOPE_PAGE`.
You may want to set custom variables in a context processor that you add You may want to set custom variables in a context processor that you add
to the :data:`TEMPLATE_CONTEXT_PROCESSORS` list in :file:`settings.py`:: to the :data:`TEMPLATE_CONTEXT_PROCESSORS` list in :file:`settings.py`::
@ -192,7 +192,7 @@ context processor, the latter clobbers the former.
.. _google-analytics-anonimyze-ips: .. _google-analytics-anonimyze-ips:
Anonymize IPs Anonymize IPs
------------- ----------------
You can enable the `IP anonymization`_ feature by setting the You can enable the `IP anonymization`_ feature by setting the
:const:`GOOGLE_ANALYTICS_ANONYMIZE_IP` configuration setting:: :const:`GOOGLE_ANALYTICS_ANONYMIZE_IP` configuration setting::
@ -205,65 +205,3 @@ concerning data privacy (e.g. Germany).
By default, IPs are not anonymized. By default, IPs are not anonymized.
.. _`IP anonymization`: https://support.google.com/analytics/bin/answer.py?hl=en&answer=2763052 .. _`IP anonymization`: https://support.google.com/analytics/bin/answer.py?hl=en&answer=2763052
.. _google-analytics-sample-rate:
Sample Rate
-----------
You can configure the `Sample Rate`_ feature by setting the
:const:`GOOGLE_ANALYTICS_SAMPLE_RATE` configuration setting::
GOOGLE_ANALYTICS_SAMPLE_RATE = 10
The value is a percentage and can be between 0 and 100 and can be a string or
decimal value of with up to two decimal places.
.. _`Sample Rate`: https://developers.google.com/analytics/devguides/collection/gajs/methods/gaJSApiBasicConfiguration#_setsamplerate
.. _google-analytics-site-speed-sample-rate:
Site Speed Sample Rate
----------------------
You can configure the `Site Speed Sample Rate`_ feature by setting the
:const:`GOOGLE_ANALYTICS_SITE_SPEED_SAMPLE_RATE` configuration setting::
GOOGLE_ANALYTICS_SITE_SPEED_SAMPLE_RATE = 10
The value is a percentage and can be between 0 and 100 and can be a string or
decimal value of with up to two decimal places.
.. _`Site Speed Sample Rate`: https://developers.google.com/analytics/devguides/collection/gajs/methods/gaJSApiBasicConfiguration#_setsitespeedsamplerate
.. _google-analytics-session-cookie-timeout:
Session Cookie Timeout
----------------------
You can configure the `Session Cookie Timeout`_ feature by setting the
:const:`GOOGLE_ANALYTICS_SESSION_COOKIE_TIMEOUT` configuration setting::
GOOGLE_ANALYTICS_SESSION_COOKIE_TIMEOUT = 3600000
The value is the session cookie timeout in milliseconds or 0 to delete the cookie when the browser is closed.
.. _`Session Cookie Timeout`: https://developers.google.com/analytics/devguides/collection/gajs/methods/gaJSApiBasicConfiguration#_setsessioncookietimeout
.. _google-analytics-visitor-cookie-timeout:
Visitor Cookie Timeout
----------------------
You can configure the `Visitor Cookie Timeout`_ feature by setting the
:const:`GOOGLE_ANALYTICS_VISITOR_COOKIE_TIMEOUT` configuration setting::
GOOGLE_ANALYTICS_VISITOR_COOKIE_TIMEOUT = 3600000
The value is the visitor cookie timeout in milliseconds or 0 to delete the cookie when the browser is closed.
.. _`Visitor Cookie Timeout`: https://developers.google.com/analytics/devguides/collection/gajs/methods/gaJSApiBasicConfiguration#_setvisitorcookietimeout

View file

@ -1,168 +0,0 @@
===============================================
Google Analytics (gtag.js) -- traffic analysis
===============================================
`Google Analytics`_ is the well-known web analytics service from
Google. The product is aimed more at marketers than webmasters or
technologists, supporting integration with AdWords and other e-commence
features. The global site tag (`gtag.js`_) is a JavaScript tagging
framework and API that allows you to send event data to Google Analytics,
Google Ads, and Google Marketing Platform.
.. _`Google Analytics`: http://www.google.com/analytics/
.. _`gtag.js`: https://developers.google.com/analytics/devguides/collection/gtagjs/
.. google-analytics-installation:
Installation
============
To start using the Google Analytics 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 Google Analytics 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:`google-analytics-configuration-gtag`.
The Google Analytics tracking code is inserted into templates using a
template tag. Load the :mod:`google_analytics_gtag` template tag library and
insert the :ttag:`google_analytics_gtag` 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 head::
{% load google_analytics_gtag %}
<html>
<head>
{% google_analytics_gtag %}
...
</head>
...
.. _google-analytics-configuration-gtag:
Configuration
=============
Before you can use the Google Analytics integration, you must first set
your website property ID. If you track multiple domains with the same
code, you also need to set-up the domain. Finally, you can add custom
segments for Google Analytics to track.
.. _google-analytics-gtag-property-id:
Setting the property ID
-----------------------
Every website you track with Google Analytics gets its own property ID,
and the :ttag:`google_analytics_gtag` tag will include it in the rendered
JavaScript code. You can find the web property ID on the overview page
of your account. Set :const:`GOOGLE_ANALYTICS_GTAG_PROPERTY_ID` in the
project :file:`settings.py` file::
GOOGLE_ANALYTICS_GTAG_PROPERTY_ID = 'UA-XXXXXX-X'
If you do not set a property ID, the tracking code will not be rendered.
Please note that the accepted Property IDs should be one of the following formats:
- 'UA-XXXXXX-Y'
- 'AW-XXXXXXXXXX'
- 'G-XXXXXXXX'
- 'DC-XXXXXXXX'
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:`GOOGLE_ANALYTICS_INTERNAL_IPS`
setting, the tracking code is commented out. It takes the value of
:const:`ANALYTICAL_INTERNAL_IPS` by default (which in turn is
:const:`INTERNAL_IPS` by default). See :ref:`identifying-visitors` for
important information about detecting the visitor IP address.
.. _google-analytics-identify-user:
Identifying authenticated users
-------------------------------
The username of an authenticated user is passed to Google Analytics
automatically as the ``user_id``. See :ref:`identifying-visitors`.
According to `Google Analytics conditions`_ you should avoid
sending Personally Identifiable Information.
Using ``username`` as ``user_id`` might not be the best option.
To avoid that, you can change the identity
by setting ``google_analytics_gtag_identity`` (or ``analytical_identity`` to
affect all providers) context variable:
.. code-block:: python
context = RequestContext({'google_analytics_gtag_identity': user.uuid})
return some_template.render(context)
or in the template:
.. code-block:: django
{% with google_analytics_gtag_identity=request.user.uuid|default:None %}
{% analytical_head_top %}
{% endwith %}
.. _`Google Analytics conditions`: https://developers.google.com/analytics/solutions/crm-integration#user_id
.. _google-analytics-custom-dimensions:
Custom dimensions
----------------
As described in the Google Analytics `custom dimensions`_ documentation
page, you can define custom dimensions which are variables specific to your
business needs. These variables can include both custom event parameters as
well as customer user properties. Using the template context variable
``google_analytics_custom_dimensions``, you can let the :ttag:`google_analytics_gtag`
pass custom dimensions to Google Analytics automatically. The ``google_analytics_custom_dimensions``
variable must be set to a dictionary where the keys are the dimension names
and the values are the dimension values. You can set the context variable in your
view when you render a template containing the tracking code::
.. code-block:: python
context = RequestContext({
'google_analytics_custom_dimensions': {
'gender': 'female',
'country': 'US',
'user_properties': {
'age': 25
}
}
})
return some_template.render(context)
Note that the ``user_properties`` key is used to pass user properties to Google
Analytics. It's not necessary to always use this key, but that'd be the way of
sending user properties to Google Analytics automatically.
You may want to set custom dimensions in a context processor that you add
to the :data:`TEMPLATE_CONTEXT_PROCESSORS` list in :file:`settings.py`::
.. code-block:: python
def google_analytics_segment_language(request):
try:
return {'google_analytics_custom_dimensions': {'language': request.LANGUAGE_CODE}}
except AttributeError:
return {}
Just remember that if you set the same context variable in the
:class:`~django.template.context.RequestContext` constructor and in a
context processor, the latter clobbers the former.
.. _`custom dimensions`: https://support.google.com/analytics/answer/10075209

View file

@ -1,238 +0,0 @@
====================================================
Google Analytics (analytics.js) -- traffic analysis
====================================================
`Google Analytics`_ is the well-known web analytics service from
Google. The product is aimed more at marketers than webmasters or
technologists, supporting integration with AdWords and other e-commence
features. The `analytics.js`_ library (also known as "the Google
Analytics tag") is a JavaScript library for measuring how users interact
with your website.
.. _`Google Analytics`: http://www.google.com/analytics/
.. _`analytics.js`: https://developers.google.com/analytics/devguides/collection/analyticsjs/
.. google-analytics-installation:
Installation
============
To start using the Google Analytics 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 Google Analytics 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:`google-analytics-configuration-js`.
The Google Analytics tracking code is inserted into templates using a
template tag. Load the :mod:`google_analytics_js` template tag library and
insert the :ttag:`google_analytics_js` 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 head::
{% load google_analytics_js %}
<html>
<head>
...
{% google_analytics_js %}
</head>
...
.. _google-analytics-configuration-js:
Configuration
=============
Before you can use the Google Analytics integration, you must first set
your website property ID. If you track multiple domains with the same
code, you also need to set-up the domain. Finally, you can add custom
segments for Google Analytics to track.
.. _google-analytics-js-property-id:
Setting the property ID
-----------------------
Every website you track with Google Analytics gets its own property ID,
and the :ttag:`google_analytics_js` tag will include it in the rendered
JavaScript code. You can find the web property ID on the overview page
of your account. Set :const:`GOOGLE_ANALYTICS_JS_PROPERTY_ID` in the
project :file:`settings.py` file::
GOOGLE_ANALYTICS_JS_PROPERTY_ID = 'UA-XXXXXXXX-X'
If you do not set a property ID, the tracking code will not be rendered.
Tracking multiple domains
-------------------------
The default code is suitable for tracking a single domain. If you track
multiple domains, set the :const:`GOOGLE_ANALYTICS_TRACKING_STYLE`
setting to one of the :const:`analytical.templatetags.google_analytics_js.TRACK_*`
constants:
============================= ===== =============================================
Constant Value Description
============================= ===== =============================================
``TRACK_SINGLE_DOMAIN`` 1 Track one domain.
``TRACK_MULTIPLE_SUBDOMAINS`` 2 Track multiple subdomains of the same top
domain (e.g. `fr.example.com` and
`nl.example.com`).
``TRACK_MULTIPLE_DOMAINS`` 3 Track multiple top domains (e.g. `example.fr`
and `example.nl`).
============================= ===== =============================================
As noted, the default tracking style is
:const:`~analytical.templatetags.google_analytics_js.TRACK_SINGLE_DOMAIN`.
When you track multiple (sub)domains, django-analytical needs to know
what domain name to pass to Google Analytics. If you use the contrib
sites app, the domain is automatically picked up from the current
:const:`~django.contrib.sites.models.Site` instance. Otherwise, you may
either pass the domain to the template tag through the context variable
:const:`google_analytics_domain` (fallback: :const:`analytical_domain`)
or set it in the project :file:`settings.py` file using
:const:`GOOGLE_ANALYTICS_DOMAIN` (fallback: :const:`ANALYTICAL_DOMAIN`).
Display Advertising
-------------------
Display Advertising allows you to view Demographics and Interests reports,
add Remarketing Lists and support DoubleClick Campain Manager integration.
You can enable `Display Advertising features`_ by setting the
:const:`GOOGLE_ANALYTICS_DISPLAY_ADVERTISING` configuration setting::
GOOGLE_ANALYTICS_DISPLAY_ADVERTISING = True
By default, display advertising features are disabled.
.. _`Display Advertising features`: https://support.google.com/analytics/answer/3450482
.. _google-analytics-js-internal-ips:
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:`GOOGLE_ANALYTICS_INTERNAL_IPS`
setting, the tracking code is commented out. It takes the value of
:const:`ANALYTICAL_INTERNAL_IPS` by default (which in turn is
:const:`INTERNAL_IPS` by default). See :ref:`identifying-visitors` for
important information about detecting the visitor IP address.
.. _google-analytics-js-custom-variables:
Custom variables
----------------
As described in the Google Analytics `custom variables`_ documentation
page, you can define custom segments. Using template context variables
``google_analytics_var1`` through ``google_analytics_var5``, you can let
the :ttag:`google_analytics_js` tag pass custom variables to Google
Analytics automatically. You can set the context variables in your view
when your render a template containing the tracking code::
context = RequestContext({'google_analytics_var1': ('gender', 'female'),
'google_analytics_var2': ('visit', 1)})
return some_template.render(context)
The value of the context variable is a tuple *(name, value)*.
You may want to set custom variables in a context processor that you add
to the :data:`TEMPLATE_CONTEXT_PROCESSORS` list in :file:`settings.py`::
def google_analytics_segment_language(request):
try:
return {'google_analytics_var3': request.LANGUAGE_CODE}
except AttributeError:
return {}
Just remember that if you set the same context variable in the
:class:`~django.template.context.RequestContext` constructor and in a
context processor, the latter clobbers the former.
.. _`custom variables`: https://developers.google.com/analytics/devguides/collection/upgrade/reference/gajs-analyticsjs#custom-vars
.. _google-analytics-js-anonimyze-ips:
Anonymize IPs
-------------
You can enable the `IP anonymization`_ feature by setting the
:const:`GOOGLE_ANALYTICS_ANONYMIZE_IP` configuration setting::
GOOGLE_ANALYTICS_ANONYMIZE_IP = True
This may be mandatory for deployments in countries that have a firm policies
concerning data privacy (e.g. Germany).
By default, IPs are not anonymized.
.. _`IP anonymization`: https://support.google.com/analytics/bin/answer.py?hl=en&answer=2763052
.. _google-analytics-js-sample-rate:
Sample Rate
-----------
You can configure the `Sample Rate`_ feature by setting the
:const:`GOOGLE_ANALYTICS_SAMPLE_RATE` configuration setting::
GOOGLE_ANALYTICS_SAMPLE_RATE = 10
The value is a percentage and can be between 0 and 100 and can be a string or
integer value.
.. _`Sample Rate`: https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#sampleRate
.. _google-analytics-js-site-speed-sample-rate:
Site Speed Sample Rate
----------------------
You can configure the `Site Speed Sample Rate`_ feature by setting the
:const:`GOOGLE_ANALYTICS_SITE_SPEED_SAMPLE_RATE` configuration setting::
GOOGLE_ANALYTICS_SITE_SPEED_SAMPLE_RATE = 10
The value is a percentage and can be between 0 and 100 and can be a string or
integer value.
.. _`Site Speed Sample Rate`: https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#siteSpeedSampleRate
.. _google-analytics-cookie-expiration:
Cookie Expiration
-----------------
You can configure the `Cookie Expiration`_ feature by setting the
:const:`GOOGLE_ANALYTICS_COOKIE_EXPIRATION` configuration setting::
GOOGLE_ANALYTICS_COOKIE_EXPIRATION = 3600000
The value is the cookie expiration in seconds or 0 to delete the cookie when the browser is closed.
.. _`Cookie Expiration`: https://developers.google.com/analytics/devguides/collection/gajs/methods/gaJSApiBasicConfiguration#_setsessioncookietimeout
Custom JavaScript Source
------------------------
You can configure a custom URL for the javascript file by setting the
:const:`GOOGLE_ANALYTICS_JS_SOURCE` configuration setting::
GOOGLE_ANALYTICS_JS_SOURCE = 'https://www.example.com/analytics.js'

View file

@ -1,59 +0,0 @@
=====================================
Heap -- analytics and events tracking
=====================================
`Heap`_ automatically captures all user interactions on your site, from the moment of installation forward.
.. _`Heap`: https://heap.io/
.. heap-installation:
Installation
============
To start using the Heap 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.
.. _heap-configuration:
Configuration
=============
Before you can use the Heap integration, you must first get your
Heap Tracker ID. If you don't have a Heap account yet,
`sign up`_ to get your Tracker ID.
.. _`sign up`: https://heap.io/
.. _heap-tracker-id:
Setting the Tracker ID
----------------------
Heap gives you a unique ID. You can find this ID on the Projects page
of your Heap account. Set :const:`HEAP_TRACKER_ID` in the project
:file:`settings.py` file::
HEAP_TRACKER_ID = 'XXXXXXXX'
If you do not set an Tracker ID, the tracking code will not be
rendered.
The tracking code will be added just before the closing head tag.
.. _heap-internal-ips:
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` setting
(which is :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,73 +0,0 @@
=====================================
Hotjar -- analytics and user feedback
=====================================
`Hotjar`_ is a website analytics and user feedback tool.
.. _`Hotjar`: https://www.hotjar.com/
.. hotjar-installation:
Installation
============
To start using the Hotjar 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 Hotjar 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:`hotjar-configuration`.
The Hotjar code is inserted into templates using template tags.
Because every page that you want to track must have the tag,
it is useful to add it to your base template.
At the top of the template, load the :mod:`hotjar` template tag library.
Then insert the :ttag:`hotjar` tag at the bottom of the head section::
{% load hotjar %}
<html>
<head>
...
{% hotjar %}
</head>
...
</html>
.. _hotjar-configuration:
Configuration
=============
Before you can use the Hotjar integration, you must first set your Site ID.
.. _hotjar-id:
Setting the Hotjar Site ID
--------------------------
You can find the Hotjar Site ID in the "Sites & Organizations" section of your Hotjar account.
Set :const:`HOTJAR_SITE_ID` in the project :file:`settings.py` file::
HOTJAR_SITE_ID = 'XXXXXXXXX'
If you do not set a Hotjar Site ID, the code will not be rendered.
.. _hotjar-internal-ips:
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:`HOTJAR_INTERNAL_IPS`
setting, the tracking code is commented out. It takes the value of
:const:`ANALYTICAL_INTERNAL_IPS` by default (which in turn is
:const:`INTERNAL_IPS` by default). See :ref:`identifying-visitors` for
important information about detecting the visitor IP address.

View file

@ -23,7 +23,7 @@ This step is only needed if you are not using the generic
:ttag:`analytical.*` tags. If you are, skip to :ttag:`analytical.*` tags. If you are, skip to
:ref:`intercom-configuration`. :ref:`intercom-configuration`.
The Intercom.io JavaScript code is inserted into templates using a The Intercom.io Javascript code is inserted into templates using a
template tag. Load the :mod:`intercom` template tag library and template tag. Load the :mod:`intercom` template tag library and
insert the :ttag:`intercom` tag. Because every page that you want to insert the :ttag:`intercom` tag. Because every page that you want to
track must have the tag, it is useful to add it to your base template. track must have the tag, it is useful to add it to your base template.
@ -55,7 +55,7 @@ Setting the app id
-------------------------- --------------------------
Intercom.io gives you a unique app id, and the :ttag:`intercom` Intercom.io gives you a unique app id, and the :ttag:`intercom`
tag will include it in the rendered JavaScript code. You can find your tag will include it in the rendered Javascript code. You can find your
app id by clicking the *Tracking Code* link when logged into app id by clicking the *Tracking Code* link when logged into
the on the intercom.io website. A page will display containing the on the intercom.io website. A page will display containing
HTML code looking like this:: HTML code looking like this::
@ -71,7 +71,7 @@ file::
INTERCOM_APP_ID = 'XXXXXXXXXXXXXXXXXXXXXXX' INTERCOM_APP_ID = 'XXXXXXXXXXXXXXXXXXXXXXX'
If you do not set an app id, the JavaScript code will not be If you do not set an app id, the Javascript code will not be
rendered. rendered.
@ -120,41 +120,22 @@ Context variable Description
-------------------- ------------------------------------------- -------------------- -------------------------------------------
``intercom_email`` The visitor's email address. ``intercom_email`` The visitor's email address.
-------------------- ------------------------------------------- -------------------- -------------------------------------------
``intercom_user_id`` The visitor's user id.
-------------------- -------------------------------------------
``created_at`` The date the visitor created an account ``created_at`` The date the visitor created an account
==================== =========================================== ==================== ===========================================
.. _`custom visitor data`: https://www.intercom.com/help/configure-intercom-for-your-product-or-site/customize-intercom-to-be-about-your-users/send-custom-user-attributes-to-intercom .. _`custom visitor data`: http://docs.intercom.io/custom-data/adding-custom-data
Identifying authenticated users Identifying authenticated users
------------------------------- -------------------------------
If you have not set the ``intercom_name``, ``intercom_email``, or ``intercom_user_id`` variables If you have not set the ``intercom_name`` or ``intercom_email`` variables
explicitly, the username and email address of an authenticated user are explicitly, the username and email address of an authenticated user are
passed to Intercom automatically. See :ref:`identifying-visitors`. passed to Intercom automatically. See :ref:`identifying-visitors`.
.. _intercom-internal-ips: .. _intercom-internal-ips:
Verifying identified users
--------------------------
Intercom supports HMAC authentication of users identified by user ID or email, in order to prevent impersonation.
For more information, see `Enable identity verification on your web product`_ in the Intercom documentation.
To enable this, configure your Intercom account's HMAC secret key::
INTERCOM_HMAC_SECRET_KEY = 'XXXXXXXXXXXXXXXXXXXXXXX'
(You can find this secret key under the "Identity verification" section of your Intercom account settings page.)
.. _`Enable identity verification on your web product`: https://www.intercom.com/help/configure-intercom-for-your-product-or-site/staying-secure/enable-identity-verification-on-your-web-product
Internal IP addresses Internal IP addresses
--------------------- ---------------------

View file

@ -54,10 +54,10 @@ Setting the account number and site code
In order to install the survey code, you need to set your KISSinsights In order to install the survey code, you need to set your KISSinsights
account number and website code. The :ttag:`kiss_insights` tag will account number and website code. The :ttag:`kiss_insights` tag will
include it in the rendered JavaScript code. You can find the account include it in the rendered Javascript code. You can find the account
number and website code by visiting the code installation page of the number and website code by visiting the code installation page of the
website you want to place the surveys on. You will see some HTML code website you want to place the surveys on. You will see some HTML code
with a JavaScript tag with a ``src`` attribute containing with a Javascript tag with a ``src`` attribute containing
``//s3.amazonaws.com/ki.js/XXXXX/YYY.js``. Here ``XXXXX`` is the ``//s3.amazonaws.com/ki.js/XXXXX/YYY.js``. Here ``XXXXX`` is the
account number and ``YYY`` the website code. Set account number and ``YYY`` the website code. Set
:const:`KISS_INSIGHTS_ACCOUNT_NUMBER` and :const:`KISS_INSIGHTS_ACCOUNT_NUMBER` and

View file

@ -24,7 +24,7 @@ This step is only needed if you are not using the generic
:ttag:`analytical.*` tags. If you are, skip to :ttag:`analytical.*` tags. If you are, skip to
:ref:`kiss-metrics-configuration`. :ref:`kiss-metrics-configuration`.
The KISSmetrics JavaScript code is inserted into templates using a The KISSmetrics Javascript code is inserted into templates using a
template tag. Load the :mod:`kiss_metrics` template tag library and template tag. Load the :mod:`kiss_metrics` template tag library and
insert the :ttag:`kiss_metrics` tag. Because every page that you want insert the :ttag:`kiss_metrics` tag. Because every page that you want
to track must have the tag, it is useful to add it to your base to track must have the tag, it is useful to add it to your base
@ -53,7 +53,7 @@ Setting the API key
Every website you track events for with KISSmetrics gets its own API Every website you track events for with KISSmetrics gets its own API
key, and the :ttag:`kiss_metrics` tag will include it in the rendered key, and the :ttag:`kiss_metrics` tag will include it in the rendered
JavaScript code. You can find the website API key by visiting the Javascript code. You can find the website API key by visiting the
website *Product center* on your KISSmetrics dashboard. Set website *Product center* on your KISSmetrics dashboard. Set
:const:`KISS_METRICS_API_KEY` in the project :file:`settings.py` file:: :const:`KISS_METRICS_API_KEY` in the project :file:`settings.py` file::
@ -144,7 +144,7 @@ For example::
}) })
return some_template.render(context) return some_template.render(context)
The output script tag will then include the corresponding JavaScript event as The output script tag will then include the corresponding Javascript event as
documented in the `KISSmetrics record API`_ docs. documented in the `KISSmetrics record API`_ docs.

View file

@ -1,75 +0,0 @@
==================================================
Lucky Orange -- All-in-one conversion optimization
==================================================
`Lucky Orange`_ is a website analytics and user feedback tool.
.. _`Lucky Orange`: https://www.luckyorange.com/
.. luckyorange-installation:
Installation
============
To start using the Lucky Orange 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 Lucky Orange 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:`luckyorange-configuration`.
The Lucky Orange tracking code is inserted into templates using template
tags. Because every page that you want to track must have the tag, it
is useful to add it to your base template. At the top of the template,
load the :mod:`luckyorange` template tag library. Then insert the
:ttag:`luckyorange` tag at the bottom of the head section::
{% load luckyorange %}
<html>
<head>
...
{% luckyorange %}
</head>
...
</html>
.. _luckyorange-configuration:
Configuration
=============
Before you can use the Lucky Orange integration, you must first set your
Site ID.
.. _luckyorange-id:
Setting the Lucky Orange Site ID
--------------------------------
You can find the Lucky Orange Site ID in the "Settings" of your Lucky
Orange account, reachable via the gear icon on the top right corner.
Set :const:`LUCKYORANGE_SITE_ID` in the project :file:`settings.py` file::
LUCKYORANGE_SITE_ID = 'XXXXXX'
If you do not set a Lucky Orange Site ID, the code will not be rendered.
.. _luckyorange-internal-ips:
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:`LUCKYORANGE_INTERNAL_IPS`
setting, the tracking code is commented out. It takes the value of
:const:`ANALYTICAL_INTERNAL_IPS` by default (which in turn is
:const:`INTERNAL_IPS` by default). See :ref:`identifying-visitors` for
important information about detecting the visitor IP address.

View file

@ -1,182 +0,0 @@
====================================================
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 between 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/
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:
`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 class="matomo_give_consent">Track me!</button>
# button to remove tracking consent
<button class="matomo_remove_consent">Don't track me anymore!</button>
.. _`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.

View file

@ -24,7 +24,7 @@ step is only needed if you are not using the generic
:ttag:`analytical.*` tags. If you are, skip to :ttag:`analytical.*` tags. If you are, skip to
:ref:`mixpanel-configuration`. :ref:`mixpanel-configuration`.
The Mixpanel JavaScript code is inserted into templates using a The Mixpanel Javascript code is inserted into templates using a
template tag. Load the :mod:`mixpanel` template tag library and template tag. Load the :mod:`mixpanel` template tag library and
insert the :ttag:`mixpanel` tag. Because every page that you want insert the :ttag:`mixpanel` tag. Because every page that you want
to track must have the tag, it is useful to add it to your base to track must have the tag, it is useful to add it to your base
@ -53,7 +53,7 @@ Setting the token
----------------- -----------------
Every website you track events for with Mixpanel gets its own token, Every website you track events for with Mixpanel gets its own token,
and the :ttag:`mixpanel` tag will include it in the rendered JavaScript and the :ttag:`mixpanel` tag will include it in the rendered Javascript
code. You can find the project token on the Mixpanel *projects* page. code. You can find the project token on the Mixpanel *projects* page.
Set :const:`MIXPANEL_API_TOKEN` in the project :file:`settings.py` Set :const:`MIXPANEL_API_TOKEN` in the project :file:`settings.py`
file:: file::
@ -137,16 +137,16 @@ For example::
Tracking events Tracking events
=============== ===============
The django-analytical app integrates the Mixpanel JavaScript API in The django-analytical app integrates the Mixpanel Javascript API in
templates. To tracking events in views or other parts of Django, you templates. To tracking events in views or other parts of Django, you
can use Wes Winham's `mixpanel-celery`_ package. can use Wes Winham's `mixpanel-celery`_ package.
If you want to track an event in JavaScript, use the asynchronous If you want to track an event in Javascript, use the asynchronous
notation, as described in the section titled notation, as described in the section titled
`"Asynchronous Tracking with JavaScript"`_ in the Mixpanel `"Asynchronous Tracking with Javascript"`_ in the Mixpanel
documentation. For example:: documentation. For example::
mixpanel.track("play-game", {"level": "12", "weapon": "sword", "character": "knight"}); mixpanel.track("play-game", {"level": "12", "weapon": "sword", "character": "knight"});
.. _mixpanel-celery: http://github.com/winhamwr/mixpanel-celery .. _mixpanel-celery: http://github.com/winhamwr/mixpanel-celery
.. _`"Asynchronous Tracking with JavaScript"`: http://mixpanel.com/api/docs/guides/integration/js#async .. _`"Asynchronous Tracking with Javascript"`: http://mixpanel.com/api/docs/guides/integration/js#async

Some files were not shown because too many files have changed in this diff Show more