Merge branch 'master' into woopra-config-options

This commit is contained in:
Peter Bittner 2019-01-12 01:03:05 +01:00 committed by GitHub
commit 31ee7414f4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
71 changed files with 1865 additions and 494 deletions

View file

@ -1,23 +1,39 @@
language: python
python: "3.5"
cache: pip
python:
- 2.7
- 3.4
- 3.5
- 3.6
env:
- DJANGO=1.7
- DJANGO=1.8
- DJANGO=1.9
- DJANGO=1.10
- DJANGO=1.11
- DJANGO=2.0
- DJANGO=2.1
matrix:
exclude:
# Python/Django combinations that aren't officially supported
- { python: 3.5, env: DJANGO=1.7 }
- { python: 3.6, env: DJANGO=1.7 }
- { python: 3.6, env: DJANGO=1.8 }
- { python: 3.6, env: DJANGO=1.9 }
- { python: 3.6, env: DJANGO=1.10 }
- { python: 2.7, env: DJANGO=2.0 }
- { python: 2.7, env: DJANGO=2.1 }
- { python: 3.4, env: DJANGO=2.1 }
include:
- { python: 3.6, env: TOXENV=flake8 }
- { python: 3.6, env: TOXENV=readme }
# Work around Travis Python 3.7 issue: https://github.com/travis-ci/travis-ci/issues/9815
- { python: 3.7, env: DJANGO=1.11, dist: xenial, sudo: true }
- { python: 3.7, env: DJANGO=2.0, dist: xenial, sudo: true }
- { python: 3.7, env: DJANGO=2.1, dist: xenial, sudo: true }
install:
# continue to support Python 3.2 (see issue #84)
- pip install "virtualenv<14.0.0"
- pip install coveralls tox
- pip install tox-travis
script:
- tox
env:
# NOTE: To generate (update) the env list run
# $ tox -l | sort | xargs -I ITEM echo " - TOXENV="ITEM
- TOXENV=py27-django17
- TOXENV=py27-django18
- TOXENV=py27-django19
- TOXENV=py32-django17
- TOXENV=py32-django18
- TOXENV=py33-django17
- TOXENV=py33-django18
- TOXENV=py34-django17
- TOXENV=py34-django18
- TOXENV=py34-django19
- TOXENV=py35-django18
- TOXENV=py35-django19

View file

@ -1,10 +1,12 @@
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`_, `Brad Pitcher`_, `Hugo Osvaldo Barrera`_,
`Nikolay Korotkiy`_, `Steve Schwarz`_, `Aleck Landgraf`_ and others.
The django-analytical package was originally written by `Joost Cassee`_
and is now maintained by the `Jazzband community`_, with contributions
from `Eric Davis`_, `Paul Oswald`_, `Uros Trebec`_, `Steven Skoczen`_,
`Pi 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`_,
`Brad Pitcher`_, `Hugo Osvaldo Barrera`_, `Nikolay Korotkiy`_,
`Steve Schwarz`_, `Aleck Landgraf`_, `Marc Bourqui`_,
`Diederik van der Boor`_, `Matthäus G. Chajdas`_ and others.
Included Javascript code snippets for integration of the analytics
services were written by the respective service providers.
@ -15,12 +17,13 @@ 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
.. _`Joost Cassee`: https://github.com/jcassee
.. _`Jazzband community`: https://jazzband.co/
.. _`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
.. _`Pi Delport`: https://github.com/pjdelport
.. _`Sandra Mau`: https://github.com/xthepoet
.. _`Simon Ye`: https://github.com/yesimon
.. _`Tinnet Coronam`: https://github.com/tinnet
@ -37,6 +40,9 @@ The work on Intercom was made possible by `GreenKahuna`_.
.. _`Nikolay Korotkiy`: https://github.com/sikmir
.. _`Steve Schwarz`: https://github.com/saschwarz
.. _`Aleck Landgraf`: https://github.com/alecklandgraf
.. _`Marc Bourqui`: https://github.com/mbourqui
.. _`Diederik van der Boor`: https://github.com/vdboor
.. _`Matthäus G. Chajdas`: https://github.com/Anteru
.. _`Analytical`: https://github.com/jkrall/analytical
.. _`Bateau Knowledge`: http://www.bateauknowledge.nl/
.. _`GreenKahuna`: http://www.greenkahuna.com/

View file

@ -1,5 +1,23 @@
Version 2.2.2
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
@ -78,7 +96,7 @@ Version 0.14.0
Version 0.13.0
--------------
* Add support for the KISSmetrics alias feature (Sandra Mau)
* Update testing code for Django 1.4 (Piet Delport)
* Update testing code for Django 1.4 (Pi Delport)
Version 0.12.0
--------------
@ -101,7 +119,7 @@ Version 0.11.0
--------------
* Added support for the Spring Metrics service.
* Allow sending events and properties to KISSmetrics (Paul Oswald).
* Add support for the Site Speed report in Google Analytics (Uros
* Add support for the Site Speed report in Google Analytics (Uros
Trebec).
Version 0.10.0
@ -121,7 +139,7 @@ Version 0.9.1
Version 0.9.0
-------------
* Updated Clicky tracking code to support multiple site ids.
* Fixed Chartbeat auto-domain bug when the Sites framework is not used
* Fixed Chartbeat auto-domain bug when the Sites framework is not used
(Eric Davis).
* Improved testing code (Eric Davis).

5
CONTRIBUTING.rst Normal file
View file

@ -0,0 +1,5 @@
.. 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,7 +1,7 @@
django-analytical |latest-version|
==================================
|travis-ci| |coveralls| |health| |python-support| |downloads| |license| |gitter|
|travis-ci| |coveralls| |health| |python-support| |license| |gitter| |jazzband|
The django-analytical application integrates analytics services into a
Django_ project.
@ -26,27 +26,27 @@ an asynchronous version of the Javascript code if possible.
.. |latest-version| image:: https://img.shields.io/pypi/v/django-analytical.svg
:alt: Latest version on PyPI
:target: https://pypi.python.org/pypi/django-analytical
.. |travis-ci| image:: https://travis-ci.org/jcassee/django-analytical.svg
.. |travis-ci| image:: https://img.shields.io/travis/jazzband/django-analytical/master.svg
:alt: Build status
:target: https://travis-ci.org/jcassee/django-analytical
.. |coveralls| image:: https://coveralls.io/repos/jcassee/django-analytical/badge.svg
:target: https://travis-ci.org/jazzband/django-analytical
.. |coveralls| image:: https://coveralls.io/repos/jazzband/django-analytical/badge.svg
:alt: Test coverage
:target: https://coveralls.io/r/jcassee/django-analytical
.. |health| image:: https://landscape.io/github/jcassee/django-analytical/master/landscape.svg?style=flat
:target: https://landscape.io/github/jcassee/django-analytical/master
:target: https://coveralls.io/r/jazzband/django-analytical
.. |health| image:: https://landscape.io/github/jazzband/django-analytical/master/landscape.svg?style=flat
:target: https://landscape.io/github/jazzband/django-analytical/master
:alt: Code health
.. |python-support| image:: https://img.shields.io/pypi/pyversions/django-analytical.svg
:target: https://pypi.python.org/pypi/django-analytical
:alt: Python versions
.. |downloads| image:: https://img.shields.io/pypi/dm/django-analytical.svg
:alt: Monthly downloads from PyPI
:target: https://pypi.python.org/pypi/django-analytical
.. |license| image:: https://img.shields.io/pypi/l/django-analytical.svg
:alt: Software license
:target: https://github.com/jcassee/django-analytical/blob/master/LICENSE.txt
:target: https://github.com/jazzband/django-analytical/blob/master/LICENSE.txt
.. |gitter| image:: https://badges.gitter.im/Join%20Chat.svg
:alt: Gitter chat room
:target: https://gitter.im/jcassee/django-analytical
:target: https://gitter.im/jazzband/django-analytical
.. |jazzband| image:: https://jazzband.co/static/img/badge.svg
:alt: Jazzband
:target: https://jazzband.co/
.. _`Django`: http://www.djangoproject.com/
Currently Supported Services
@ -56,9 +56,11 @@ Currently Supported Services
* `Clickmap`_ visual click tracking
* `Clicky`_ traffic analysis
* `Crazy Egg`_ visual click tracking
* `Facebook Pixel`_ advertising analytics
* `Gaug.es`_ real time web analytics
* `Google Analytics`_ traffic analysis
* `GoSquared`_ traffic monitoring
* `Hotjar`_ analytics and user feedback
* `HubSpot`_ inbound marketing
* `Intercom`_ live chat and support
* `KISSinsights`_ feedback surveys
@ -67,7 +69,7 @@ Currently Supported Services
* `Olark`_ visitor chat
* `Optimizely`_ A/B testing
* `Performable`_ web analytics and landing pages
* `Piwik`_ open source web analytics
* `Matomo (formerly Piwik)`_ open source web analytics
* `Rating\@Mail.ru`_ web analytics
* `SnapEngage`_ live chat
* `Spring Metrics`_ conversion tracking
@ -79,9 +81,11 @@ Currently Supported Services
.. _`Clickmap`: http://getclickmap.com/
.. _`Clicky`: http://getclicky.com/
.. _`Crazy Egg`: http://www.crazyegg.com/
.. _`Facebook Pixel`: https://developers.facebook.com/docs/facebook-pixel/
.. _`Gaug.es`: http://get.gaug.es/
.. _`Google Analytics`: http://www.google.com/analytics/
.. _`GoSquared`: http://www.gosquared.com/
.. _`Hotjar`: https://www.hotjar.com/
.. _`HubSpot`: http://www.hubspot.com/
.. _`Intercom`: http://www.intercom.io/
.. _`KISSinsights`: http://www.kissinsights.com/
@ -90,7 +94,7 @@ Currently Supported Services
.. _`Olark`: http://www.olark.com/
.. _`Optimizely`: http://www.optimizely.com/
.. _`Performable`: http://www.performable.com/
.. _`Piwik`: http://www.piwik.org/
.. _`Matomo (formerly Piwik)`: https://matomo.org
.. _`Rating\@Mail.ru`: http://top.mail.ru/
.. _`SnapEngage`: http://www.snapengage.com/
.. _`Spring Metrics`: http://www.springmetrics.com/
@ -107,9 +111,9 @@ GitHub`_. Bugs should be reported there, whereas for lengthy chats
and coding support when implementing new service integrations you're
welcome to use our `Gitter chat room`_.
.. _`read online`: https://packages.python.org/django-analytical/
.. _`hosted by GitHub`: https://github.com/jcassee/django-analytical
.. _`Gitter chat room`: https://gitter.im/jcassee/django-analytical
.. _`read online`: https://django-analytical.readthedocs.io/
.. _`hosted by GitHub`: https://github.com/jazzband/django-analytical
.. _`Gitter chat room`: https://gitter.im/jazzband/django-analytical
How To Contribute
-----------------
@ -123,7 +127,13 @@ services to support, or suggesting documentation improvements, use the
the repository, make changes and place a `pull request`_. Creating an
issue to discuss your plans is useful.
.. _`issue tracker`: https://github.com/jcassee/django-analytical/issues
.. _`pull request`: https://github.com/jcassee/django-analytical/pulls
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
.. _`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,15 +1,9 @@
"""
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/
Analytics service integration for Django projects
"""
__author__ = "Joost Cassee"
__email__ = "joost@cassee.net"
__version__ = "2.2.2"
__copyright__ = "Copyright (C) 2011-2016 Joost Cassee and others"
__version__ = "2.5.0"
__copyright__ = "Copyright (C) 2011-2017 Joost Cassee and others"
__license__ = "MIT License"

View file

@ -20,9 +20,12 @@ TAG_MODULES = [
'analytical.clickmap',
'analytical.clicky',
'analytical.crazy_egg',
'analytical.facebook_pixel',
'analytical.gauges',
'analytical.google_analytics',
'analytical.google_analytics_js',
'analytical.gosquared',
'analytical.hotjar',
'analytical.hubspot',
'analytical.intercom',
'analytical.kiss_insights',

View file

@ -35,7 +35,7 @@ SETUP_CODE = """
loadChartbeat : function() { oldonload(); loadChartbeat(); };
})();
</script>
"""
""" # noqa
DOMAIN_CONTEXT_KEY = 'chartbeat_domain'
@ -80,7 +80,7 @@ def chartbeat_bottom(parser, token):
class ChartbeatBottomNode(Node):
def __init__(self):
self.user_id = get_required_setting('CHARTBEAT_USER_ID', USER_ID_RE,
"must be (a string containing) a number")
"must be (a string containing) a number")
def render(self, context):
config = {'uid': self.user_id}

View file

@ -29,8 +29,7 @@ TRACKING_CODE = """
})();
</script>
<noscript><p><img alt="Clicky" width="1" height="1" src="//in.getclicky.com/%(site_id)sns.gif" /></p></noscript>
"""
""" # noqa
register = Library()
@ -52,8 +51,9 @@ def clicky(parser, token):
class ClickyNode(Node):
def __init__(self):
self.site_id = get_required_setting('CLICKY_SITE_ID', SITE_ID_RE,
"must be a (string containing) a number")
self.site_id = get_required_setting(
'CLICKY_SITE_ID', SITE_ID_RE,
"must be a (string containing) a number")
def render(self, context):
custom = {}
@ -66,8 +66,10 @@ class ClickyNode(Node):
if identity is not None:
custom.setdefault('session', {})['username'] = identity
html = TRACKING_CODE % {'site_id': self.site_id,
'custom': json.dumps(custom, sort_keys=True)}
html = TRACKING_CODE % {
'site_id': self.site_id,
'custom': json.dumps(custom, sort_keys=True),
}
if is_internal_ip(context, 'CLICKY'):
html = disable_html(html, 'Clicky')
return html

