diff --git a/.travis.yml b/.travis.yml index 989706d..69a13a3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/AUTHORS.rst b/AUTHORS.rst index 9c8b416..7cc73a7 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -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/ diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4af2715..6ddaa21 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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). diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..fc3af32 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,5 @@ +.. image:: https://jazzband.co/static/img/jazzband.svg + :target: https://jazzband.co/ + :alt: Jazzband + +This is a `Jazzband `_ project. By contributing you agree to abide by the `Contributor Code of Conduct `_ and follow the `guidelines `_. diff --git a/README.rst b/README.rst index 5ce9748..9b41962 100644 --- a/README.rst +++ b/README.rst @@ -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 diff --git a/analytical/__init__.py b/analytical/__init__.py index f9eab87..d5354f6 100644 --- a/analytical/__init__.py +++ b/analytical/__init__.py @@ -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" diff --git a/analytical/templatetags/analytical.py b/analytical/templatetags/analytical.py index ad4fba7..6e74bd7 100644 --- a/analytical/templatetags/analytical.py +++ b/analytical/templatetags/analytical.py @@ -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', diff --git a/analytical/templatetags/chartbeat.py b/analytical/templatetags/chartbeat.py index 3b0e7d8..6168c13 100644 --- a/analytical/templatetags/chartbeat.py +++ b/analytical/templatetags/chartbeat.py @@ -35,7 +35,7 @@ SETUP_CODE = """ loadChartbeat : function() { oldonload(); loadChartbeat(); }; })(); -""" +""" # 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} diff --git a/analytical/templatetags/clicky.py b/analytical/templatetags/clicky.py index bee7f07..f493684 100644 --- a/analytical/templatetags/clicky.py +++ b/analytical/templatetags/clicky.py @@ -29,8 +29,7 @@ TRACKING_CODE = """ })(); -""" - +""" # 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 diff --git a/analytical/templatetags/facebook_pixel.py b/analytical/templatetags/facebook_pixel.py new file mode 100644 index 0000000..33dac0a --- /dev/null +++ b/analytical/templatetags/facebook_pixel.py @@ -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 = """\ + +""" + +FACEBOOK_PIXEL_BODY_CODE = """\ + +""" + +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) diff --git a/analytical/templatetags/google_analytics.py b/analytical/templatetags/google_analytics.py index 4297c31..3ac1deb 100644 --- a/analytical/templatetags/google_analytics.py +++ b/analytical/templatetags/google_analytics.py @@ -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) diff --git a/analytical/templatetags/google_analytics_js.py b/analytical/templatetags/google_analytics_js.py new file mode 100644 index 0000000..377d1f8 --- /dev/null +++ b/analytical/templatetags/google_analytics_js.py @@ -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 = """ + +""" +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) diff --git a/analytical/templatetags/gosquared.py b/analytical/templatetags/gosquared.py index 72edf6c..a42267e 100644 --- a/analytical/templatetags/gosquared.py +++ b/analytical/templatetags/gosquared.py @@ -26,7 +26,7 @@ TRACKING_CODE = """ w.addEventListener?w.addEventListener("load",gs,false):w.attachEvent("onload",gs); })(window); -""" +""" # noqa TOKEN_CODE = 'GoSquared.acct = "%s";' IDENTIFY_CODE = 'GoSquared.UserName = "%s";' diff --git a/analytical/templatetags/hotjar.py b/analytical/templatetags/hotjar.py new file mode 100644 index 0000000..b85a126 --- /dev/null +++ b/analytical/templatetags/hotjar.py @@ -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 = """\ + +""" + + +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) diff --git a/analytical/templatetags/hubspot.py b/analytical/templatetags/hubspot.py index 5b6824d..fc011e3 100644 --- a/analytical/templatetags/hubspot.py +++ b/analytical/templatetags/hubspot.py @@ -23,7 +23,7 @@ TRACKING_CODE = """ })(document,"script","hs-analytics",300000); -""" +""" # 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} diff --git a/analytical/templatetags/intercom.py b/analytical/templatetags/intercom.py index b9e7dd7..888a0ab 100644 --- a/analytical/templatetags/intercom.py +++ b/analytical/templatetags/intercom.py @@ -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 = """ -""" +""" # 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 diff --git a/analytical/templatetags/kiss_insights.py b/analytical/templatetags/kiss_insights.py index 17c70a6..8381eb3 100644 --- a/analytical/templatetags/kiss_insights.py +++ b/analytical/templatetags/kiss_insights.py @@ -16,7 +16,7 @@ SITE_CODE_RE = re.compile(r'^[\w]+$') SETUP_CODE = """ -""" +""" # 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 diff --git a/analytical/templatetags/kiss_metrics.py b/analytical/templatetags/kiss_metrics.py index 4288b1b..4706dbd 100644 --- a/analytical/templatetags/kiss_metrics.py +++ b/analytical/templatetags/kiss_metrics.py @@ -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 diff --git a/analytical/templatetags/mixpanel.py b/analytical/templatetags/mixpanel.py index 0490f76..caab030 100644 --- a/analytical/templatetags/mixpanel.py +++ b/analytical/templatetags/mixpanel.py @@ -22,7 +22,7 @@ e,d])};b.__SV=1.2}})(document,window.mixpanel||[]); mixpanel.init('%(token)s'); %(commands)s -""" +""" # 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) diff --git a/analytical/templatetags/olark.py b/analytical/templatetags/olark.py index 1d3a327..2b85648 100644 --- a/analytical/templatetags/olark.py +++ b/analytical/templatetags/olark.py @@ -18,7 +18,7 @@ SETUP_CODE = """ /*{literal}{/literal}*/ %(extra_code)s -""" +""" # 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): diff --git a/analytical/templatetags/performable.py b/analytical/templatetags/performable.py index 847f9c9..3364e18 100644 --- a/analytical/templatetags/performable.py +++ b/analytical/templatetags/performable.py @@ -16,7 +16,7 @@ from analytical.utils import is_internal_ip, disable_html, get_identity, \ API_KEY_RE = re.compile(r'^\w+$') SETUP_CODE = """ -""" +""" # noqa IDENTIFY_CODE = """ -""" +""" # 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): diff --git a/analytical/templatetags/piwik.py b/analytical/templatetags/piwik.py index 9951f82..9eebc4a 100644 --- a/analytical/templatetags/piwik.py +++ b/analytical/templatetags/piwik.py @@ -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 = """ - + """ # 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') diff --git a/analytical/templatetags/rating_mailru.py b/analytical/templatetags/rating_mailru.py index d495824..ba1da7e 100644 --- a/analytical/templatetags/rating_mailru.py +++ b/analytical/templatetags/rating_mailru.py @@ -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 = """ -""" +""" # 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 % { diff --git a/analytical/templatetags/snapengage.py b/analytical/templatetags/snapengage.py index 57134c3..5881a50 100644 --- a/analytical/templatetags/snapengage.py +++ b/analytical/templatetags/snapengage.py @@ -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")); -""" +""" # 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): diff --git a/analytical/templatetags/spring_metrics.py b/analytical/templatetags/spring_metrics.py index ed2f063..a3093ea 100644 --- a/analytical/templatetags/spring_metrics.py +++ b/analytical/templatetags/spring_metrics.py @@ -29,8 +29,7 @@ TRACKING_CODE = """ )(); %(custom_commands)s -""" - +""" # noqa register = Library() diff --git a/analytical/templatetags/uservoice.py b/analytical/templatetags/uservoice.py index d1ab6f7..68f59db 100644 --- a/analytical/templatetags/uservoice.py +++ b/analytical/templatetags/uservoice.py @@ -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') diff --git a/analytical/templatetags/woopra.py b/analytical/templatetags/woopra.py index a0a12eb..504986d 100644 --- a/analytical/templatetags/woopra.py +++ b/analytical/templatetags/woopra.py @@ -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(); -""" +""" # 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: diff --git a/analytical/templatetags/yandex_metrica.py b/analytical/templatetags/yandex_metrica.py index e2ec20b..4e3270f 100644 --- a/analytical/templatetags/yandex_metrica.py +++ b/analytical/templatetags/yandex_metrica.py @@ -37,7 +37,7 @@ COUNTER_CODE = """ })(document, window, "yandex_metrika_callbacks"); -""" +""" # noqa register = Library() diff --git a/analytical/tests/settings.py b/analytical/tests/settings.py index ecb42e4..ccd75a6 100644 --- a/analytical/tests/settings.py +++ b/analytical/tests/settings.py @@ -22,3 +22,10 @@ MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', ) + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'APP_DIRS': True, + }, +] diff --git a/analytical/tests/templatetags/dummy.py b/analytical/tests/templatetags/dummy.py index 40d9781..f92bc98 100644 --- a/analytical/tests/templatetags/dummy.py +++ b/analytical/tests/templatetags/dummy.py @@ -18,6 +18,7 @@ def _location_node(location): return "" % 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)) diff --git a/analytical/tests/test_tag_analytical.py b/analytical/tests/test_tag_analytical.py index 8439197..e4e47a8 100644 --- a/analytical/tests/test_tag_analytical.py +++ b/analytical/tests/test_tag_analytical.py @@ -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): diff --git a/analytical/tests/test_tag_chartbeat.py b/analytical/tests/test_tag_chartbeat.py index 322fbd6..67ebd10 100644 --- a/analytical/tests/test_tag_chartbeat.py +++ b/analytical/tests/test_tag_chartbeat.py @@ -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): diff --git a/analytical/tests/test_tag_clicky.py b/analytical/tests/test_tag_clicky.py index a2b3876..b2f2731 100644 --- a/analytical/tests/test_tag_clicky.py +++ b/analytical/tests/test_tag_clicky.py @@ -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): diff --git a/analytical/tests/test_tag_facebook_pixel.py b/analytical/tests/test_tag_facebook_pixel.py new file mode 100644 index 0000000..75d52aa --- /dev/null +++ b/analytical/tests/test_tag_facebook_pixel.py @@ -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 = """\ + +""" + + +expected_body_html = """\ + +""" + + +@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([ + '', + ]) + + 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) diff --git a/analytical/tests/test_tag_gauges.py b/analytical/tests/test_tag_gauges.py index a995a0c..8d80b80 100644 --- a/analytical/tests/test_tag_gauges.py +++ b/analytical/tests/test_tag_gauges.py @@ -32,8 +32,7 @@ class GaugesTagTestCase(TagTestCase): s.parentNode.insertBefore(t, s); })(); -""", - 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); })(); -""", - GaugesNode().render(Context())) +""", GaugesNode().render(Context())) @override_settings(GAUGES_SITE_ID=None) def test_no_account_number(self): diff --git a/analytical/tests/test_tag_google_analytics.py b/analytical/tests/test_tag_google_analytics.py index 76e03ba..67c8360 100644 --- a/analytical/tests/test_tag_google_analytics.py +++ b/analytical/tests/test_tag_google_analytics.py @@ -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) diff --git a/analytical/tests/test_tag_google_analytics_js.py b/analytical/tests/test_tag_google_analytics_js.py new file mode 100644 index 0000000..517eb8b --- /dev/null +++ b/analytical/tests/test_tag_google_analytics_js.py @@ -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( + ''), 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) diff --git a/analytical/tests/test_tag_gosquared.py b/analytical/tests/test_tag_gosquared.py index 9b946c0..844205c 100644 --- a/analytical/tests/test_tag_gosquared.py +++ b/analytical/tests/test_tag_gosquared.py @@ -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) diff --git a/analytical/tests/test_tag_hotjar.py b/analytical/tests/test_tag_hotjar.py new file mode 100644 index 0000000..c7e656d --- /dev/null +++ b/analytical/tests/test_tag_hotjar.py @@ -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 = """\ + +""" + + +@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([ + '', + ]) + 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) diff --git a/analytical/tests/test_tag_hubspot.py b/analytical/tests/test_tag_hubspot.py index f7c7a0e..ee9d2ff 100644 --- a/analytical/tests/test_tag_hubspot.py +++ b/analytical/tests/test_tag_hubspot.py @@ -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): diff --git a/analytical/tests/test_tag_intercom.py b/analytical/tests/test_tag_intercom.py index 78b9f50..2085fcb 100644 --- a/analytical/tests/test_tag_intercom.py +++ b/analytical/tests/test_tag_intercom.py @@ -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(''), r) diff --git a/analytical/tests/test_tag_kiss_insights.py b/analytical/tests/test_tag_kiss_insights.py index ea58250..ffbba16 100644 --- a/analytical/tests/test_tag_kiss_insights.py +++ b/analytical/tests/test_tag_kiss_insights.py @@ -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) diff --git a/analytical/tests/test_tag_kiss_metrics.py b/analytical/tests/test_tag_kiss_metrics.py index 9c2c19d..506b635 100644 --- a/analytical/tests/test_tag_kiss_metrics.py +++ b/analytical/tests/test_tag_kiss_metrics.py @@ -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']) diff --git a/analytical/tests/test_tag_mixpanel.py b/analytical/tests/test_tag_mixpanel.py index b43a8d2..033c2c7 100644 --- a/analytical/tests/test_tag_mixpanel.py +++ b/analytical/tests/test_tag_mixpanel.py @@ -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) diff --git a/analytical/tests/test_tag_olark.py b/analytical/tests/test_tag_olark.py index ecc3f56..f65514b 100644 --- a/analytical/tests/test_tag_olark.py +++ b/analytical/tests/test_tag_olark.py @@ -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) diff --git a/analytical/tests/test_tag_piwik.py b/analytical/tests/test_tag_piwik.py index f8c57dc..32661ee 100644 --- a/analytical/tests/test_tag_piwik.py +++ b/analytical/tests/test_tag_piwik.py @@ -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) diff --git a/analytical/tests/test_tag_rating_mailru.py b/analytical/tests/test_tag_rating_mailru.py index 2e91a60..45d0309 100644 --- a/analytical/tests/test_tag_rating_mailru.py +++ b/analytical/tests/test_tag_rating_mailru.py @@ -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 diff --git a/analytical/tests/test_tag_snapengage.py b/analytical/tests/test_tag_snapengage.py index 07b96bc..02b37cc 100644 --- a/analytical/tests/test_tag_snapengage.py +++ b/analytical/tests/test_tag_snapengage.py @@ -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): diff --git a/analytical/tests/test_tag_spring_metrics.py b/analytical/tests/test_tag_spring_metrics.py index 5256d55..14aeb3c 100644 --- a/analytical/tests/test_tag_spring_metrics.py +++ b/analytical/tests/test_tag_spring_metrics.py @@ -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): diff --git a/analytical/tests/test_tag_uservoice.py b/analytical/tests/test_tag_uservoice.py index d8f6a5b..9bce78d 100644 --- a/analytical/tests/test_tag_uservoice.py +++ b/analytical/tests/test_tag_uservoice.py @@ -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) diff --git a/analytical/tests/test_tag_woopra.py b/analytical/tests/test_tag_woopra.py index 8eb4ec2..671e7de 100644 --- a/analytical/tests/test_tag_woopra.py +++ b/analytical/tests/test_tag_woopra.py @@ -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) diff --git a/analytical/tests/test_tag_yandex_metrica.py b/analytical/tests/test_tag_yandex_metrica.py index f85924a..fa287db 100644 --- a/analytical/tests/test_tag_yandex_metrica.py +++ b/analytical/tests/test_tag_yandex_metrica.py @@ -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 diff --git a/analytical/tests/test_utils.py b/analytical/tests/test_utils.py index 0f4ad58..83c4cf6 100644 --- a/analytical/tests/test_utils.py +++ b/analytical/tests/test_utils.py @@ -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): diff --git a/analytical/utils.py b/analytical/utils.py index d915cf4..45e0fcf 100644 --- a/analytical/utils.py +++ b/analytical/utils.py @@ -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: diff --git a/docs/conf.py b/docs/conf.py index e7dc08f..f84736b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -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 -------------------------------------------------- diff --git a/docs/features.rst b/docs/features.rst index 765916a..4485b92 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -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 diff --git a/docs/history.rst b/docs/history.rst index 703bfa4..6939438 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -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. diff --git a/docs/index.rst b/docs/index.rst index 9630154..1e41fba 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -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 diff --git a/docs/install.rst b/docs/install.rst index 0b25521..b641065 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -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 %} - - - - {% analytical_head_top %} + {% load analytical %} + + + + {% analytical_head_top %} - ... + ... - {% analytical_head_bottom %} - - - {% analytical_body_top %} + {% analytical_head_bottom %} + + + {% analytical_body_top %} - ... + ... - {% analytical_body_bottom %} - - + {% analytical_body_bottom %} + + 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 `:: + + FACEBOOK_PIXEL_ID = '1234567890' + * :doc:`Gaug.es `:: GAUGES_SITE_ID = '0123456789abcdef0123456789abcdef' @@ -167,7 +171,7 @@ settings required to enable each service are listed here: PERFORMABLE_API_KEY = '123abc' -* :doc:`Piwik `:: +* :doc:`Matomo (formerly Piwik) `:: PIWIK_DOMAIN_PATH = 'your.piwik.server/optional/path' PIWIK_SITE_ID = '123' diff --git a/docs/services.rst b/docs/services.rst index 57dec35..f777b1f 100644 --- a/docs/services.rst +++ b/docs/services.rst @@ -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/* diff --git a/docs/services/chartbeat.rst b/docs/services/chartbeat.rst index 9de4e5b..62820dc 100644 --- a/docs/services/chartbeat.rst +++ b/docs/services/chartbeat.rst @@ -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. diff --git a/docs/services/facebook_pixel.rst b/docs/services/facebook_pixel.rst new file mode 100644 index 0000000..cfe4d21 --- /dev/null +++ b/docs/services/facebook_pixel.rst @@ -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 %} + + + ... + {% facebook_pixel_head %} + + + ... + {% facebook_pixel_body %} + + + +.. 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. diff --git a/docs/services/google_analytics.rst b/docs/services/google_analytics.rst index c34ba36..94587de 100644 --- a/docs/services/google_analytics.rst +++ b/docs/services/google_analytics.rst @@ -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: ============================= ===== ============================================= diff --git a/docs/services/google_analytics_js.rst b/docs/services/google_analytics_js.rst new file mode 100644 index 0000000..764e023 --- /dev/null +++ b/docs/services/google_analytics_js.rst @@ -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 %} + + + ... + {% google_analytics_js %} + + ... + + +.. _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 diff --git a/docs/services/hotjar.rst b/docs/services/hotjar.rst new file mode 100644 index 0000000..54e3403 --- /dev/null +++ b/docs/services/hotjar.rst @@ -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 %} + + + ... + {% hotjar %} + + ... + + + +.. _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. diff --git a/docs/services/intercom.rst b/docs/services/intercom.rst index 99b0ac1..f458d28 100644 --- a/docs/services/intercom.rst +++ b/docs/services/intercom.rst @@ -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 --------------------- diff --git a/docs/services/piwik.rst b/docs/services/piwik.rst index 1bc4cb6..e4c9aee 100644 --- a/docs/services/piwik.rst +++ b/docs/services/piwik.rst @@ -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 --------------------- diff --git a/docs/settings.rst b/docs/settings.rst index 46b203a..4865049 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -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`` diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 976ff19..27d272c 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -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 %} @@ -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', '') diff --git a/setup.py b/setup.py index 9361840..51026df 100644 --- a/setup.py +++ b/setup.py @@ -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', diff --git a/tox.ini b/tox.ini index 5f71e15..383fdba 100644 --- a/tox.ini +++ b/tox.ini @@ -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