View file

@ -0,0 +1,97 @@
"""
Facebook Pixel template tags and filters.
"""
from __future__ import absolute_import
import re
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import get_required_setting, is_internal_ip, disable_html
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

@ -1,5 +1,7 @@
"""
Google Analytics template tags and filters.
DEPRECATED
"""
from __future__ import absolute_import
@ -32,7 +34,6 @@ SETUP_CODE = """
var _gaq = _gaq || [];
_gaq.push(['_setAccount', '%(property_id)s']);
_gaq.push(['_trackPageview']);
%(commands)s
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
@ -44,15 +45,16 @@ SETUP_CODE = """
"""
DOMAIN_CODE = "_gaq.push(['_setDomainName', '%s']);"
NO_ALLOW_HASH_CODE = "_gaq.push(['_setAllowHash', false]);"
TRACK_PAGE_VIEW = "_gaq.push(['_trackPageview']);"
ALLOW_LINKER_CODE = "_gaq.push(['_setAllowLinker', true]);"
CUSTOM_VAR_CODE = "_gaq.push(['_setCustomVar', %(index)s, '%(name)s', " \
"'%(value)s', %(scope)s]);"
SITE_SPEED_CODE = "_gaq.push(['_trackPageLoadTime']);"
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']);"
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'")
DISPLAY_ADVERTISING_SOURCE = ("'https://' : 'http://'", "'stats.g.doubleclick.net/dc.js'")
@ -87,14 +89,17 @@ class GoogleAnalyticsNode(Node):
commands = self._get_domain_commands(context)
commands.extend(self._get_custom_var_commands(context))
commands.extend(self._get_other_commands(context))
commands.append(TRACK_PAGE_VIEW)
if getattr(settings, 'GOOGLE_ANALYTICS_DISPLAY_ADVERTISING', False):
source = DISPLAY_ADVERTISING_SOURCE
else:
source = DEFAULT_SOURCE
html = SETUP_CODE % {'property_id': self.property_id,
'commands': " ".join(commands),
'source_scheme': source[0],
'source_url': source[1]}
html = SETUP_CODE % {
'property_id': self.property_id,
'commands': " ".join(commands),
'source_scheme': source[0],
'source_url': source[1],
}
if is_internal_ip(context, 'GOOGLE_ANALYTICS'):
html = disable_html(html, 'Google Analytics')
return html
@ -109,8 +114,7 @@ class GoogleAnalyticsNode(Node):
domain = get_domain(context, 'google_analytics')
if domain is None:
raise AnalyticalException(
"tracking multiple domains with Google Analytics"
" requires a domain name")
"tracking multiple domains with Google Analytics requires a domain name")
commands.append(DOMAIN_CODE % domain)
commands.append(NO_ALLOW_HASH_CODE)
if tracking_type == TRACK_MULTIPLE_DOMAINS:
@ -157,7 +161,8 @@ class GoogleAnalyticsNode(Node):
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")
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)

View file

@ -0,0 +1,155 @@
"""
Google Analytics template tags and filters, using the new analytics.js library.
"""
from __future__ import absolute_import
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','https://www.google-analytics.com/analytics.js','ga');
ga('create', '{property_id}', 'auto', {create_fields});
{display_features}
ga('send', 'pageview');
{commands}
</script>
"""
REQUIRE_DISPLAY_FEATURES = "ga('require', 'displayfeatures');"
CUSTOM_VAR_CODE = "ga('set', '{name}', {value});"
ANONYMIZE_IP_CODE = "ga('set', 'anonymizeIp', true);"
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)
html = SETUP_CODE.format(
property_id=self.property_id,
create_fields=json.dumps(create_fields),
display_features=REQUIRE_DISPLAY_FEATURES if display_features else '',
commands=" ".join(commands),
)
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 = "'{}'".format(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

@ -26,7 +26,7 @@ TRACKING_CODE = """
w.addEventListener?w.addEventListener("load",gs,false):w.attachEvent("onload",gs);
})(window);
</script>
"""
""" # noqa
TOKEN_CODE = 'GoSquared.acct = "%s";'
IDENTIFY_CODE = 'GoSquared.UserName = "%s";'

View file

@ -0,0 +1,65 @@
"""
Hotjar template tags and filters.
"""
from __future__ import absolute_import
import re
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import get_required_setting, is_internal_ip, disable_html
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

@ -23,7 +23,7 @@ TRACKING_CODE = """
})(document,"script","hs-analytics",300000);
</script>
<!-- End of Async HubSpot Analytics Code -->
"""
""" # noqa
register = Library()
@ -44,8 +44,8 @@ def hubspot(parser, token):
class HubSpotNode(Node):
def __init__(self):
self.portal_id = get_required_setting('HUBSPOT_PORTAL_ID',
PORTAL_ID_RE, "must be a (string containing a) number")
self.portal_id = get_required_setting('HUBSPOT_PORTAL_ID', PORTAL_ID_RE,
"must be a (string containing a) number")
def render(self, context):
html = TRACKING_CODE % {'portal_id': self.portal_id}

View file

@ -3,26 +3,68 @@ intercom.io template tags and filters.
"""
from __future__ import absolute_import
import hashlib
import hmac
import json
import sys
import time
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, get_user_from_context, get_identity
is_internal_ip, get_user_from_context, get_identity, \
get_user_is_authenticated
APP_ID_RE = re.compile(r'[\da-f]+$')
APP_ID_RE = re.compile(r'[\da-z]+$')
TRACKING_CODE = """
<script id="IntercomSettingsScriptTag">
window.intercomSettings = %(settings_json)s;
</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()
def _timestamp(when): # type: (datetime) -> float
"""
Python 2 compatibility for `datetime.timestamp()`.
"""
return (time.mktime(when.timetuple()) if sys.version_info < (3,) else
when.timestamp())
def _hashable_bytes(data): # type: (AnyStr) -> bytes
"""
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): # type: (AnyStr) -> Optional[str]
"""
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
def intercom(parser, token):
"""
@ -58,31 +100,39 @@ class IntercomNode(Node):
params[var[9:]] = val
user = get_user_from_context(context)
if user is not None and user.is_authenticated():
if user is not None and get_user_is_authenticated(user):
if 'name' not in params:
params['name'] = get_identity(
context, 'intercom', self._identify, user)
if 'email' not in params and user.email:
params['email'] = user.email
params['created_at'] = int(time.mktime(
user.date_joined.timetuple()))
params.setdefault('user_id', user.pk)
params['created_at'] = int(_timestamp(user.date_joined))
else:
params['created_at'] = None
# Generate a user_hash HMAC to verify the user's identity, if configured.
# (If both user_id and email are present, the user_id field takes precedence.)
# See:
# https://www.intercom.com/help/configure-intercom-for-your-product-or-site/staying-secure/enable-identity-verification-on-your-web-product
user_hash_data = params.get('user_id', params.get('email')) # type: Optional[str]
if user_hash_data:
user_hash = intercom_user_hash(str(user_hash_data)) # type: Optional[str]
if user_hash is not None:
params.setdefault('user_hash', user_hash)
return params
def render(self, context):
user = get_user_from_context(context)
params = self._get_custom_attrs(context)
params["app_id"] = self.app_id
html = TRACKING_CODE % {
"settings_json": json.dumps(params, sort_keys=True)
}
if is_internal_ip(context, 'INTERCOM') \
or not user or not user.is_authenticated():
# Intercom is disabled for non-logged in users.
if is_internal_ip(context, 'INTERCOM'):
html = disable_html(html, 'Intercom')
return html

View file

@ -16,7 +16,7 @@ SITE_CODE_RE = re.compile(r'^[\w]+$')
SETUP_CODE = """
<script type="text/javascript">var _kiq = _kiq || []; %(commands)s</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']);"
SHOW_SURVEY_CODE = "_kiq.push(['showSurvey', %s]);"
SHOW_SURVEY_CONTEXT_KEY = 'kiss_insights_show_survey'
@ -44,10 +44,11 @@ def kiss_insights(parser, token):
class KissInsightsNode(Node):
def __init__(self):
self.account_number = get_required_setting(
'KISS_INSIGHTS_ACCOUNT_NUMBER', ACCOUNT_NUMBER_RE,
"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")
'KISS_INSIGHTS_ACCOUNT_NUMBER', ACCOUNT_NUMBER_RE,
"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")
def render(self, context):
commands = []
@ -55,12 +56,14 @@ class KissInsightsNode(Node):
if identity is not None:
commands.append(IDENTIFY_CODE % identity)
try:
commands.append(SHOW_SURVEY_CODE
% context[SHOW_SURVEY_CONTEXT_KEY])
commands.append(SHOW_SURVEY_CODE % context[SHOW_SURVEY_CONTEXT_KEY])
except KeyError:
pass
html = SETUP_CODE % {'account_number': self.account_number,
'site_code': self.site_code, 'commands': " ".join(commands)}
html = SETUP_CODE % {
'account_number': self.account_number,
'site_code': self.site_code,
'commands': " ".join(commands),
}
return html

View file

@ -61,9 +61,9 @@ def kiss_metrics(parser, token):
class KissMetricsNode(Node):
def __init__(self):
self.api_key = get_required_setting('KISS_METRICS_API_KEY',
API_KEY_RE,
"must be a string containing a 40-digit hexadecimal number")
self.api_key = get_required_setting(
'KISS_METRICS_API_KEY', API_KEY_RE,
"must be a string containing a 40-digit hexadecimal number")
def render(self, context):
commands = []
@ -78,18 +78,23 @@ class KissMetricsNode(Node):
pass
try:
name, properties = context[EVENT_CONTEXT_KEY]
commands.append(EVENT_CODE % {'name': name,
'properties': json.dumps(properties, sort_keys=True)})
commands.append(EVENT_CODE % {
'name': name,
'properties': json.dumps(properties, sort_keys=True),
})
except KeyError:
pass
try:
properties = context[PROPERTY_CONTEXT_KEY]
commands.append(PROPERTY_CODE % {
'properties': json.dumps(properties, sort_keys=True)})
'properties': json.dumps(properties, sort_keys=True),
})
except KeyError:
pass
html = TRACKING_CODE % {'api_key': self.api_key,
'commands': " ".join(commands)}
html = TRACKING_CODE % {
'api_key': self.api_key,
'commands': " ".join(commands),
}
if is_internal_ip(context, 'KISS_METRICS'):
html = disable_html(html, 'KISSmetrics')
return html

View file

@ -22,7 +22,7 @@ e,d])};b.__SV=1.2}})(document,window.mixpanel||[]);
mixpanel.init('%(token)s');
%(commands)s
</script>
"""
""" # noqa
IDENTIFY_CODE = "mixpanel.identify('%s');"
IDENTIFY_PROPERTIES = "mixpanel.people.set(%s);"
EVENT_CODE = "mixpanel.track('%(name)s', %(properties)s);"
@ -62,12 +62,16 @@ class MixpanelNode(Node):
commands.append(IDENTIFY_CODE % identity)
try:
name, properties = context[EVENT_CONTEXT_KEY]
commands.append(EVENT_CODE % {'name': name,
'properties': json.dumps(properties, sort_keys=True)})
commands.append(EVENT_CODE % {
'name': name,
'properties': json.dumps(properties, sort_keys=True),
})
except KeyError:
pass
html = TRACKING_CODE % {'token': self._token,
'commands': " ".join(commands)}
html = TRACKING_CODE % {
'token': self._token,
'commands': " ".join(commands),
}
if is_internal_ip(context, 'MIXPANEL'):
html = disable_html(html, 'Mixpanel')
return mark_safe(html)

View file

@ -18,7 +18,7 @@ SETUP_CODE = """
/*{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
</script>
"""
""" # noqa
NICKNAME_CODE = "olark('api.chat.updateVisitorNickname', {snippet: '%s'});"
NICKNAME_CONTEXT_KEY = 'olark_nickname'
FULLNAME_CODE = "olark('api.visitor.updateFullName', {{fullName: '{0}'}});"
@ -28,14 +28,16 @@ EMAIL_CONTEXT_KEY = 'olark_email'
STATUS_CODE = "olark('api.chat.updateVisitorStatus', {snippet: %s});"
STATUS_CONTEXT_KEY = 'olark_status'
MESSAGE_CODE = "olark.configure('locale.%(key)s', \"%(msg)s\");"
MESSAGE_KEYS = set(["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"])
MESSAGE_KEYS = {
"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",
}
register = Library()
@ -56,8 +58,9 @@ def olark(parser, token):
class OlarkNode(Node):
def __init__(self):
self.site_id = get_required_setting('OLARK_SITE_ID', SITE_ID_RE,
"must be a string looking like 'XXXX-XXX-XX-XXXX'")
self.site_id = get_required_setting(
'OLARK_SITE_ID', SITE_ID_RE,
"must be a string looking like 'XXXX-XXX-XX-XXXX'")
def render(self, context):
extra_code = []
@ -76,13 +79,15 @@ class OlarkNode(Node):
except KeyError:
pass
try:
extra_code.append(STATUS_CODE %
json.dumps(context[STATUS_CONTEXT_KEY], sort_keys=True))
extra_code.append(STATUS_CODE % json.dumps(context[STATUS_CONTEXT_KEY],
sort_keys=True))
except KeyError:
pass
extra_code.extend(self._get_configuration(context))
html = SETUP_CODE % {'site_id': self.site_id,
'extra_code': " ".join(extra_code)}
html = SETUP_CODE % {
'site_id': self.site_id,
'extra_code': " ".join(extra_code),
}
return html
def _get_nickname(self, user):

View file

@ -16,7 +16,7 @@ from analytical.utils import is_internal_ip, disable_html, get_identity, \
API_KEY_RE = re.compile(r'^\w+$')
SETUP_CODE = """
<script src="//d1nu2rn22elx8m.cloudfront.net/performable/pax/%(api_key)s.js" type="text/javascript"></script>
"""
""" # noqa
IDENTIFY_CODE = """
<script type="text/javascript">
var _paq = _paq || [];
@ -32,7 +32,7 @@ EMBED_CODE = """
$f.write();
})()
</script>
"""
""" # noqa
register = Library()
@ -54,8 +54,9 @@ def performable(parser, token):
class PerformableNode(Node):
def __init__(self):
self.api_key = get_required_setting('PERFORMABLE_API_KEY', API_KEY_RE,
"must be a string looking like 'XXXXX'")
self.api_key = get_required_setting(
'PERFORMABLE_API_KEY', API_KEY_RE,
"must be a string looking like 'XXXXX'")
def render(self, context):
html = SETUP_CODE % {'api_key': self.api_key}
@ -72,7 +73,10 @@ def performable_embed(hostname, page_id):
"""
Include a Performable landing page.
"""
return mark_safe(EMBED_CODE % {'hostname': hostname, 'page_id': page_id})
return mark_safe(EMBED_CODE % {
'hostname': hostname,
'page_id': page_id,
})
def contribute_to_analytical(add_node):

View file

@ -8,6 +8,7 @@ from collections import namedtuple
from itertools import chain
import re
from django.conf import settings
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import (is_internal_ip, disable_html,
@ -24,21 +25,23 @@ TRACKING_CODE = """
<script type="text/javascript">
var _paq = _paq || [];
%(variables)s
%(commands)s
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u=(("https:" == document.location.protocol) ? "https" : "http") + "://%(url)s/";
var u="//%(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);
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+'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>
<noscript><p><img src="//%(url)s/piwik.php?idsite=%(siteid)s" style="border:0;" alt="" /></p></noscript>
""" # noqa
VARIABLE_CODE = '_paq.push(["setCustomVariable", %(index)s, "%(name)s", "%(value)s", "%(scope)s"]);' # noqa
IDENTITY_CODE = '_paq.push(["setUserId", "%(userid)s"]);'
DISABLE_COOKIES_CODE = '_paq.push([\'disableCookies\']);'
DEFAULT_SCOPE = 'page'
@ -89,6 +92,10 @@ class PiwikNode(Node):
variables_code = (VARIABLE_CODE % PiwikVar(*var)._asdict()
for var in complete_variables)
commands = []
if getattr(settings, 'PIWIK_DISABLE_COOKIES', False):
commands.append(DISABLE_COOKIES_CODE)
userid = get_identity(context, 'piwik')
if userid is not None:
variables_code = chain(variables_code, (
@ -98,7 +105,8 @@ class PiwikNode(Node):
html = TRACKING_CODE % {
'url': self.domain_path,
'siteid': self.site_id,
'variables': '\n '.join(variables_code)
'variables': '\n '.join(variables_code),
'commands': '\n '.join(commands)
}
if is_internal_ip(context, 'PIWIK'):
html = disable_html(html, 'Piwik')

View file

@ -4,10 +4,8 @@ Rating@Mail.ru template tags and filters.
from __future__ import absolute_import
import json
import re
from django.conf import settings
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import is_internal_ip, disable_html, \
@ -30,7 +28,7 @@ COUNTER_CODE = """
<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()
@ -54,8 +52,8 @@ def rating_mailru(parser, token):
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'")
'RATING_MAILRU_COUNTER_ID', COUNTER_ID_RE,
"must be (a string containing) a number'")
def render(self, context):
html = COUNTER_CODE % {

View file

@ -12,7 +12,6 @@ from django.utils import translation
from analytical.utils import get_identity, get_required_setting
BUTTON_LOCATION_LEFT = 0
BUTTON_LOCATION_RIGHT = 1
BUTTON_LOCATION_TOP = 2
@ -33,7 +32,7 @@ SETUP_CODE = """
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
</script>
"""
""" # noqa
DOMAIN_CODE = 'SnapABug.setDomain("%s");'
SECURE_CONNECTION_CODE = 'SnapABug.setSecureConnexion();'
INIT_CODE = 'SnapABug.init("%s");'
@ -69,21 +68,22 @@ def snapengage(parser, token):
class SnapEngageNode(Node):
def __init__(self):
self.widget_id = get_required_setting('SNAPENGAGE_WIDGET_ID',
WIDGET_ID_RE, "must be a string looking like this: "
"'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'")
self.widget_id = get_required_setting(
'SNAPENGAGE_WIDGET_ID', WIDGET_ID_RE,
"must be a string looking like this: 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'")
def render(self, context):
settings_code = []
domain = self._get_setting(context, 'snapengage_domain',
'SNAPENGAGE_DOMAIN')
'SNAPENGAGE_DOMAIN')
if domain is not None:
settings_code.append(DOMAIN_CODE % domain)
secure_connection = self._get_setting(context,
'snapengage_secure_connection', 'SNAPENGAGE_SECURE_CONNECTION',
False)
'snapengage_secure_connection',
'SNAPENGAGE_SECURE_CONNECTION',
False)
if secure_connection:
settings_code.append(SECURE_CONNECTION_CODE)
@ -92,41 +92,42 @@ class SnapEngageNode(Node):
email = get_identity(context, 'snapengage', lambda u: u.email)
if email is not None:
if self._get_setting(context, 'snapengage_readonly_email',
'SNAPENGAGE_READONLY_EMAIL', False):
'SNAPENGAGE_READONLY_EMAIL', False):
readonly_tail = ',true'
else:
readonly_tail = ''
settings_code.append(SETEMAIL_CODE % (email, readonly_tail))
locale = self._get_setting(context, 'snapengage_locale',
'SNAPENGAGE_LOCALE')
'SNAPENGAGE_LOCALE')
if locale is None:
locale = translation.to_locale(translation.get_language())
settings_code.append(SETLOCALE_CODE % locale)
form_position = self._get_setting(context,
'snapengage_form_position', 'SNAPENGAGE_FORM_POSITION')
'snapengage_form_position', 'SNAPENGAGE_FORM_POSITION')
if form_position is not None:
settings_code.append(FORM_POSITION_CODE % form_position)
form_top_position = self._get_setting(context,
'snapengage_form_top_position', 'SNAPENGAGE_FORM_TOP_POSITION')
'snapengage_form_top_position',
'SNAPENGAGE_FORM_TOP_POSITION')
if form_top_position is not None:
settings_code.append(FORM_TOP_POSITION_CODE % form_top_position)
show_offline = self._get_setting(context, 'snapengage_show_offline',
'SNAPENGAGE_SHOW_OFFLINE', True)
'SNAPENGAGE_SHOW_OFFLINE', True)
if not show_offline:
settings_code.append(DISABLE_OFFLINE_CODE)
screenshots = self._get_setting(context, 'snapengage_screenshots',
'SNAPENGAGE_SCREENSHOTS', True)
'SNAPENGAGE_SCREENSHOTS', True)
if not screenshots:
settings_code.append(DISABLE_SCREENSHOT_CODE)
offline_screenshots = self._get_setting(context,
'snapengage_offline_screenshots',
'SNAPENGAGE_OFFLINE_SCREENSHOTS', True)
'snapengage_offline_screenshots',
'SNAPENGAGE_OFFLINE_SCREENSHOTS', True)
if not offline_screenshots:
settings_code.append(DISABLE_OFFLINE_SCREENSHOT_CODE)
@ -134,37 +135,41 @@ class SnapEngageNode(Node):
settings_code.append(DISABLE_PROACTIVE_CHAT_CODE)
sounds = self._get_setting(context, 'snapengage_sounds',
'SNAPENGAGE_SOUNDS', True)
'SNAPENGAGE_SOUNDS', True)
if not sounds:
settings_code.append(DISABLE_SOUNDS_CODE)
button_effect = self._get_setting(context, 'snapengage_button_effect',
'SNAPENGAGE_BUTTON_EFFECT')
'SNAPENGAGE_BUTTON_EFFECT')
if button_effect is not None:
settings_code.append(BUTTONEFFECT_CODE % button_effect)
button = self._get_setting(context, 'snapengage_button',
'SNAPENGAGE_BUTTON', BUTTON_STYLE_DEFAULT)
'SNAPENGAGE_BUTTON', BUTTON_STYLE_DEFAULT)
if button == BUTTON_STYLE_NONE:
settings_code.append(INIT_CODE % self.widget_id)
else:
if not isinstance(button, int):
# Assume button as a URL to a custom image
settings_code.append(SETBUTTON_CODE % button)
button_location = self._get_setting(context,
'snapengage_button_location', 'SNAPENGAGE_BUTTON_LOCATION',
BUTTON_LOCATION_LEFT)
button_offset = self._get_setting(context,
'snapengage_button_location_offset',
'SNAPENGAGE_BUTTON_LOCATION_OFFSET', '55%')
button_location = self._get_setting(
context,
'snapengage_button_location', 'SNAPENGAGE_BUTTON_LOCATION',
BUTTON_LOCATION_LEFT)
button_offset = self._get_setting(
context,
'snapengage_button_location_offset',
'SNAPENGAGE_BUTTON_LOCATION_OFFSET', '55%')
settings_code.append(ADDBUTTON_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)}
})
html = SETUP_CODE % {
'widget_id': self.widget_id,
'settings_code': " ".join(settings_code),
}
return html
def _get_setting(self, context, context_key, setting=None, default=None):

View file

@ -29,8 +29,7 @@ TRACKING_CODE = """
)();
%(custom_commands)s
</script>
"""
""" # noqa
register = Library()

View file

@ -49,8 +49,8 @@ def uservoice(parser, token):
class UserVoiceNode(Node):
def __init__(self):
self.default_widget_key = get_required_setting('USERVOICE_WIDGET_KEY',
WIDGET_KEY_RE, "must be an alphanumeric string")
self.default_widget_key = get_required_setting(
'USERVOICE_WIDGET_KEY', WIDGET_KEY_RE, "must be an alphanumeric string")
def render(self, context):
widget_key = context.get('uservoice_widget_key')

View file

@ -15,6 +15,7 @@ from analytical.utils import (
get_identity,
get_required_setting,
get_user_from_context,
get_user_is_authenticated,
is_internal_ip,
)
@ -28,7 +29,7 @@ TRACKING_CODE = """
woopra.identify(woo_visitor);
woopra.track();
</script>
"""
""" # noqa
register = Library()
@ -138,7 +139,7 @@ class WoopraNode(Node):
params[var[7:]] = val
if 'name' not in params and 'email' not in params:
user = get_user_from_context(context)
if user is not None and user.is_authenticated():
if user is not None and get_user_is_authenticated(user):
params['name'] = get_identity(
context, 'woopra', self._identify, user)
if user.email:

View file

@ -37,7 +37,7 @@ COUNTER_CODE = """
})(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()

View file

@ -22,3 +22,10 @@ MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
)
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'APP_DIRS': True,
},
]

View file

@ -18,6 +18,7 @@ def _location_node(location):
return "<!-- dummy_%s -->" % location
return DummyNode
_location_nodes = dict((l, _location_node(l)) for l in TAG_LOCATIONS)
@ -29,6 +30,7 @@ def _location_tag(location):
return _location_nodes[location]
return dummy_tag
for loc in TAG_LOCATIONS:
register.tag('dummy_%s' % loc, _location_tag(loc))

View file

@ -27,8 +27,7 @@ class AnalyticsTagTestCase(TagTestCase):
def render_location_tag(self, location, vars=None):
if vars is None:
vars = {}
t = Template("{%% load analytical %%}{%% analytical_%s %%}"
% location)
t = Template("{%% load analytical %%}{%% analytical_%s %%}" % location)
return t.render(Context(vars))
def test_location_tags(self):

View file

@ -39,10 +39,8 @@ class ChartbeatTagTestCaseWithSites(TestCase):
site = Site.objects.create(domain="test.com", name="test")
with override_settings(SITE_ID=site.id):
r = ChartbeatBottomNode().render(Context())
self.assertTrue(re.search(
'var _sf_async_config={.*"uid": "12345".*};', r), r)
self.assertTrue(re.search(
'var _sf_async_config={.*"domain": "test.com".*};', r), r)
self.assertTrue(re.search('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)
def test_auto_domain_false(self):
@ -62,30 +60,26 @@ class ChartbeatTagTestCase(TagTestCase):
"""
def test_top_tag(self):
r = self.render_tag('chartbeat', 'chartbeat_top',
{'chartbeat_domain': "test.com"})
r = self.render_tag('chartbeat', 'chartbeat_top', {'chartbeat_domain': "test.com"})
self.assertTrue('var _sf_startpt=(new Date()).getTime()' in r, r)
def test_bottom_tag(self):
r = self.render_tag('chartbeat', 'chartbeat_bottom',
{'chartbeat_domain': "test.com"})
self.assertTrue(re.search(
'var _sf_async_config={.*"uid": "12345".*};', r), r)
self.assertTrue(re.search(
'var _sf_async_config={.*"domain": "test.com".*};', r), r)
r = self.render_tag('chartbeat', 'chartbeat_bottom', {'chartbeat_domain': "test.com"})
self.assertTrue(re.search('var _sf_async_config={.*"uid": "12345".*};', r), r)
self.assertTrue(re.search('var _sf_async_config={.*"domain": "test.com".*};', r), r)
def test_top_node(self):
r = ChartbeatTopNode().render(
Context({'chartbeat_domain': "test.com"}))
r = ChartbeatTopNode().render(Context({
'chartbeat_domain': "test.com",
}))
self.assertTrue('var _sf_startpt=(new Date()).getTime()' in r, r)
def test_bottom_node(self):
r = ChartbeatBottomNode().render(
Context({'chartbeat_domain': "test.com"}))
self.assertTrue(re.search(
'var _sf_async_config={.*"uid": "12345".*};', r), r)
self.assertTrue(re.search(
'var _sf_async_config={.*"domain": "test.com".*};', r), r)
r = ChartbeatBottomNode().render(Context({
'chartbeat_domain': "test.com",
}))
self.assertTrue(re.search('var _sf_async_config={.*"uid": "12345".*};', r), r)
self.assertTrue(re.search('var _sf_async_config={.*"domain": "test.com".*};', r), r)
@override_settings(CHARTBEAT_USER_ID=None)
def test_no_user_id(self):

View file

@ -23,14 +23,12 @@ class ClickyTagTestCase(TagTestCase):
def test_tag(self):
r = self.render_tag('clicky', 'clicky')
self.assertTrue('clicky_site_ids.push(12345678);' in r, r)
self.assertTrue('src="//in.getclicky.com/12345678ns.gif"' in r,
r)
self.assertTrue('src="//in.getclicky.com/12345678ns.gif"' in r, r)
def test_node(self):
r = ClickyNode().render(Context({}))
self.assertTrue('clicky_site_ids.push(12345678);' in r, r)
self.assertTrue('src="//in.getclicky.com/12345678ns.gif"' in r,
r)
self.assertTrue('src="//in.getclicky.com/12345678ns.gif"' in r, r)
@override_settings(CLICKY_SITE_ID=None)
def test_no_site_id(self):
@ -43,9 +41,7 @@ class ClickyTagTestCase(TagTestCase):
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
def test_identify(self):
r = ClickyNode().render(Context({'user': User(username='test')}))
self.assertTrue(
'var clicky_custom = {"session": {"username": "test"}};' in r,
r)
self.assertTrue('var clicky_custom = {"session": {"username": "test"}};' in r, r)
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
def test_identify_anonymous_user(self):
@ -53,10 +49,13 @@ class ClickyTagTestCase(TagTestCase):
self.assertFalse('var clicky_custom = {"session": {"username":' in r, r)
def test_custom(self):
r = ClickyNode().render(Context({'clicky_var1': 'val1',
'clicky_var2': 'val2'}))
self.assertTrue(re.search('var clicky_custom = {.*'
'"var1": "val1", "var2": "val2".*};', r), r)
r = ClickyNode().render(Context({
'clicky_var1': 'val1',
'clicky_var2': 'val2',
}))
self.assertTrue(
re.search(r'var clicky_custom = {.*"var1": "val1", "var2": "val2".*};', r),
r)
@override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
def test_render_internal_ip(self):

View file

@ -0,0 +1,114 @@
"""
Tests for the Facebook Pixel template tags.
"""
from django.http import HttpRequest
from django.template import Context, Template, TemplateSyntaxError
from django.test import override_settings
from analytical.templatetags.analytical import _load_template_nodes
from analytical.templatetags.facebook_pixel import FacebookPixelHeadNode, FacebookPixelBodyNode
from analytical.tests.utils import TagTestCase
from analytical.utils import AnalyticalException
expected_head_html = """\
<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', '1234567890');
fbq('track', 'PageView');
</script>
"""
expected_body_html = """\
<noscript><img height="1" width="1" style="display:none"
src="https://www.facebook.com/tr?id=1234567890&ev=PageView&noscript=1"
/></noscript>
"""
@override_settings(FACEBOOK_PIXEL_ID='1234567890')
class FacebookPixelTagTestCase(TagTestCase):
maxDiff = None
def test_head_tag(self):
html = self.render_tag('facebook_pixel', 'facebook_pixel_head')
self.assertEqual(expected_head_html, html)
def test_head_node(self):
html = FacebookPixelHeadNode().render(Context({}))
self.assertEqual(expected_head_html, html)
def test_body_tag(self):
html = self.render_tag('facebook_pixel', 'facebook_pixel_body')
self.assertEqual(expected_body_html, html)
def test_body_node(self):
html = FacebookPixelBodyNode().render(Context({}))
self.assertEqual(expected_body_html, html)
def test_tags_take_no_args(self):
self.assertRaisesRegexp(
TemplateSyntaxError,
r"^'facebook_pixel_head' takes no arguments$",
lambda: (Template('{% load facebook_pixel %}{% facebook_pixel_head "arg" %}')
.render(Context({}))),
)
self.assertRaisesRegexp(
TemplateSyntaxError,
r"^'facebook_pixel_body' takes no arguments$",
lambda: (Template('{% load facebook_pixel %}{% facebook_pixel_body "arg" %}')
.render(Context({}))),
)
@override_settings(FACEBOOK_PIXEL_ID=None)
def test_no_id(self):
expected_pattern = r'^FACEBOOK_PIXEL_ID setting is not set$'
self.assertRaisesRegexp(AnalyticalException, expected_pattern, FacebookPixelHeadNode)
self.assertRaisesRegexp(AnalyticalException, expected_pattern, FacebookPixelBodyNode)
@override_settings(FACEBOOK_PIXEL_ID='invalid')
def test_invalid_id(self):
expected_pattern = (
r"^FACEBOOK_PIXEL_ID setting: must be \(a string containing\) a number: 'invalid'$")
self.assertRaisesRegexp(AnalyticalException, expected_pattern, FacebookPixelHeadNode)
self.assertRaisesRegexp(AnalyticalException, expected_pattern, FacebookPixelBodyNode)
@override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
def test_render_internal_ip(self):
request = HttpRequest()
request.META['REMOTE_ADDR'] = '1.1.1.1'
context = Context({'request': request})
def _disabled(html):
return '\n'.join([
'<!-- Facebook Pixel disabled on internal IP address',
html,
'-->',
])
head_html = FacebookPixelHeadNode().render(context)
self.assertEqual(_disabled(expected_head_html), head_html)
body_html = FacebookPixelBodyNode().render(context)
self.assertEqual(_disabled(expected_body_html), body_html)
def test_contribute_to_analytical(self):
"""
`facebook_pixel.contribute_to_analytical` registers the head and body nodes.
"""
template_nodes = _load_template_nodes()
self.assertEqual({
'head_top': [],
'head_bottom': [FacebookPixelHeadNode],
'body_top': [],
'body_bottom': [FacebookPixelBodyNode],
}, template_nodes)

View file

@ -32,8 +32,7 @@ class GaugesTagTestCase(TagTestCase):
s.parentNode.insertBefore(t, s);
})();
</script>
""",
self.render_tag('gauges', 'gauges'))
""", self.render_tag('gauges', 'gauges'))
def test_node(self):
self.assertEqual(
@ -51,8 +50,7 @@ class GaugesTagTestCase(TagTestCase):
s.parentNode.insertBefore(t, s);
})();
</script>
""",
GaugesNode().render(Context()))
""", GaugesNode().render(Context()))
@override_settings(GAUGES_SITE_ID=None)
def test_no_account_number(self):

View file

@ -14,7 +14,7 @@ from analytical.utils import AnalyticalException
@override_settings(GOOGLE_ANALYTICS_PROPERTY_ID='UA-123456-7',
GOOGLE_ANALYTICS_TRACKING_STYLE=TRACK_SINGLE_DOMAIN)
GOOGLE_ANALYTICS_TRACKING_STYLE=TRACK_SINGLE_DOMAIN)
class GoogleAnalyticsTagTestCase(TagTestCase):
"""
Tests for the ``google_analytics`` template tag.
@ -38,16 +38,15 @@ class GoogleAnalyticsTagTestCase(TagTestCase):
def test_wrong_property_id(self):
self.assertRaises(AnalyticalException, GoogleAnalyticsNode)
@override_settings(
GOOGLE_ANALYTICS_TRACKING_STYLE=TRACK_MULTIPLE_SUBDOMAINS,
GOOGLE_ANALYTICS_DOMAIN='example.com')
@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')
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)
@ -62,14 +61,10 @@ class GoogleAnalyticsTagTestCase(TagTestCase):
'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)
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):
@ -97,96 +92,90 @@ class GoogleAnalyticsTagTestCase(TagTestCase):
@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)
self.assertTrue("_gaq.push(['_gat._anonymizeIp']);" in r, r)
self.assertTrue(r.index('_gat._anonymizeIp') < r.index('_trackPageview'), 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)
self.assertFalse("_gaq.push(['_gat._anonymizeIp']);" in r, r)
@override_settings(GOOGLE_ANALYTICS_SAMPLE_RATE=0.0)
def test_set_sample_rate_min(self):
r = GoogleAnalyticsNode().render(Context())
self.assertTrue("_gaq.push (['_setSampleRate', '0.00']);" in r, r)
self.assertTrue("_gaq.push(['_setSampleRate', '0.00']);" in r, r)
@override_settings(GOOGLE_ANALYTICS_SAMPLE_RATE='100.00')
def test_set_sample_rate_max(self):
r = GoogleAnalyticsNode().render(Context())
self.assertTrue("_gaq.push (['_setSampleRate', '100.00']);" in r, r)
self.assertTrue("_gaq.push(['_setSampleRate', '100.00']);" in r, r)
@override_settings(GOOGLE_ANALYTICS_SAMPLE_RATE=-1)
def test_exception_whenset_sample_rate_too_small(self):
context = Context()
self.assertRaises(AnalyticalException, GoogleAnalyticsNode().render,
context)
self.assertRaises(AnalyticalException, GoogleAnalyticsNode().render, context)
@override_settings(GOOGLE_ANALYTICS_SAMPLE_RATE=101)
def test_exception_when_set_sample_rate_too_large(self):
context = Context()
self.assertRaises(AnalyticalException, GoogleAnalyticsNode().render,
context)
self.assertRaises(AnalyticalException, GoogleAnalyticsNode().render, context)
@override_settings(GOOGLE_ANALYTICS_SITE_SPEED_SAMPLE_RATE=0.0)
def test_set_site_speed_sample_rate_min(self):
r = GoogleAnalyticsNode().render(Context())
self.assertTrue("_gaq.push (['_setSiteSpeedSampleRate', '0.00']);" in r, r)
self.assertTrue("_gaq.push(['_setSiteSpeedSampleRate', '0.00']);" in r, r)
@override_settings(GOOGLE_ANALYTICS_SITE_SPEED_SAMPLE_RATE='100.00')
def test_set_site_speed_sample_rate_max(self):
r = GoogleAnalyticsNode().render(Context())
self.assertTrue("_gaq.push (['_setSiteSpeedSampleRate', '100.00']);" in r, r)
self.assertTrue("_gaq.push(['_setSiteSpeedSampleRate', '100.00']);" in r, r)
@override_settings(GOOGLE_ANALYTICS_SITE_SPEED_SAMPLE_RATE=-1)
def test_exception_whenset_site_speed_sample_rate_too_small(self):
context = Context()
self.assertRaises(AnalyticalException, GoogleAnalyticsNode().render,
context)
self.assertRaises(AnalyticalException, GoogleAnalyticsNode().render, context)
@override_settings(GOOGLE_ANALYTICS_SITE_SPEED_SAMPLE_RATE=101)
def test_exception_when_set_site_speed_sample_rate_too_large(self):
context = Context()
self.assertRaises(AnalyticalException, GoogleAnalyticsNode().render,
context)
self.assertRaises(AnalyticalException, GoogleAnalyticsNode().render, context)
@override_settings(GOOGLE_ANALYTICS_SESSION_COOKIE_TIMEOUT=0)
def test_set_session_cookie_timeout_min(self):
r = GoogleAnalyticsNode().render(Context())
self.assertTrue("_gaq.push (['_setSessionCookieTimeout', '0']);" in r, r)
self.assertTrue("_gaq.push(['_setSessionCookieTimeout', '0']);" in r, r)
@override_settings(GOOGLE_ANALYTICS_SESSION_COOKIE_TIMEOUT='10000')
def test_set_session_cookie_timeout_as_string(self):
r = GoogleAnalyticsNode().render(Context())
self.assertTrue("_gaq.push (['_setSessionCookieTimeout', '10000']);" in r, r)
self.assertTrue("_gaq.push(['_setSessionCookieTimeout', '10000']);" in r, r)
@override_settings(GOOGLE_ANALYTICS_SESSION_COOKIE_TIMEOUT=-1)
def test_exception_when_set_session_cookie_timeout_too_small(self):
context = Context()
self.assertRaises(AnalyticalException, GoogleAnalyticsNode().render,
context)
self.assertRaises(AnalyticalException, GoogleAnalyticsNode().render, context)
@override_settings(GOOGLE_ANALYTICS_VISITOR_COOKIE_TIMEOUT=0)
def test_set_visitor_cookie_timeout_min(self):
r = GoogleAnalyticsNode().render(Context())
self.assertTrue("_gaq.push (['_setVisitorCookieTimeout', '0']);" in r, r)
self.assertTrue("_gaq.push(['_setVisitorCookieTimeout', '0']);" in r, r)
@override_settings(GOOGLE_ANALYTICS_VISITOR_COOKIE_TIMEOUT='10000')
def test_set_visitor_cookie_timeout_as_string(self):
r = GoogleAnalyticsNode().render(Context())
self.assertTrue("_gaq.push (['_setVisitorCookieTimeout', '10000']);" in r, r)
self.assertTrue("_gaq.push(['_setVisitorCookieTimeout', '10000']);" in r, r)
@override_settings(GOOGLE_ANALYTICS_VISITOR_COOKIE_TIMEOUT=-1)
def test_exception_when_set_visitor_cookie_timeout_too_small(self):
context = Context()
self.assertRaises(AnalyticalException, GoogleAnalyticsNode().render,
context)
self.assertRaises(AnalyticalException, GoogleAnalyticsNode().render, context)
@override_settings(GOOGLE_ANALYTICS_PROPERTY_ID='UA-123456-7',
GOOGLE_ANALYTICS_TRACKING_STYLE=TRACK_MULTIPLE_DOMAINS,
GOOGLE_ANALYTICS_DOMAIN=None,
ANALYTICAL_DOMAIN=None)
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)
self.assertRaises(AnalyticalException, GoogleAnalyticsNode().render, context)

View file

@ -0,0 +1,169 @@
"""
Tests for the Google Analytics template tags and filters, using the new analytics.js library.
"""
from django.http import HttpRequest
from django.template import Context
from django.test.utils import override_settings
from analytical.templatetags.google_analytics_js import GoogleAnalyticsJsNode, \
TRACK_SINGLE_DOMAIN, TRACK_MULTIPLE_DOMAINS, TRACK_MULTIPLE_SUBDOMAINS
from analytical.tests.utils import TestCase, TagTestCase
from analytical.utils import AnalyticalException
@override_settings(GOOGLE_ANALYTICS_JS_PROPERTY_ID='UA-123456-7',
GOOGLE_ANALYTICS_TRACKING_STYLE=TRACK_SINGLE_DOMAIN)
class GoogleAnalyticsTagTestCase(TagTestCase):
"""
Tests for the ``google_analytics_js`` template tag.
"""
def test_tag(self):
r = self.render_tag('google_analytics_js', 'google_analytics_js')
self.assertTrue("""(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','https://www.google-analytics.com/analytics.js','ga');""" in r, r)
self.assertTrue("ga('create', 'UA-123456-7', 'auto', {});" in r, r)
self.assertTrue("ga('send', 'pageview');" in r, r)
def test_node(self):
r = GoogleAnalyticsJsNode().render(Context())
self.assertTrue("""(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','https://www.google-analytics.com/analytics.js','ga');""" in r, r)
self.assertTrue("ga('create', 'UA-123456-7', 'auto', {});" in r, r)
self.assertTrue("ga('send', 'pageview');" in r, r)
@override_settings(GOOGLE_ANALYTICS_JS_PROPERTY_ID=None)
def test_no_property_id(self):
self.assertRaises(AnalyticalException, GoogleAnalyticsJsNode)
@override_settings(GOOGLE_ANALYTICS_JS_PROPERTY_ID='wrong')
def test_wrong_property_id(self):
self.assertRaises(AnalyticalException, GoogleAnalyticsJsNode)
@override_settings(GOOGLE_ANALYTICS_TRACKING_STYLE=TRACK_MULTIPLE_SUBDOMAINS,
GOOGLE_ANALYTICS_DOMAIN='example.com')
def test_track_multiple_subdomains(self):
r = GoogleAnalyticsJsNode().render(Context())
self.assertTrue(
"""ga('create', 'UA-123456-7', 'auto', {"legacyCookieDomain": "example.com"}""" in r, r)
@override_settings(GOOGLE_ANALYTICS_TRACKING_STYLE=TRACK_MULTIPLE_DOMAINS,
GOOGLE_ANALYTICS_DOMAIN='example.com')
def test_track_multiple_domains(self):
r = GoogleAnalyticsJsNode().render(Context())
self.assertTrue("ga('create', 'UA-123456-7', 'auto', {" in r, r)
self.assertTrue('"legacyCookieDomain": "example.com"' in r, r)
self.assertTrue('"allowLinker\": true' in r, r)
def test_custom_vars(self):
context = Context({
'google_analytics_var1': ('test1', 'foo'),
'google_analytics_var2': ('test2', 'bar'),
'google_analytics_var4': ('test4', 1),
'google_analytics_var5': ('test5', 2.2),
})
r = GoogleAnalyticsJsNode().render(context)
self.assertTrue("ga('set', 'test1', 'foo');" in r, r)
self.assertTrue("ga('set', 'test2', 'bar');" in r, r)
self.assertTrue("ga('set', 'test4', 1);" in r, r)
self.assertTrue("ga('set', 'test5', 2.2);" in r, r)
def test_display_advertising(self):
with override_settings(GOOGLE_ANALYTICS_DISPLAY_ADVERTISING=True):
r = GoogleAnalyticsJsNode().render(Context())
self.assertTrue("""ga('create', 'UA-123456-7', 'auto', {});
ga('require', 'displayfeatures');
ga('send', 'pageview');""" 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 = GoogleAnalyticsJsNode().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 = GoogleAnalyticsJsNode().render(Context())
self.assertTrue("ga('set', 'anonymizeIp', true);" in r, r)
@override_settings(GOOGLE_ANALYTICS_ANONYMIZE_IP=False)
def test_anonymize_ip_not_present(self):
r = GoogleAnalyticsJsNode().render(Context())
self.assertFalse("ga('set', 'anonymizeIp', true);" in r, r)
@override_settings(GOOGLE_ANALYTICS_SAMPLE_RATE=0.0)
def test_set_sample_rate_min(self):
r = GoogleAnalyticsJsNode().render(Context())
self.assertTrue("""ga('create', 'UA-123456-7', 'auto', {"sampleRate": 0});""" in r, r)
@override_settings(GOOGLE_ANALYTICS_SAMPLE_RATE='100.00')
def test_set_sample_rate_max(self):
r = GoogleAnalyticsJsNode().render(Context())
self.assertTrue("""ga('create', 'UA-123456-7', 'auto', {"sampleRate": 100});""" in r, r)
@override_settings(GOOGLE_ANALYTICS_SAMPLE_RATE=-1)
def test_exception_whenset_sample_rate_too_small(self):
context = Context()
self.assertRaises(AnalyticalException, GoogleAnalyticsJsNode().render, context)
@override_settings(GOOGLE_ANALYTICS_SAMPLE_RATE=101)
def test_exception_when_set_sample_rate_too_large(self):
context = Context()
self.assertRaises(AnalyticalException, GoogleAnalyticsJsNode().render, context)
@override_settings(GOOGLE_ANALYTICS_SITE_SPEED_SAMPLE_RATE=0.0)
def test_set_site_speed_sample_rate_min(self):
r = GoogleAnalyticsJsNode().render(Context())
self.assertTrue(
"""ga('create', 'UA-123456-7', 'auto', {"siteSpeedSampleRate": 0});""" in r, r)
@override_settings(GOOGLE_ANALYTICS_SITE_SPEED_SAMPLE_RATE='100.00')
def test_set_site_speed_sample_rate_max(self):
r = GoogleAnalyticsJsNode().render(Context())
self.assertTrue(
"""ga('create', 'UA-123456-7', 'auto', {"siteSpeedSampleRate": 100});""" in r, r)
@override_settings(GOOGLE_ANALYTICS_SITE_SPEED_SAMPLE_RATE=-1)
def test_exception_whenset_site_speed_sample_rate_too_small(self):
context = Context()
self.assertRaises(AnalyticalException, GoogleAnalyticsJsNode().render, context)
@override_settings(GOOGLE_ANALYTICS_SITE_SPEED_SAMPLE_RATE=101)
def test_exception_when_set_site_speed_sample_rate_too_large(self):
context = Context()
self.assertRaises(AnalyticalException, GoogleAnalyticsJsNode().render, context)
@override_settings(GOOGLE_ANALYTICS_COOKIE_EXPIRATION=0)
def test_set_cookie_expiration_min(self):
r = GoogleAnalyticsJsNode().render(Context())
self.assertTrue("""ga('create', 'UA-123456-7', 'auto', {"cookieExpires": 0});""" in r, r)
@override_settings(GOOGLE_ANALYTICS_COOKIE_EXPIRATION='10000')
def test_set_cookie_expiration_as_string(self):
r = GoogleAnalyticsJsNode().render(Context())
self.assertTrue(
"""ga('create', 'UA-123456-7', 'auto', {"cookieExpires": 10000});""" in r, r)
@override_settings(GOOGLE_ANALYTICS_COOKIE_EXPIRATION=-1)
def test_exception_when_set_cookie_expiration_too_small(self):
context = Context()
self.assertRaises(AnalyticalException, GoogleAnalyticsJsNode().render, context)
@override_settings(GOOGLE_ANALYTICS_JS_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, GoogleAnalyticsJsNode().render, context)

View file

@ -36,8 +36,9 @@ class GoSquaredTagTestCase(TagTestCase):
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
def test_auto_identify(self):
r = GoSquaredNode().render(Context({'user': User(username='test',
first_name='Test', last_name='User')}))
r = GoSquaredNode().render(Context({
'user': User(username='test', first_name='Test', last_name='User'),
}))
self.assertTrue('GoSquared.UserName = "Test User";' in r, r)
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)

View file

@ -0,0 +1,84 @@
"""
Tests for the Hotjar template tags.
"""
from django.http import HttpRequest
from django.template import Context, Template, TemplateSyntaxError
from django.test import override_settings
from analytical.templatetags.analytical import _load_template_nodes
from analytical.templatetags.hotjar import HotjarNode
from analytical.tests.utils import TagTestCase
from analytical.utils import AnalyticalException
expected_html = """\
<script>
(function(h,o,t,j,a,r){
h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)};
h._hjSettings={hjid:123456789,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>
"""
@override_settings(HOTJAR_SITE_ID='123456789')
class HotjarTagTestCase(TagTestCase):
maxDiff = None
def test_tag(self):
html = self.render_tag('hotjar', 'hotjar')
self.assertEqual(expected_html, html)
def test_node(self):
html = HotjarNode().render(Context({}))
self.assertEqual(expected_html, html)
def test_tags_take_no_args(self):
self.assertRaisesRegexp(
TemplateSyntaxError,
r"^'hotjar' takes no arguments$",
lambda: (Template('{% load hotjar %}{% hotjar "arg" %}')
.render(Context({}))),
)
@override_settings(HOTJAR_SITE_ID=None)
def test_no_id(self):
expected_pattern = r'^HOTJAR_SITE_ID setting is not set$'
self.assertRaisesRegexp(AnalyticalException, expected_pattern, HotjarNode)
@override_settings(HOTJAR_SITE_ID='invalid')
def test_invalid_id(self):
expected_pattern = (
r"^HOTJAR_SITE_ID setting: must be \(a string containing\) a number: 'invalid'$")
self.assertRaisesRegexp(AnalyticalException, expected_pattern, HotjarNode)
@override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
def test_render_internal_ip(self):
request = HttpRequest()
request.META['REMOTE_ADDR'] = '1.1.1.1'
context = Context({'request': request})
actual_html = HotjarNode().render(context)
disabled_html = '\n'.join([
'<!-- Hotjar disabled on internal IP address',
expected_html,
'-->',
])
self.assertEqual(disabled_html, actual_html)
def test_contribute_to_analytical(self):
"""
`hotjar.contribute_to_analytical` registers the head and body nodes.
"""
template_nodes = _load_template_nodes()
self.assertEqual({
'head_top': [],
'head_bottom': [HotjarNode],
'body_top': [],
'body_bottom': [],
}, template_nodes)

View file

@ -19,13 +19,13 @@ class HubSpotTagTestCase(TagTestCase):
def test_tag(self):
r = self.render_tag('hubspot', 'hubspot')
self.assertTrue("n.id=i;n.src='//js.hs-analytics.net/analytics/'+(Math.ceil(new Date()/r)*r)+'/1234.js';"
in r, r)
self.assertTrue("n.id=i;n.src='//js.hs-analytics.net/analytics/'"
"+(Math.ceil(new Date()/r)*r)+'/1234.js';" in r, r)
def test_node(self):
r = HubSpotNode().render(Context())
self.assertTrue("n.id=i;n.src='//js.hs-analytics.net/analytics/'+(Math.ceil(new Date()/r)*r)+'/1234.js';"
in r, r)
self.assertTrue("n.id=i;n.src='//js.hs-analytics.net/analytics/'"
"+(Math.ceil(new Date()/r)*r)+'/1234.js';" in r, r)
@override_settings(HUBSPOT_PORTAL_ID=None)
def test_no_portal_id(self):

View file

@ -4,16 +4,17 @@ Tests for the intercom template tags and filters.
import datetime
from django.contrib.auth.models import User, AnonymousUser
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.intercom import IntercomNode
from analytical.templatetags.intercom import IntercomNode, intercom_user_hash, _timestamp
from analytical.tests.utils import TagTestCase
from analytical.utils import AnalyticalException
@override_settings(INTERCOM_APP_ID='1234567890abcdef0123456789')
@override_settings(INTERCOM_APP_ID="abc123xyz")
class IntercomTagTestCase(TagTestCase):
"""
Tests for the ``intercom`` template tag.
@ -21,25 +22,25 @@ class IntercomTagTestCase(TagTestCase):
def test_tag(self):
rendered_tag = self.render_tag('intercom', 'intercom')
self.assertTrue(rendered_tag.startswith('<!-- Intercom disabled on internal IP address'))
self.assertTrue(rendered_tag.strip().startswith('<script id="IntercomSettingsScriptTag">'))
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)
}))
user = User.objects.create(
username='test',
first_name='Firstname',
last_name='Lastname',
email="test@example.com",
date_joined=now,
)
rendered_tag = IntercomNode().render(Context({'user': user}))
# 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"};
window.intercomSettings = {"app_id": "abc123xyz", "created_at": 1397074500, "email": "test@example.com", "name": "Firstname Lastname", "user_id": %(user_id)s};
</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)
""" % {'user_id': user.pk}, rendered_tag) # noqa
@override_settings(INTERCOM_APP_ID=None)
def test_no_account_number(self):
@ -51,13 +52,21 @@ class IntercomTagTestCase(TagTestCase):
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
user = User.objects.create(
username='test',
first_name='Firstname',
last_name='Lastname',
email="test@example.com",
date_joined=now,
)
r = IntercomNode().render(Context({
'user': user,
}))
self.assertTrue('window.intercomSettings = {'
'"app_id": "abc123xyz", "created_at": 1397074500, '
'"email": "test@example.com", "name": "Firstname Lastname", '
'"user_id": %(user_id)s'
'};' % {'user_id': user.pk} in r, msg=r)
def test_custom(self):
r = IntercomNode().render(Context({
@ -68,10 +77,11 @@ class IntercomTagTestCase(TagTestCase):
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")
'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)
@ -80,15 +90,83 @@ class IntercomTagTestCase(TagTestCase):
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')}))
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')}))
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()}))
@override_settings(INTERCOM_HMAC_SECRET_KEY='secret')
def test_user_hash__without_user_details(self):
"""
No `user_hash` without `user_id` or `email`.
"""
attrs = IntercomNode()._get_custom_attrs(Context())
self.assertEqual({
'created_at': None,
}, attrs)
@override_settings(INTERCOM_HMAC_SECRET_KEY='secret')
def test_user_hash__with_user(self):
"""
'user_hash' of default `user_id`.
"""
user = User.objects.create(
email='test@example.com',
) # type: User
attrs = IntercomNode()._get_custom_attrs(Context({'user': user}))
self.assertEqual({
'created_at': int(_timestamp(user.date_joined)),
'email': 'test@example.com',
'name': '',
'user_hash': intercom_user_hash(str(user.pk)),
'user_id': user.pk,
}, attrs)
@override_settings(INTERCOM_HMAC_SECRET_KEY='secret')
def test_user_hash__with_explicit_user_id(self):
"""
'user_hash' of context-provided `user_id`.
"""
attrs = IntercomNode()._get_custom_attrs(Context({
'intercom_email': 'test@example.com',
'intercom_user_id': '5',
}))
self.assertEqual({
'created_at': None,
'email': 'test@example.com',
# HMAC for user_id:
'user_hash': 'd3123a7052b42272d9b520235008c248a5aff3221cc0c530b754702ad91ab102',
'user_id': '5',
}, attrs)
@override_settings(INTERCOM_HMAC_SECRET_KEY='secret')
def test_user_hash__with_explicit_email(self):
"""
'user_hash' of context-provided `email`.
"""
attrs = IntercomNode()._get_custom_attrs(Context({
'intercom_email': 'test@example.com',
}))
self.assertEqual({
'created_at': None,
'email': 'test@example.com',
# HMAC for email:
'user_hash': '49e43229ee99dca2565241719b8341b04e71dd4de0628f991b5bea30a526e153',
}, attrs)
@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 = IntercomNode().render(context)
self.assertTrue(r.startswith('<!-- Intercom disabled on internal IP address'), r)
self.assertTrue(r.endswith('-->'), r)

View file

@ -11,8 +11,7 @@ from analytical.tests.utils import TagTestCase
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):
"""
Tests for the ``kiss_insights`` template tag.
@ -53,6 +52,5 @@ class KissInsightsTagTestCase(TagTestCase):
self.assertFalse("_kiq.push(['identify', " in r, r)
def test_show_survey(self):
r = KissInsightsNode().render(
Context({'kiss_insights_show_survey': 1234}))
r = KissInsightsNode().render(Context({'kiss_insights_show_survey': 1234}))
self.assertTrue("_kiq.push(['showSurvey', 1234]);" in r, r)

View file

@ -12,8 +12,7 @@ from analytical.tests.utils import TagTestCase
from analytical.utils import AnalyticalException
@override_settings(KISS_METRICS_API_KEY='0123456789abcdef0123456789abcdef'
'01234567')
@override_settings(KISS_METRICS_API_KEY='0123456789abcdef0123456789abcdef01234567')
class KissMetricsTagTestCase(TagTestCase):
"""
Tests for the ``kiss_metrics`` template tag.
@ -21,25 +20,23 @@ class KissMetricsTagTestCase(TagTestCase):
def test_tag(self):
r = self.render_tag('kiss_metrics', 'kiss_metrics')
self.assertTrue("//doug1izaerwt3.cloudfront.net/0123456789abcdef012345"
"6789abcdef01234567.1.js" in r, r)
self.assertTrue("//doug1izaerwt3.cloudfront.net/"
"0123456789abcdef0123456789abcdef01234567.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)
self.assertTrue("//doug1izaerwt3.cloudfront.net/"
"0123456789abcdef0123456789abcdef01234567.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')
@override_settings(KISS_METRICS_API_KEY='0123456789abcdef0123456789abcdef0123456')
def test_api_key_too_short(self):
self.assertRaises(AnalyticalException, KissMetricsNode)
@override_settings(KISS_METRICS_API_KEY='0123456789abcdef0123456789abcdef'
'012345678')
@override_settings(KISS_METRICS_API_KEY='0123456789abcdef0123456789abcdef012345678')
def test_api_key_too_long(self):
self.assertRaises(AnalyticalException, KissMetricsNode)
@ -54,20 +51,23 @@ class KissMetricsTagTestCase(TagTestCase):
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'})}))
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)
'{"prop1": "val1", "prop2": "val2"}]);' in r, r)
def test_property(self):
r = KissMetricsNode().render(Context({'kiss_metrics_properties':
{'prop1': 'val1', 'prop2': 'val2'}}))
r = KissMetricsNode().render(Context({
'kiss_metrics_properties': {'prop1': 'val1', 'prop2': 'val2'},
}))
self.assertTrue("_kmq.push([\'set\', "
'{"prop1": "val1", "prop2": "val2"}]);' in r, r)
'{"prop1": "val1", "prop2": "val2"}]);' in r, r)
def test_alias(self):
r = KissMetricsNode().render(Context({'kiss_metrics_alias':
{'test': 'test_alias'}}))
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'])

View file

@ -20,15 +20,11 @@ class MixpanelTagTestCase(TagTestCase):
def test_tag(self):
r = self.render_tag('mixpanel', 'mixpanel')
self.assertIn(
"mixpanel.init('0123456789abcdef0123456789abcdef');", r,
)
self.assertIn("mixpanel.init('0123456789abcdef0123456789abcdef');", r)
def test_node(self):
r = MixpanelNode().render(Context())
self.assertIn(
"mixpanel.init('0123456789abcdef0123456789abcdef');", r,
)
self.assertIn("mixpanel.init('0123456789abcdef0123456789abcdef');", r)
@override_settings(MIXPANEL_API_TOKEN=None)
def test_no_token(self):
@ -53,8 +49,9 @@ class MixpanelTagTestCase(TagTestCase):
self.assertFalse("mixpanel.register_once({distinct_id:" in r, r)
def test_event(self):
r = MixpanelNode().render(Context({'mixpanel_event':
('test_event', {'prop1': 'val1', 'prop2': 'val2'})}))
r = MixpanelNode().render(Context({
'mixpanel_event': ('test_event', {'prop1': 'val1', 'prop2': 'val2'}),
}))
self.assertTrue("mixpanel.track('test_event', "
'{"prop1": "val1", "prop2": "val2"});' in r, r)

View file

@ -35,10 +35,11 @@ class OlarkTestCase(TagTestCase):
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
def test_identify(self):
r = OlarkNode().render(Context({'user':
User(username='test', first_name='Test', last_name='User')}))
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)
"{snippet: 'Test User (test)'});" in r, r)
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
def test_identify_anonymous_user(self):
@ -48,18 +49,19 @@ class OlarkTestCase(TagTestCase):
def test_nickname(self):
r = OlarkNode().render(Context({'olark_nickname': 'testnick'}))
self.assertTrue("olark('api.chat.updateVisitorNickname', "
"{snippet: 'testnick'});" in r, r)
"{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)
'{snippet: "teststatus"});' in r, r)
def test_status_string_list(self):
r = OlarkNode().render(Context({'olark_status':
['teststatus1', 'teststatus2']}))
r = OlarkNode().render(Context({
'olark_status': ['teststatus1', 'teststatus2'],
}))
self.assertTrue("olark('api.chat.updateVisitorStatus', "
'{snippet: ["teststatus1", "teststatus2"]});' in r, r)
'{snippet: ["teststatus1", "teststatus2"]});' in r, r)
def test_messages(self):
messages = [
@ -88,5 +90,4 @@ class OlarkTestCase(TagTestCase):
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)
self.assertTrue("olark.configure('locale.%s', \"%s\");" % (m, m) in r, r)

View file

@ -20,38 +20,35 @@ class PiwikTagTestCase(TagTestCase):
def test_tag(self):
r = self.render_tag('piwik', 'piwik')
self.assertTrue(' ? "https" : "http") + "://example.com/";' in r, r)
self.assertTrue('"//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"'
self.assertTrue('img src="//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('"//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"'
self.assertTrue('img src="//example.com/piwik.php?idsite=345"'
in r, r)
@override_settings(PIWIK_DOMAIN_PATH='example.com/piwik',
PIWIK_SITE_ID='345')
def test_domain_path_valid(self):
r = self.render_tag('piwik', 'piwik')
self.assertTrue(' ? "https" : "http") + "://example.com/piwik/";' in r,
r)
self.assertTrue('"//example.com/piwik/"' in r, r)
@override_settings(PIWIK_DOMAIN_PATH='example.com:1234',
PIWIK_SITE_ID='345')
def test_domain_port_valid(self):
r = self.render_tag('piwik', 'piwik')
self.assertTrue(' ? "https" : "http") + "://example.com:1234/";' in r,
r)
self.assertTrue('"//example.com:1234/";' in r, r)
@override_settings(PIWIK_DOMAIN_PATH='example.com:1234/piwik',
PIWIK_SITE_ID='345')
def test_domain_port_path_valid(self):
r = self.render_tag('piwik', 'piwik')
self.assertTrue(' ? "https" : "http") + "://example.com:1234/piwik/";' in r,
r)
self.assertTrue('"//example.com:1234/piwik/"' in r, r)
@override_settings(PIWIK_DOMAIN_PATH=None)
def test_no_domain(self):
@ -148,3 +145,8 @@ class PiwikTagTestCase(TagTestCase):
msg = 'Incorrect Piwik user tracking rendering.\nFound:\n%s\nIn:\n%s'
var_code = '_paq.push(["setUserId", "BDFL"]);'
self.assertNotIn(var_code, r, msg % (var_code, r))
@override_settings(PIWIK_DISABLE_COOKIES=True)
def test_disable_cookies(self):
r = PiwikNode().render(Context({}))
self.assertTrue("_paq.push(['disableCookies']);" in r, r)

View file

@ -2,9 +2,6 @@
Tests for the Rating@Mail.ru 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

View file

@ -50,16 +50,19 @@ class SnapEngageTestCase(TagTestCase):
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)
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}))
r = SnapEngageNode().render(Context({
'snapengage_button': BUTTON_STYLE_LIVE,
}))
self.assertTrue(
'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","0",'
'"55%",true);' in r, r)
@ -71,7 +74,8 @@ class SnapEngageTestCase(TagTestCase):
def test_custom_button(self):
r = SnapEngageNode().render(Context({
'snapengage_button': "http://www.example.com/button.png"}))
'snapengage_button': "http://www.example.com/button.png",
}))
self.assertTrue(
'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","0",'
'"55%");' in r, r)
@ -89,12 +93,12 @@ class SnapEngageTestCase(TagTestCase):
def test_button_location_right(self):
r = SnapEngageNode().render(Context({
'snapengage_button_location': BUTTON_LOCATION_RIGHT}))
'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):
with override_settings(SNAPENGAGE_BUTTON_LOCATION=BUTTON_LOCATION_RIGHT):
r = SnapEngageNode().render(Context())
self.assertTrue(
'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","1",'
@ -102,7 +106,8 @@ class SnapEngageTestCase(TagTestCase):
def test_button_location_top(self):
r = SnapEngageNode().render(Context({
'snapengage_button_location': BUTTON_LOCATION_TOP}))
'snapengage_button_location': BUTTON_LOCATION_TOP,
}))
self.assertTrue(
'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","2",'
'"55%");' in r, r)
@ -114,7 +119,8 @@ class SnapEngageTestCase(TagTestCase):
def test_button_location_bottom(self):
r = SnapEngageNode().render(Context({
'snapengage_button_location': BUTTON_LOCATION_BOTTOM}))
'snapengage_button_location': BUTTON_LOCATION_BOTTOM,
}))
self.assertTrue(
'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","3",'
'"55%");' in r, r)
@ -127,7 +133,8 @@ class SnapEngageTestCase(TagTestCase):
def test_button_offset(self):
r = SnapEngageNode().render(Context({
'snapengage_button_location_offset': "30%"}))
'snapengage_button_location_offset': "30%",
}))
self.assertTrue(
'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","0",'
'"30%");' in r, r)
@ -139,7 +146,8 @@ class SnapEngageTestCase(TagTestCase):
def test_button_effect(self):
r = SnapEngageNode().render(Context({
'snapengage_button_effect': "-4px"}))
'snapengage_button_effect': "-4px",
}))
self.assertTrue('SnapABug.setButtonEffect("-4px");' in r, r)
with override_settings(SNAPENGAGE_BUTTON_EFFECT="-4px"):
r = SnapEngageNode().render(Context())
@ -147,7 +155,8 @@ class SnapEngageTestCase(TagTestCase):
def test_form_position(self):
r = SnapEngageNode().render(Context({
'snapengage_form_position': FORM_POSITION_TOP_LEFT}))
'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())
@ -155,7 +164,8 @@ class SnapEngageTestCase(TagTestCase):
def test_form_top_position(self):
r = SnapEngageNode().render(Context({
'snapengage_form_top_position': 40}))
'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())
@ -178,7 +188,9 @@ class SnapEngageTestCase(TagTestCase):
self.assertTrue('SnapABug.setSecureConnexion();' in r, r)
def test_show_offline(self):
r = SnapEngageNode().render(Context({'snapengage_show_offline': False}))
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())
@ -190,15 +202,18 @@ class SnapEngageTestCase(TagTestCase):
self.assertTrue('SnapABug.allowProactiveChat(false);' in r, r)
def test_screenshot(self):
r = SnapEngageNode().render(Context({'snapengage_screenshots': False}))
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}))
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())
@ -213,30 +228,35 @@ class SnapEngageTestCase(TagTestCase):
@override_settings(SNAPENGAGE_READONLY_EMAIL=False)
def test_email(self):
r = SnapEngageNode().render(Context({'snapengage_email':
'test@example.com'}))
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)
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)
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')}))
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()}))
r = SnapEngageNode().render(Context({
'user': AnonymousUser(),
}))
self.assertFalse('SnapABug.setUserEmail(' in r, r)
def test_language(self):

View file

@ -2,8 +2,6 @@
Tests for the Spring Metrics template tags and filters.
"""
import re
from django.contrib.auth.models import User, AnonymousUser
from django.http import HttpRequest
from django.template import Context
@ -38,10 +36,10 @@ class SpringMetricsTagTestCase(TagTestCase):
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
def test_identify(self):
r = SpringMetricsNode().render(Context({'user':
User(email='test@test.com')}))
self.assertTrue("_springMetq.push(['setdata', "
"{'email': 'test@test.com'}]);" in r, r)
r = SpringMetricsNode().render(Context({
'user': User(email='test@test.com'),
}))
self.assertTrue("_springMetq.push(['setdata', {'email': 'test@test.com'}]);" in r, r)
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
def test_identify_anonymous_user(self):
@ -49,12 +47,12 @@ class SpringMetricsTagTestCase(TagTestCase):
self.assertFalse("_springMetq.push(['setdata', {'email':" in r, r)
def test_custom(self):
r = SpringMetricsNode().render(Context({'spring_metrics_var1': 'val1',
'spring_metrics_var2': 'val2'}))
self.assertTrue("_springMetq.push(['setdata', {'var1': 'val1'}]);" in r,
r)
self.assertTrue("_springMetq.push(['setdata', {'var2': 'val2'}]);" in r,
r)
r = SpringMetricsNode().render(Context({
'spring_metrics_var1': 'val1',
'spring_metrics_var2': 'val2',
}))
self.assertTrue("_springMetq.push(['setdata', {'var1': 'val1'}]);" in r, r)
self.assertTrue("_springMetq.push(['setdata', {'var2': 'val2'}]);" in r, r)
@override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
def test_render_internal_ip(self):

View file

@ -40,14 +40,7 @@ class UserVoiceTagTestCase(TagTestCase):
@override_settings(USERVOICE_WIDGET_KEY='')
def test_empty_key(self):
r = UserVoiceNode().render(Context())
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)
self.assertRaises(AnalyticalException, UserVoiceNode)
def test_overridden_key(self):
vars = {'uservoice_widget_key': 'defghijklmnopqrstuvw'}
@ -65,7 +58,7 @@ class UserVoiceTagTestCase(TagTestCase):
r = UserVoiceNode().render(Context(data))
self.assertIn("""UserVoice.push(['set', {"key1": "val2"}]);""", r)
def test_auto_trigger(self):
def test_auto_trigger_default(self):
r = UserVoiceNode().render(Context())
self.assertTrue("UserVoice.push(['addTrigger', {}]);" in r, r)

View file

@ -37,22 +37,26 @@ class WoopraTagTestCase(TagTestCase):
@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)
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)
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)
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):
@ -61,14 +65,18 @@ class WoopraTagTestCase(TagTestCase):
@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')}))
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')}))
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)

View file

@ -2,9 +2,7 @@
Tests for the Yandex.Metrica 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

View file

@ -29,15 +29,15 @@ class SettingDeletedTestCase(TestCase):
# 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")
with self.assertRaisesRegex(AnalyticalException, "^USER_ID setting is not set$"):
get_required_setting("USER_ID", r"\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")
with self.assertRaisesRegexp(AnalyticalException, "^USER_ID setting is not set$"):
get_required_setting("USER_ID", r"\d+", "invalid USER_ID")
else:
self.assertRaises(AnalyticalException,
get_required_setting, "USER_ID", "\d+", "invalid USER_ID")
get_required_setting, "USER_ID", r"\d+", "invalid USER_ID")
class MyUser(AbstractBaseUser):
@ -72,10 +72,9 @@ class GetDomainTestCase(TestCase):
# 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):
# @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):

View file

@ -21,8 +21,8 @@ def get_required_setting(setting, value_re, invalid_msg):
value = getattr(settings, setting)
except AttributeError:
raise AnalyticalException("%s setting: not found" % setting)
if value is None:
raise AnalyticalException("%s setting is set to None" % setting)
if not value:
raise AnalyticalException("%s setting is not set" % setting)
value = str(value)
if not value_re.search(value):
raise AnalyticalException("%s setting: %s: '%s'"
@ -49,6 +49,19 @@ def get_user_from_context(context):
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):
"""
Get the identity of a logged in user from a template context.
@ -71,7 +84,7 @@ def get_identity(context, prefix=None, identity_func=None, user=None):
try:
if user is None:
user = get_user_from_context(context)
if user.is_authenticated():
if get_user_is_authenticated(user):
if identity_func is not None:
return identity_func(user)
else:

View file

@ -9,7 +9,7 @@ import sys
sys.path.append(os.path.join(os.path.abspath('.'), '_ext'))
sys.path.append(os.path.dirname(os.path.abspath('.')))
import analytical
import analytical # noqa
# -- General configuration --------------------------------------------------

View file

@ -18,7 +18,9 @@ initialization code if the client IP address is detected as one from the
:data:`ANALYTICAL_INTERNAL_IPS` setting. The default value for this
setting is :data:`INTERNAL_IPS`.
Example::
Example:
.. code-block:: python
ANALYTICAL_INTERNAL_IPS = ['192.168.1.45', '192.168.1.57']
@ -45,7 +47,9 @@ logged in through the standard Django authentication system and the
current user is accessible in the template context, the username can be
passed to the analytics services that support identifying users. This
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

View file

@ -10,7 +10,7 @@ version numbers. Patch-level increments indicate bug fixes, minor
version increments indicate new functionality and major version
increments indicate backwards incompatible changes.
Version 1.0.0 is the last to support Django < 1.7. Users of older django
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.

View file

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

View file

@ -32,10 +32,10 @@ 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
.. _GitHub: http://github.com/jazzband/django-analytical
Then install the package by running the setup script:
@ -74,26 +74,26 @@ HTML page, django-analytical provides four general-purpose template tags
that will render the code needed for the services you are using. Your
base template should look like this:
.. code-block:: html
.. 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>
</html>
{% analytical_body_bottom %}
</body>
</html>
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
@ -125,6 +125,10 @@ settings required to enable each service are listed here:
CRAZY_EGG_ACCOUNT_NUMBER = '12345678'
* :doc:`Facebook Pixel <services/facebook_pixel>`::
FACEBOOK_PIXEL_ID = '1234567890'
* :doc:`Gaug.es <services/gauges>`::
GAUGES_SITE_ID = '0123456789abcdef0123456789abcdef'
@ -167,7 +171,7 @@ settings required to enable each service are listed here:
PERFORMABLE_API_KEY = '123abc'
* :doc:`Piwik <services/piwik>`::
* :doc:`Matomo (formerly Piwik) <services/piwik>`::
PIWIK_DOMAIN_PATH = 'your.piwik.server/optional/path'
PIWIK_SITE_ID = '123'

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
`issue tracker`_. See also :ref:`helping-out`.
.. _`issue tracker`: http://github.com/jcassee/django-analytical/issues
.. _`issue tracker`: http://github.com/jazzband/django-analytical/issues
Currently supported services:
.. toctree::
:maxdepth: 1
:glob:
:maxdepth: 1
: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
project :file:`settings.py` file::
CHARTBEAT_SITE_ID = 'XXXXX'
CHARTBEAT_USER_ID = 'XXXXX'
If you do not set a User ID, the tracking code will not be rendered.

View file

@ -0,0 +1,84 @@
=======================================
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

@ -1,5 +1,5 @@
======================================
Google Analytics -- traffic analysis
Google Analytics (legacy) -- traffic analysis
======================================
`Google Analytics`_ is the well-known web analytics service from
@ -15,7 +15,7 @@ features.
Installation
============
To start using the Google Analytics integration, you must have installed
To start using the Google Analytics (legacy) 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.
@ -72,7 +72,7 @@ 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.SCOPE_*`
setting to one of the :const:`analytical.templatetags.google_analytics.TRACK_*`
constants:
============================= ===== =============================================

View file

@ -0,0 +1,227 @@
======================================
Google Analytics -- 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.
.. _`Google Analytics`: http://www.google.com/analytics/
.. 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`.
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:
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-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-XXXXXX-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-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-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-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-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-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

73
docs/services/hotjar.rst Normal file
View file

@ -0,0 +1,73 @@
=====================================
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

@ -120,22 +120,41 @@ Context variable Description
-------------------- -------------------------------------------
``intercom_email`` The visitor's email address.
-------------------- -------------------------------------------
``intercom_user_id`` The visitor's user id.
-------------------- -------------------------------------------
``created_at`` The date the visitor created an account
==================== ===========================================
.. _`custom visitor data`: http://docs.intercom.io/custom-data/adding-custom-data
.. _`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
Identifying authenticated users
-------------------------------
If you have not set the ``intercom_name`` or ``intercom_email`` variables
If you have not set the ``intercom_name``, ``intercom_email``, or ``intercom_user_id`` variables
explicitly, the username and email address of an authenticated user are
passed to Intercom automatically. See :ref:`identifying-visitors`.
.. _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
---------------------

View file

@ -1,5 +1,5 @@
==================================
Piwik -- open source web analytics
Matomo (formerly Piwik) -- open source web analytics
==================================
Piwik_ is an open analytics platform currently used by individuals,
@ -142,6 +142,13 @@ set the context variable ``analytical_identity`` (for global configuration) or
.. _`track individual users`: http://developer.piwik.org/guides/tracking-javascript-guide#user-id
Disabling cookies
-----------------
If you want to `disable cookies`_, set :data:`PIWIKI_DISABLE_COOKIES` to
:const:`True`. This is disabled by default.
.. _`disable cookies`: https://matomo.org/faq/general/faq_157/
Internal IP addresses
---------------------

View file

@ -6,7 +6,6 @@ Here's a full list of all available settings, in alphabetical order, and
their default values.
.. data:: ANALYTICAL_AUTO_IDENTIFY
Default: ``True``

View file

@ -29,7 +29,9 @@ Setting up basic tracking
To get started with django-analytical, the package must first be
installed. You can download and install the latest stable package from
the Python Package Index automatically by using ``easy_install``::
the Python Package Index automatically by using ``easy_install``:
.. code-block:: bash
$ easy_install django-analytical
@ -37,7 +39,9 @@ For more ways to install django-analytical, see
:ref:`installing-the-package`.
After you install django-analytical, you need to add it to the list of
installed applications in the ``settings.py`` file of your project::
installed applications in the ``settings.py`` file of your project:
.. code-block:: python
INSTALLED_APPS = [
...
@ -46,7 +50,9 @@ installed applications in the ``settings.py`` file of your project::
]
Then you have to add the general-purpose django-analytical template tags
to your base template::
to your base template:
.. code-block:: django
{% load analytical %}
<!DOCTYPE ... >
@ -69,7 +75,9 @@ to your base template::
Finally, you need to configure the Clicky Site ID and the Crazy Egg
account number. Add the following to your project :file:`settings.py`
file (replacing the ``x``'s with your own codes)::
file (replacing the ``x``'s with your own codes):
.. code-block:: python
CLICKY_SITE_ID = 'xxxxxxxx'
CRAZY_EGG_ACCOUNT_NUMBER = 'xxxxxxxx'
@ -110,7 +118,9 @@ protocol version.
In order to filter on protocol version in Crazy Egg, you need to
include the visitor IP protocol version in the Crazy Egg tracking code.
The easiest way to do this is by using a context processor::
The easiest way to do this is by using a context processor:
.. code-block:: python
def track_ip_proto(request):
addr = request.META.get('HTTP_X_FORWARDED_FOR', '')

View file

@ -16,13 +16,6 @@ try:
except ImportError:
pass
try:
from sphinx_pypi_upload import UploadDoc
cmdclass['upload_sphinx'] = UploadDoc
except ImportError:
pass
class TestCommand(Command):
description = "run package tests"
@ -43,7 +36,7 @@ class TestCommand(Command):
cmdclass['test'] = TestCommand
def read(fname):
def read_file(fname):
return open(os.path.join(os.path.dirname(__file__), fname)).read()
@ -60,16 +53,16 @@ except ImportError:
"This is fine, unless you intend to run unit tests."
)
import analytical
import analytical as package # noqa
setup(
name='django-analytical',
version=analytical.__version__,
license=analytical.__license__,
description='Analytics service integration for Django projects',
long_description=read('README.rst'),
author=analytical.__author__,
author_email=analytical.__email__,
version=package.__version__,
license=package.__license__,
description=package.__doc__.strip(),
long_description=read_file('README.rst'),
author=package.__author__,
author_email=package.__email__,
packages=[
'analytical',
'analytical.templatetags',
@ -84,6 +77,10 @@ setup(
'Framework :: Django :: 1.7',
'Framework :: Django :: 1.8',
'Framework :: Django :: 1.9',
'Framework :: Django :: 1.10',
'Framework :: Django :: 1.11',
'Framework :: Django :: 2.0',
'Framework :: Django :: 2.1',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
@ -93,14 +90,14 @@ setup(
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.2',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
],
platforms=['any'],
url='https://github.com/jcassee/django-analytical',
download_url='https://github.com/jcassee/django-analytical/archive/master.zip',
url='https://github.com/jazzband/django-analytical',
download_url='https://github.com/jazzband/django-analytical/archive/master.zip',
cmdclass=cmdclass,
install_requires=[
'Django>=1.7.0',

39
tox.ini
View file

@ -1,19 +1,50 @@
[tox]
envlist =
py{27,32,33,34}-django17,
py{27,32,33,34,35}-django18,
# Python/Django combinations that are officially supported
py{27,34}-django17
py{27,34,35}-django18
py{27,34,35}-django19
py{27,34,35}-django110
py{27,34,35,36,37}-django111
py{34,35,36,37}-django20
py{35,36,37}-django21
flake8
readme
[testenv]
commands =
coverage run setup.py test
sh -c 'coveralls | true'
deps =
coverage==3.7.1
coverage
coveralls
django17: Django>=1.7,<1.8
django18: Django>=1.8,<1.9
django19: Django>=1.9,<1.10
virtualenv<14.0.0
django110: Django>=1.10,<1.11
django111: Django>=1.11,<2.0
django20: Django>=2.0,<2.1
django21: Django>=2.1,<2.2
passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH
whitelist_externals = sh
[testenv:flake8]
deps = flake8
commands = flake8
[testenv:readme]
deps = readme_renderer
commands = python setup.py check --restructuredtext --strict
[travis:env]
DJANGO =
1.7: django17
1.8: django18
1.9: django19
1.10: django110
1.11: django111
2.0: django20
2.1: django21
[flake8]
max-line-length = 100