diff --git a/.bandit b/.bandit deleted file mode 120000 index bf39a01..0000000 --- a/.bandit +++ /dev/null @@ -1 +0,0 @@ -tox.ini \ No newline at end of file diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 99095c1..0000000 --- a/.coveragerc +++ /dev/null @@ -1,3 +0,0 @@ -[run] -source = analytical -omit = analytical/tests/* diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..9a7923e --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,35 @@ +name: Check + +on: + pull_request: + branches: + - main + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + env: + - lint + - format + - audit + - package + - docs + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install prerequisites + run: python -m pip install tox + + - name: Run ${{ matrix.env }} + run: tox -e ${{ matrix.env }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..2e97910 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,44 @@ +name: Release + +on: + push: + tags: + - '*' + +env: + PIP_DISABLE_PIP_VERSION_CHECK: '1' + PY_COLORS: '1' + +jobs: + build: + if: github.repository == 'jazzband/django-analytical' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + cache: pip + cache-dependency-path: | + **/pyproject.toml + + - name: Install dependencies + run: | + python -m pip install tox + + - name: Build package + run: | + tox -e package + + - name: Upload packages to Jazzband + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: jazzband + password: ${{ secrets.JAZZBAND_RELEASE_KEY }} + repository_url: https://jazzband.co/projects/django-analytical/upload diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..5fa02dc --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,58 @@ +name: Test + +on: + pull_request: + branches: + - main + push: + branches: + - main + +env: + PIP_DISABLE_PIP_VERSION_CHECK: '1' + PY_COLORS: '1' + +jobs: + python-django: + runs-on: ubuntu-latest + strategy: + max-parallel: 5 + matrix: + python-version: + - '3.10' + - '3.11' + - '3.12' + - '3.13' + django-version: + - '4.2' + - '5.1' + - '5.2' + include: + - { python-version: '3.9', django-version: '4.2' } + exclude: + - { python-version: '3.13', django-version: '4.2' } + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + cache-dependency-path: | + **/pyproject.toml + + - name: Install dependencies + run: | + python -m pip install tox tox-gh-actions + + - name: Tox tests (Python ${{ matrix.python-version }}, Django ${{ matrix.django-version }}) + run: tox + env: + DJANGO: ${{ matrix.django-version }} + + - name: Upload coverage + uses: codecov/codecov-action@v5 + with: + name: Python ${{ matrix.python-version }} diff --git a/.gitignore b/.gitignore index aaebe01..9c8a4a7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,23 +4,21 @@ /.tox /.vscode /.envrc +/playground *.pyc *.pyo /.coverage +/coverage.xml +/tests/*-report.json +/tests/*-report.xml /build /dist /docs/_build -/MANIFEST -/playground - /docs/_templates/layout.html - -*.pyc -*.pyo - +/MANIFEST *.egg-info /requirements.txt diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..4e6a92c --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1 @@ +repos: [] diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..1a89680 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,18 @@ +# Read the Docs configuration file +# https://docs.readthedocs.io/en/stable/config-file/v2.html + +version: 2 + +build: + os: ubuntu-24.04 + tools: + python: "3.12" + +sphinx: + configuration: docs/conf.py + +# We recommend specifying your dependencies to enable reproducible builds: +# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +# python: +# install: +# - requirements: docs/requirements.txt diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 6731930..0000000 --- a/.travis.yml +++ /dev/null @@ -1,57 +0,0 @@ -dist: xenial -sudo: true - -language: python -python: -- 2.7 -- 3.4 -- 3.5 -- 3.6 -- 3.7 - -env: -- DJANGO=1.11 -- DJANGO=2.1 -- DJANGO=2.2 - -matrix: - allow_failures: - - env: TOXENV=bandit - exclude: - # Python/Django combinations that aren't officially supported - - { env: DJANGO=1.11, python: 3.7 } - - { env: DJANGO=2.1, python: 2.7 } - - { env: DJANGO=2.1, python: 3.4 } - - { env: DJANGO=2.2, python: 2.7 } - - { env: DJANGO=2.2, python: 3.4 } - -install: -- pip install tox-travis -script: -- tox - -stages: -- lint -- test -- deploy - -jobs: - include: - - { stage: lint, env: TOXENV=flake8, python: 3.7 } - - { stage: lint, env: TOXENV=bandit, python: 3.7 } - - { stage: lint, env: TOXENV=readme, python: 3.7 } - - stage: deploy - env: - python: 3.7 - install: skip - script: skip - deploy: - provider: pypi - server: https://jazzband.co/projects/django-analytical/upload - distributions: sdist bdist_wheel - user: jazzband - password: - secure: JCr5hRjAeXuiISodCJf8HWd4BTJMpl2eiHI8NciPaSM9WwOOeUXxmlcP8+lWlXxgM4BYUC/O7Q90fkwj5x06n+z4oyJSEVerTvCDcpeZ68KMMG1tR1jTbHcxfEKoEvcs2J0fThJ9dIMtfbtUbIpzusJHkZPjsIy8HAJDw8knnJs= - on: - tags: true - repo: jazzband/django-analytical diff --git a/AUTHORS.rst b/AUTHORS.rst deleted file mode 100644 index a32ba85..0000000 --- a/AUTHORS.rst +++ /dev/null @@ -1,50 +0,0 @@ -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`_, `Scott Karlin`_ -and others. - -Included Javascript code snippets for integration of the analytics -services were written by the respective service providers. - -The application was inspired by and uses ideas from Analytical_, Joshua -Krall's all-purpose analytics front-end for Rails. - -The work on Crazy Egg was made possible by `Bateau Knowledge`_. -The work on Intercom was made possible by `GreenKahuna`_. - -.. _`Joost Cassee`: 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 -.. _`Pi Delport`: https://github.com/pjdelport -.. _`Sandra Mau`: https://github.com/xthepoet -.. _`Simon Ye`: https://github.com/yesimon -.. _`Tinnet Coronam`: https://github.com/tinnet -.. _`Philippe O. Wagner`: mailto:admin@arteria.ch -.. _`Max Arnold`: https://github.com/max-arnold -.. _`Martín Gaitán`: https://github.com/mgaitan -.. _`Craig Bruce`: https://github.com/craigbruce -.. _`Peter Bittner`: https://github.com/bittner -.. _`Scott Adams`: https://github.com/7wonders -.. _`Eric Amador`: https://github.com/amadornimbis -.. _`Alexandre Pocquet`: https://github.com/apocquet -.. _`Brad Pitcher`: https://github.com/brad -.. _`Hugo Osvaldo Barrera`: https://github.com/hobarrera -.. _`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/ -.. _`Scott Karlin`: https://github.com/sckarlin diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6ddaa21..4f3cc3f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,8 +1,53 @@ +(unreleased) +------------ +* Fix GA gtag user_id setup and add support for custom dimensions (Erick Massip) +* Change spelling of "JavaScript" across all files in docstrings and docs + (Peter Bittner) + +Version 3.2.0 +------------- +* Remove deprecated Piwik integration. Use Matomo instead! (Peter Bittner) +* Migrate packaging from setup.py to pyproject.toml with Ruff for linting + and formatting (Peter Bittner) +* Remove obsolete type attribute in script tags for JavaScript (Peter Bittner) +* Drop the end-of-life Python 3.8 as required by changed semantics of the + license metadata field (Peter Bittner) +* Remove AUTHORS file to avoid confusion; this is now metadata maintained + in pyproject.toml (Peter Bittner) +* Add more configuration options for Woopra (Peter Bittner) + +Version 3.1.0 +------------- +* Rename default branch from master to main (Peter Bittner, Jannis Leidel) +* Modernize packaging setup, add pyproject.toml (Peter Bittner) +* Integrate isort, reorganize imports (David Smith) +* Refactor test suite from Python unit tests to Pytest (David Smith) +* Add Heap integration (Garrett Coakley) +* Drop Django 3.1, cover Django 4.0 and Python 3.10 in test suite (David Smith) + +Version 3.0.0 +------------- +* Add support for Lucky Orange (Peter Bittner) +* Add missing instructions in Installation chapter of the docs (Peter Bittner) +* Migrate test setup to Pytest (David Smith, Peter Bittner, Pi Delport) +* Support Django 3.1 and Python 3.9, drop Django 1.11 and Python 2.7/3.5 (David Smith) +* Migrate from Travis CI to GitHub Actions (Jannis Leidel) +* Update accepted patterns (regex) for Google Analytics GTag (Taha Rushain) +* Scope Piwik warning to use of Piwik (Hugo Barrera) +* Add ``user_id`` to Google Analytics GTag (Sean Wallace) + +Version 2.6.0 +------------- +* Support Django 3.0 and Python 3.8, drop Django 2.1 +* Add support for Google Analytics Tag Manager (Marc Bourqui) +* Add Matomo, the renamed version of Piwik (Scott Karlin) +* Move Joost's project over to the Jazzband + Version 2.5.0 ------------- * Add support for Google analytics.js (Marc Bourqui) * Add support for Intercom HMAC identity verification (Pi Delport) -* Add support for HotJar (Pi Delport) +* Add support for Hotjar (Pi Delport) * Make sure _trackPageview happens before other settings in Google Analytics (Diederik van der Boor) @@ -172,7 +217,7 @@ Version 0.5.0 ------------- * Split off Geckoboard support into django-geckoboard_. -.. _django-geckoboard: http://pypi.python.org/pypi/django-geckoboard +.. _django-geckoboard: https://pypi.org/project/django-geckoboard Version 0.4.0 ------------- diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..e0d5efa --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Code of Conduct + +As contributors and maintainers of the Jazzband projects, and in the interest of +fostering an open and welcoming community, we pledge to respect all people who +contribute through reporting issues, posting feature requests, updating documentation, +submitting pull requests or patches, and other activities. + +We are committed to making participation in the Jazzband a harassment-free experience +for everyone, regardless of the level of experience, gender, gender identity and +expression, sexual orientation, disability, personal appearance, body size, race, +ethnicity, age, religion, or nationality. + +Examples of unacceptable behavior by participants include: + +- The use of sexualized language or imagery +- Personal attacks +- Trolling or insulting/derogatory comments +- Public or private harassment +- Publishing other's private information, such as physical or electronic addresses, + without explicit permission +- Other unethical or unprofessional conduct + +The Jazzband roadies have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are not +aligned to this Code of Conduct, or to ban temporarily or permanently any contributor +for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +By adopting this Code of Conduct, the roadies commit themselves to fairly and +consistently applying these principles to every aspect of managing the jazzband +projects. Roadies who do not follow or enforce the Code of Conduct may be permanently +removed from the Jazzband roadies. + +This code of conduct applies both within project spaces and in public spaces when an +individual is representing the project or its community. + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by +contacting the roadies at `roadies@jazzband.co`. All complaints will be reviewed and +investigated and will result in a response that is deemed necessary and appropriate to +the circumstances. Roadies are obligated to maintain confidentiality with regard to the +reporter of an incident. + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version +1.3.0, available at [https://contributor-covenant.org/version/1/3/0/][version] + +[homepage]: https://contributor-covenant.org +[version]: https://contributor-covenant.org/version/1/3/0/ diff --git a/MANIFEST.in b/MANIFEST.in index 8bded2d..444ac9e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,3 @@ include LICENSE.txt *.rst recursive-include docs *.rst *.py +recursive-include tests *.py diff --git a/README.rst b/README.rst index 0fa7527..0ed5fe1 100644 --- a/README.rst +++ b/README.rst @@ -1,14 +1,14 @@ django-analytical |latest-version| ================================== -|build-status| |coverage| |python-support| |license| |gitter| |jazzband| +|build-status| |coverage| |python-support| |license| |jazzband| The django-analytical application integrates analytics services into a Django_ project. .. start docs include -Using an analytics service with a Django project means adding Javascript +Using an analytics service with a Django project means adding JavaScript tracking code to the project templates. Of course, every service has its own specific installation instructions. Furthermore, you need to include your unique identifiers, which then end up in the templates. @@ -19,31 +19,28 @@ behind a generic interface, and keeps personal information and configuration out of the templates. Its goal is to make the basic set-up very simple, while allowing advanced users to customize tracking. Each service is set up as recommended by the services themselves, using -an asynchronous version of the Javascript code if possible. +an asynchronous version of the JavaScript code if possible. .. end docs include .. |latest-version| image:: https://img.shields.io/pypi/v/django-analytical.svg :alt: Latest version on PyPI :target: https://pypi.org/project/django-analytical/ -.. |build-status| image:: https://img.shields.io/travis/jazzband/django-analytical/master.svg - :alt: Build status - :target: https://travis-ci.org/jazzband/django-analytical -.. |coverage| image:: https://img.shields.io/coveralls/github/jazzband/django-analytical/master.svg +.. |build-status| image:: https://github.com/jazzband/django-analytical/workflows/Test/badge.svg + :target: https://github.com/jazzband/django-analytical/actions + :alt: GitHub Actions +.. |coverage| image:: https://codecov.io/gh/jazzband/django-analytical/branch/main/graph/badge.svg :alt: Test coverage - :target: https://coveralls.io/r/jazzband/django-analytical + :target: https://codecov.io/gh/jazzband/django-analytical .. |python-support| image:: https://img.shields.io/pypi/pyversions/django-analytical.svg :target: https://pypi.org/project/django-analytical/ :alt: Python versions .. |license| image:: https://img.shields.io/pypi/l/django-analytical.svg :alt: Software license - :target: https://github.com/jazzband/django-analytical/blob/master/LICENSE.txt -.. |gitter| image:: https://img.shields.io/gitter/room/jazzband/django-analytical.svg - :alt: Gitter chat room - :target: https://gitter.im/jazzband/django-analytical + :target: https://github.com/jazzband/django-analytical/blob/main/LICENSE.txt .. |jazzband| image:: https://jazzband.co/static/img/badge.svg :alt: Jazzband - :target: https://jazzband.co/ + :target: https://jazzband.co/projects/django-analytical .. _`Django`: http://www.djangoproject.com/ Currently Supported Services @@ -57,11 +54,13 @@ Currently Supported Services * `Gaug.es`_ real time web analytics * `Google Analytics`_ traffic analysis * `GoSquared`_ traffic monitoring +* `Heap`_ analytics and events tracking * `Hotjar`_ analytics and user feedback * `HubSpot`_ inbound marketing * `Intercom`_ live chat and support * `KISSinsights`_ feedback surveys * `KISSmetrics`_ funnel analysis +* `Lucky Orange`_ analytics and user feedback * `Mixpanel`_ event tracking * `Olark`_ visitor chat * `Optimizely`_ A/B testing @@ -75,18 +74,20 @@ Currently Supported Services * `Yandex.Metrica`_ web analytics .. _`Chartbeat`: http://www.chartbeat.com/ -.. _`Clickmap`: http://getclickmap.com/ +.. _`Clickmap`: http://clickmap.ch/ .. _`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/ +.. _`Heap`: https://heapanalytics.com/ .. _`Hotjar`: https://www.hotjar.com/ .. _`HubSpot`: http://www.hubspot.com/ .. _`Intercom`: http://www.intercom.io/ .. _`KISSinsights`: http://www.kissinsights.com/ .. _`KISSmetrics`: http://www.kissmetrics.com/ +.. _`Lucky Orange`: http://www.luckyorange.com/ .. _`Mixpanel`: http://www.mixpanel.com/ .. _`Olark`: http://www.olark.com/ .. _`Optimizely`: http://www.optimizely.com/ @@ -104,9 +105,7 @@ Documentation and Support The documentation can be found in the ``docs`` directory or `read online`_. The source code and issue tracker are generously `hosted by -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`_. +GitHub`_. .. _`read online`: https://django-analytical.readthedocs.io/ .. _`hosted by GitHub`: https://github.com/jazzband/django-analytical @@ -124,11 +123,17 @@ 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. +At the end, don't forget to add yourself to the `list of authors`_ and +update the `changelog`_ with a short description of your contribution. +We want you to stand out from the crowd as an open source superstar! ✦ + This is a `Jazzband`_ project. By contributing you agree to abide by the `Contributor Code of Conduct`_ and follow the `guidelines`_. .. _`issue tracker`: https://github.com/jazzband/django-analytical/issues .. _`pull request`: https://github.com/jazzband/django-analytical/pulls +.. _`list of authors`: https://github.com/jazzband/django-analytical/blob/main/pyproject.toml +.. _`changelog`: https://github.com/jazzband/django-analytical/blob/main/CHANGELOG.rst .. _`Jazzband`: https://jazzband.co .. _`Contributor Code of Conduct`: https://jazzband.co/about/conduct .. _`guidelines`: https://jazzband.co/about/guidelines diff --git a/analytical/__init__.py b/analytical/__init__.py index 9a9d9b1..cf46079 100644 --- a/analytical/__init__.py +++ b/analytical/__init__.py @@ -1,9 +1,5 @@ """ -Analytics service integration for Django projects +Analytics service integration for Django projects. """ -__author__ = "Joost Cassee" -__email__ = "joost@cassee.net" -__version__ = "2.5.0" -__copyright__ = "Copyright (C) 2011-2019 Joost Cassee and contributors" -__license__ = "MIT" +__version__ = '3.2.0' diff --git a/analytical/templatetags/analytical.py b/analytical/templatetags/analytical.py index f74684e..7ae606b 100644 --- a/analytical/templatetags/analytical.py +++ b/analytical/templatetags/analytical.py @@ -2,17 +2,14 @@ Analytical template tags and filters. """ -from __future__ import absolute_import - import logging +from importlib import import_module from django import template from django.template import Node, TemplateSyntaxError -from importlib import import_module from analytical.utils import AnalyticalException - TAG_LOCATIONS = ['head_top', 'head_bottom', 'body_top', 'body_bottom'] TAG_POSITIONS = ['first', None, 'last'] TAG_MODULES = [ @@ -24,18 +21,20 @@ TAG_MODULES = [ 'analytical.gauges', 'analytical.google_analytics', 'analytical.google_analytics_js', + 'analytical.google_analytics_gtag', 'analytical.gosquared', + 'analytical.heap', 'analytical.hotjar', 'analytical.hubspot', 'analytical.intercom', 'analytical.kiss_insights', 'analytical.kiss_metrics', + 'analytical.luckyorange', 'analytical.matomo', 'analytical.mixpanel', 'analytical.olark', 'analytical.optimizely', 'analytical.performable', - 'analytical.piwik', 'analytical.rating_mailru', 'analytical.snapengage', 'analytical.spring_metrics', @@ -67,12 +66,11 @@ class AnalyticalNode(Node): self.nodes = [node_cls() for node_cls in template_nodes[location]] def render(self, context): - return "".join([node.render(context) for node in self.nodes]) + return ''.join([node.render(context) for node in self.nodes]) def _load_template_nodes(): - template_nodes = dict((l, dict((p, []) for p in TAG_POSITIONS)) - for l in TAG_LOCATIONS) + template_nodes = {loc: {pos: [] for pos in TAG_POSITIONS} for loc in TAG_LOCATIONS} def add_node_cls(location, node, position=None): template_nodes[location][position].append(node) @@ -84,14 +82,15 @@ def _load_template_nodes(): except AnalyticalException as e: logger.debug("not loading tags from '%s': %s", path, e) for location in TAG_LOCATIONS: - template_nodes[location] = sum((template_nodes[location][p] - for p in TAG_POSITIONS), []) + template_nodes[location] = sum( + (template_nodes[location][p] for p in TAG_POSITIONS), [] + ) return template_nodes def _import_tag_module(path): app_name, lib_name = path.rsplit('.', 1) - return import_module("%s.templatetags.%s" % (app_name, lib_name)) + return import_module('%s.templatetags.%s' % (app_name, lib_name)) template_nodes = _load_template_nodes() diff --git a/analytical/templatetags/chartbeat.py b/analytical/templatetags/chartbeat.py index 6168c13..dfac04b 100644 --- a/analytical/templatetags/chartbeat.py +++ b/analytical/templatetags/chartbeat.py @@ -2,8 +2,6 @@ Chartbeat template tags and filters. """ -from __future__ import absolute_import - import json import re @@ -11,13 +9,12 @@ from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.template import Library, Node, TemplateSyntaxError -from analytical.utils import is_internal_ip, disable_html, get_required_setting - +from analytical.utils import disable_html, get_required_setting, is_internal_ip USER_ID_RE = re.compile(r'^\d+$') -INIT_CODE = """""" +INIT_CODE = """""" SETUP_CODE = """ - '.\ - format(placeholder_url='//dnn506yrbagrg.cloudfront.net/pages/scripts/' - '%(account_nr_1)s/%(account_nr_2)s.js') +SETUP_CODE = ''.format( + placeholder_url='//dnn506yrbagrg.cloudfront.net/pages/scripts/' + '%(account_nr_1)s/%(account_nr_2)s.js' +) USERVAR_CODE = "CE2.set(%(varnr)d, '%(value)s');" @@ -27,7 +24,7 @@ def crazy_egg(parser, token): """ Crazy Egg tracking template tag. - Renders Javascript code to track page clicks. You must supply + Renders JavaScript code to track page clicks. You must supply your Crazy Egg account number (as a string) in the ``CRAZY_EGG_ACCOUNT_NUMBER`` setting. """ @@ -41,7 +38,8 @@ class CrazyEggNode(Node): def __init__(self): self.account_nr = get_required_setting( 'CRAZY_EGG_ACCOUNT_NUMBER', - ACCOUNT_NUMBER_RE, "must be (a string containing) a number" + ACCOUNT_NUMBER_RE, + 'must be (a string containing) a number', ) def render(self, context): @@ -52,12 +50,15 @@ class CrazyEggNode(Node): values = (context.get('crazy_egg_var%d' % i) for i in range(1, 6)) params = [(i, v) for i, v in enumerate(values, 1) if v is not None] if params: - js = " ".join(USERVAR_CODE % { - 'varnr': varnr, - 'value': value, - } for (varnr, value) in params) - html = '%s\n' \ - '' % (html, js) + js = ' '.join( + USERVAR_CODE + % { + 'varnr': varnr, + 'value': value, + } + for (varnr, value) in params + ) + html = '%s\n' % (html, js) if is_internal_ip(context, 'CRAZY_EGG'): html = disable_html(html, 'Crazy Egg') return html diff --git a/analytical/templatetags/facebook_pixel.py b/analytical/templatetags/facebook_pixel.py index 33dac0a..e1b5e43 100644 --- a/analytical/templatetags/facebook_pixel.py +++ b/analytical/templatetags/facebook_pixel.py @@ -1,14 +1,12 @@ """ 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 - +from analytical.utils import disable_html, get_required_setting, is_internal_ip FACEBOOK_PIXEL_HEAD_CODE = """\ + +""" + +register = Library() + + +@register.tag +def google_analytics_gtag(parser, token): + """ + Google Analytics tracking template tag. + + Renders JavaScript code to track page visits. You must supply + your website property ID (as a string) in the + ``GOOGLE_ANALYTICS_GTAG_PROPERTY_ID`` setting. + """ + bits = token.split_contents() + if len(bits) > 1: + raise TemplateSyntaxError("'%s' takes no arguments" % bits[0]) + return GoogleAnalyticsGTagNode() + + +class GoogleAnalyticsGTagNode(Node): + def __init__(self): + self.property_id = get_required_setting( + 'GOOGLE_ANALYTICS_GTAG_PROPERTY_ID', + PROPERTY_ID_RE, + """must be a string looking like one of these patterns + ('UA-XXXXXX-Y' , 'AW-XXXXXXXXXX', + 'G-XXXXXXXX', 'DC-XXXXXXXX')""", + ) + + def render(self, context): + custom_dimensions = context.get('google_analytics_custom_dimensions', {}) + + identity = get_identity(context, prefix='google_analytics_gtag') + if identity is not None: + custom_dimensions['user_id'] = identity + + html = SETUP_CODE.format( + property_id=self.property_id, + custom_dimensions=json.dumps(custom_dimensions), + ) + if is_internal_ip(context, 'GOOGLE_ANALYTICS'): + html = disable_html(html, 'Google Analytics') + return html + + +def contribute_to_analytical(add_node): + GoogleAnalyticsGTagNode() # ensure properly configured + add_node('head_top', GoogleAnalyticsGTagNode) diff --git a/analytical/templatetags/google_analytics_js.py b/analytical/templatetags/google_analytics_js.py index d23467e..495ad03 100644 --- a/analytical/templatetags/google_analytics_js.py +++ b/analytical/templatetags/google_analytics_js.py @@ -2,10 +2,9 @@ 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 @@ -27,7 +26,7 @@ SETUP_CODE = """ (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'); +}})(window,document,'script','{js_source}','ga'); ga('create', '{property_id}', 'auto', {create_fields}); {commands}ga('send', 'pageview'); @@ -45,7 +44,7 @@ def google_analytics_js(parser, token): """ Google Analytics tracking template tag. - Renders Javascript code to track page visits. You must supply + Renders JavaScript code to track page visits. You must supply your website property ID (as a string) in the ``GOOGLE_ANALYTICS_JS_PROPERTY_ID`` setting. """ @@ -58,23 +57,35 @@ def google_analytics_js(parser, token): 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'") + '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) + display_features = getattr( + settings, 'GOOGLE_ANALYTICS_DISPLAY_ADVERTISING', False + ) if display_features: commands.insert(0, REQUIRE_DISPLAY_FEATURES) + js_source = getattr( + settings, + 'GOOGLE_ANALYTICS_JS_SOURCE', + 'https://www.google-analytics.com/analytics.js', + ) + html = SETUP_CODE.format( property_id=self.property_id, create_fields=json.dumps(create_fields), - commands="".join(commands), + commands=''.join(commands), + js_source=js_source, ) if is_internal_ip(context, 'GOOGLE_ANALYTICS'): html = disable_html(html, 'Google Analytics') @@ -82,14 +93,17 @@ class GoogleAnalyticsJsNode(Node): def _get_domain_fields(self, context): domain_fields = {} - tracking_type = getattr(settings, 'GOOGLE_ANALYTICS_TRACKING_STYLE', TRACK_SINGLE_DOMAIN) + 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") + 'tracking multiple domains with Google Analytics requires a domain name' + ) domain_fields['legacyCookieDomain'] = domain if tracking_type == TRACK_MULTIPLE_DOMAINS: domain_fields['allowLinker'] = True @@ -98,34 +112,39 @@ class GoogleAnalyticsJsNode(Node): def _get_other_create_fields(self, context): other_fields = {} - site_speed_sample_rate = getattr(settings, 'GOOGLE_ANALYTICS_SITE_SPEED_SAMPLE_RATE', False) + 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") + "'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") + 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") + 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) - ) + 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: @@ -134,11 +153,13 @@ class GoogleAnalyticsJsNode(Node): try: float(value) except ValueError: - value = "'{}'".format(value) - commands.append(CUSTOM_VAR_CODE.format( - name=name, - value=value, - )) + value = f"'{value}'" + commands.append( + CUSTOM_VAR_CODE.format( + name=name, + value=value, + ) + ) return commands def _get_other_commands(self, context): diff --git a/analytical/templatetags/gosquared.py b/analytical/templatetags/gosquared.py index a42267e..9668351 100644 --- a/analytical/templatetags/gosquared.py +++ b/analytical/templatetags/gosquared.py @@ -2,19 +2,20 @@ GoSquared template tags and filters. """ -from __future__ import absolute_import - import re from django.template import Library, Node, TemplateSyntaxError -from analytical.utils import get_identity, \ - is_internal_ip, disable_html, get_required_setting - +from analytical.utils import ( + disable_html, + get_identity, + get_required_setting, + is_internal_ip, +) TOKEN_RE = re.compile(r'^\S+-\S+-\S+$') TRACKING_CODE = """ - + +""" # noqa + +register = Library() + + +def _validate_no_args(token): + bits = token.split_contents() + if len(bits) > 1: + raise TemplateSyntaxError("'%s' takes no arguments" % bits[0]) + + +@register.tag +def heap(parser, token): + """ + Heap tracker template tag. + + Renders JavaScript code to track page visits. You must supply + your heap tracker ID (as a string) in the ``HEAP_TRACKER_ID`` + setting. + """ + _validate_no_args(token) + return HeapNode() + + +class HeapNode(Node): + def __init__(self): + self.tracker_id = get_required_setting( + 'HEAP_TRACKER_ID', HEAP_TRACKER_ID_RE, 'must be an numeric string' + ) + + def render(self, context): + html = TRACKING_CODE % {'tracker_id': self.tracker_id} + if is_internal_ip(context, 'HEAP'): + html = disable_html(html, 'Heap') + return html + + +def contribute_to_analytical(add_node): + HeapNode() # ensure properly configured + add_node('head_bottom', HeapNode) diff --git a/analytical/templatetags/hotjar.py b/analytical/templatetags/hotjar.py index b85a126..9c5bf38 100644 --- a/analytical/templatetags/hotjar.py +++ b/analytical/templatetags/hotjar.py @@ -1,25 +1,23 @@ """ 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 - +from analytical.utils import disable_html, get_required_setting, is_internal_ip HOTJAR_TRACKING_CODE = """\ """ @@ -43,12 +41,11 @@ def hotjar(parser, token): 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", + 'must be (a string containing) a number', ) def render(self, context): diff --git a/analytical/templatetags/hubspot.py b/analytical/templatetags/hubspot.py index fc011e3..37e7408 100644 --- a/analytical/templatetags/hubspot.py +++ b/analytical/templatetags/hubspot.py @@ -2,19 +2,16 @@ HubSpot template tags and filters. """ -from __future__ import absolute_import - import re from django.template import Library, Node, TemplateSyntaxError -from analytical.utils import is_internal_ip, disable_html, get_required_setting - +from analytical.utils import disable_html, get_required_setting, is_internal_ip PORTAL_ID_RE = re.compile(r'^\d+$') TRACKING_CODE = """ - - + + """ # noqa IDENTIFY_CODE = "_kiq.push(['identify', '%s']);" SHOW_SURVEY_CODE = "_kiq.push(['showSurvey', %s]);" @@ -30,7 +27,7 @@ def kiss_insights(parser, token): """ KISSinsights set-up template tag. - Renders Javascript code to set-up surveys. You must supply + Renders JavaScript code to set-up surveys. You must supply your account number and site code in the ``KISS_INSIGHTS_ACCOUNT_NUMBER`` and ``KISS_INSIGHTS_SITE_CODE`` settings. @@ -44,11 +41,15 @@ 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") + '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_SITE_CODE', + SITE_CODE_RE, + 'must be a string containing three characters', + ) def render(self, context): commands = [] @@ -62,7 +63,7 @@ class KissInsightsNode(Node): html = SETUP_CODE % { 'account_number': self.account_number, 'site_code': self.site_code, - 'commands': " ".join(commands), + 'commands': ' '.join(commands), } return html diff --git a/analytical/templatetags/kiss_metrics.py b/analytical/templatetags/kiss_metrics.py index 4706dbd..683e15a 100644 --- a/analytical/templatetags/kiss_metrics.py +++ b/analytical/templatetags/kiss_metrics.py @@ -2,20 +2,21 @@ KISSmetrics template tags. """ -from __future__ import absolute_import - import json import re from django.template import Library, Node, TemplateSyntaxError -from analytical.utils import is_internal_ip, disable_html, get_identity, \ - get_required_setting - +from analytical.utils import ( + disable_html, + get_identity, + get_required_setting, + is_internal_ip, +) API_KEY_RE = re.compile(r'^[0-9a-f]{40}$') 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 luckyorange(parser, token): + """ + Lucky Orange template tag. + """ + _validate_no_args(token) + return LuckyOrangeNode() + + +class LuckyOrangeNode(Node): + def __init__(self): + self.site_id = get_required_setting( + 'LUCKYORANGE_SITE_ID', + re.compile(r'^\d+$'), + 'must be (a string containing) a number', + ) + + def render(self, context): + html = LUCKYORANGE_TRACKING_CODE % {'LUCKYORANGE_SITE_ID': self.site_id} + if is_internal_ip(context, 'LUCKYORANGE'): + return disable_html(html, 'Lucky Orange') + else: + return html + + +def contribute_to_analytical(add_node): + # ensure properly configured + LuckyOrangeNode() + add_node('head_bottom', LuckyOrangeNode) diff --git a/analytical/templatetags/matomo.py b/analytical/templatetags/matomo.py index 977cb91..9107516 100644 --- a/analytical/templatetags/matomo.py +++ b/analytical/templatetags/matomo.py @@ -2,18 +2,19 @@ Matomo template tags and filters. """ -from __future__ import absolute_import - +import re from collections import namedtuple from itertools import chain -import re from django.conf import settings from django.template import Library, Node, TemplateSyntaxError -from analytical.utils import (is_internal_ip, disable_html, - get_required_setting, get_identity) - +from analytical.utils import ( + disable_html, + get_identity, + get_required_setting, + is_internal_ip, +) # domain name (characters separated by a dot), optional port, optional URI path, no slash DOMAINPATH_RE = re.compile(r'^(([^./?#@:]+\.)*[^./?#@:]+)+(:[0-9]+)?(/[^/?#@:]+)*$') @@ -22,7 +23,7 @@ DOMAINPATH_RE = re.compile(r'^(([^./?#@:]+\.)*[^./?#@:]+)+(:[0-9]+)?(/[^/?#@:]+) SITEID_RE = re.compile(r'^\d+$') TRACKING_CODE = """ - - + """ # noqa -VARIABLE_CODE = '_paq.push(["setCustomVariable", %(index)s, "%(name)s", "%(value)s", "%(scope)s"]);' # 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']);" @@ -80,7 +83,7 @@ def matomo(parser, token): """ Matomo tracking template tag. - Renders Javascript code to track page visits. You must supply + Renders JavaScript code to track page visits. You must supply your Matomo domain (plus optional URI path), and tracked site ID in the ``MATOMO_DOMAIN_PATH`` and the ``MATOMO_SITE_ID`` setting. @@ -98,23 +101,27 @@ def matomo(parser, token): class MatomoNode(Node): def __init__(self): - self.domain_path = \ - get_required_setting('MATOMO_DOMAIN_PATH', DOMAINPATH_RE, - "must be a domain name, optionally followed " - "by an URI path, no trailing slash (e.g. " - "matomo.example.com or my.matomo.server/path)") - self.site_id = \ - get_required_setting('MATOMO_SITE_ID', SITEID_RE, - "must be a (string containing a) number") + self.domain_path = get_required_setting( + 'MATOMO_DOMAIN_PATH', + DOMAINPATH_RE, + 'must be a domain name, optionally followed ' + 'by an URI path, no trailing slash (e.g. ' + 'matomo.example.com or my.matomo.server/path)', + ) + self.site_id = get_required_setting( + 'MATOMO_SITE_ID', SITEID_RE, 'must be a (string containing a) number' + ) def render(self, context): custom_variables = context.get('matomo_vars', ()) - complete_variables = (var if len(var) >= 4 else var + (DEFAULT_SCOPE,) - for var in custom_variables) + complete_variables = ( + var if len(var) >= 4 else var + (DEFAULT_SCOPE,) for var in custom_variables + ) - variables_code = (VARIABLE_CODE % MatomoVar(*var)._asdict() - for var in complete_variables) + variables_code = ( + VARIABLE_CODE % MatomoVar(*var)._asdict() for var in complete_variables + ) commands = [] if getattr(settings, 'MATOMO_DISABLE_COOKIES', False): @@ -125,15 +132,15 @@ class MatomoNode(Node): userid = get_identity(context, 'matomo') if userid is not None: - variables_code = chain(variables_code, ( - IDENTITY_CODE % {'userid': userid}, - )) + variables_code = chain( + variables_code, (IDENTITY_CODE % {'userid': userid},) + ) html = TRACKING_CODE % { 'url': self.domain_path, 'siteid': self.site_id, 'variables': '\n '.join(variables_code), - 'commands': '\n '.join(commands) + 'commands': '\n '.join(commands), } if is_internal_ip(context, 'MATOMO'): html = disable_html(html, 'Matomo') diff --git a/analytical/templatetags/mixpanel.py b/analytical/templatetags/mixpanel.py index caab030..3bc94b2 100644 --- a/analytical/templatetags/mixpanel.py +++ b/analytical/templatetags/mixpanel.py @@ -2,21 +2,22 @@ Mixpanel template tags and filters. """ -from __future__ import absolute_import - import json import re from django.template import Library, Node, TemplateSyntaxError from django.utils.safestring import mark_safe -from analytical.utils import is_internal_ip, disable_html, get_identity, \ - get_required_setting - +from analytical.utils import ( + disable_html, + get_identity, + get_required_setting, + is_internal_ip, +) MIXPANEL_API_TOKEN_RE = re.compile(r'^[0-9a-f]{32}$') TRACKING_CODE = """ - """ # noqa IDENTIFY_CODE = "mixpanel.identify('%s');" -IDENTIFY_PROPERTIES = "mixpanel.people.set(%s);" +IDENTIFY_PROPERTIES = 'mixpanel.people.set(%s);' EVENT_CODE = "mixpanel.track('%(name)s', %(properties)s);" EVENT_CONTEXT_KEY = 'mixpanel_event' @@ -36,7 +37,7 @@ def mixpanel(parser, token): """ Mixpanel tracking template tag. - Renders Javascript code to track page visits. You must supply + Renders JavaScript code to track page visits. You must supply your Mixpanel token in the ``MIXPANEL_API_TOKEN`` setting. """ bits = token.split_contents() @@ -48,29 +49,38 @@ def mixpanel(parser, token): class MixpanelNode(Node): def __init__(self): self._token = get_required_setting( - 'MIXPANEL_API_TOKEN', MIXPANEL_API_TOKEN_RE, - "must be a string containing a 32-digit hexadecimal number") + 'MIXPANEL_API_TOKEN', + MIXPANEL_API_TOKEN_RE, + 'must be a string containing a 32-digit hexadecimal number', + ) def render(self, context): commands = [] identity = get_identity(context, 'mixpanel') if identity is not None: if isinstance(identity, dict): - commands.append(IDENTIFY_CODE % identity.get('id', identity.get('username'))) - commands.append(IDENTIFY_PROPERTIES % json.dumps(identity, sort_keys=True)) + commands.append( + IDENTIFY_CODE % identity.get('id', identity.get('username')) + ) + commands.append( + IDENTIFY_PROPERTIES % json.dumps(identity, sort_keys=True) + ) else: 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), + 'commands': ' '.join(commands), } if is_internal_ip(context, 'MIXPANEL'): html = disable_html(html, 'Mixpanel') diff --git a/analytical/templatetags/olark.py b/analytical/templatetags/olark.py index 2b85648..e1ceb07 100644 --- a/analytical/templatetags/olark.py +++ b/analytical/templatetags/olark.py @@ -2,8 +2,6 @@ Olark template tags. """ -from __future__ import absolute_import - import json import re @@ -11,7 +9,6 @@ from django.template import Library, Node, TemplateSyntaxError from analytical.utils import get_identity, get_required_setting - SITE_ID_RE = re.compile(r'^\d+-\d+-\d+-\d+$') SETUP_CODE = """ """ @@ -23,7 +20,7 @@ def optimizely(parser, token): """ Optimizely template tag. - Renders Javascript code to set-up A/B testing. You must supply + Renders JavaScript code to set-up A/B testing. You must supply your Optimizely account number in the ``OPTIMIZELY_ACCOUNT_NUMBER`` setting. """ @@ -36,8 +33,10 @@ def optimizely(parser, token): class OptimizelyNode(Node): def __init__(self): self.account_number = get_required_setting( - 'OPTIMIZELY_ACCOUNT_NUMBER', ACCOUNT_NUMBER_RE, - "must be a string looking like 'XXXXXXX'") + 'OPTIMIZELY_ACCOUNT_NUMBER', + ACCOUNT_NUMBER_RE, + "must be a string looking like 'XXXXXXX'", + ) def render(self, context): html = SETUP_CODE % {'account_number': self.account_number} diff --git a/analytical/templatetags/performable.py b/analytical/templatetags/performable.py index 3364e18..55bbe40 100644 --- a/analytical/templatetags/performable.py +++ b/analytical/templatetags/performable.py @@ -2,30 +2,31 @@ Performable template tags and filters. """ -from __future__ import absolute_import - import re from django.template import Library, Node, TemplateSyntaxError from django.utils.safestring import mark_safe -from analytical.utils import is_internal_ip, disable_html, get_identity, \ - get_required_setting - +from analytical.utils import ( + disable_html, + get_identity, + get_required_setting, + is_internal_ip, +) API_KEY_RE = re.compile(r'^\w+$') SETUP_CODE = """ - + """ # noqa IDENTIFY_CODE = """ - """ EMBED_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']);" - -GIVE_CONSENT_CLASS = "piwik_give_consent" -REMOVE_CONSENT_CLASS = "piwik_remove_consent" -ASK_FOR_CONSENT_CODE = """ -_paq.push(['requireConsent']); - -var elements = document.getElementsByClassName("{}"); -for (var i = 0; i < elements.length; i++) {{ - elements[i].addEventListener("click", - function () {{ - _paq.push(["forgetConsentGiven"]); - }} - ); -}} - -var elements = document.getElementsByClassName("{}"); -for (var i = 0; i < elements.length; i++) {{ - elements[i].addEventListener("click", - function () {{ - _paq.push(["rememberConsentGiven"]); - }} - ); -}} -""".format(REMOVE_CONSENT_CLASS, GIVE_CONSENT_CLASS) - -DEFAULT_SCOPE = 'page' - -PiwikVar = namedtuple('PiwikVar', ('index', 'name', 'value', 'scope')) - - -register = Library() - - -@register.tag -def piwik(parser, token): - """ - Piwik tracking template tag. - - Renders Javascript code to track page visits. You must supply - your Piwik domain (plus optional URI path), and tracked site ID - in the ``PIWIK_DOMAIN_PATH`` and the ``PIWIK_SITE_ID`` setting. - - Custom variables can be passed in the ``piwik_vars`` context - variable. It is an iterable of custom variables as tuples like: - ``(index, name, value[, scope])`` where scope may be ``'page'`` - (default) or ``'visit'``. Index should be an integer and the - other parameters should be strings. - """ - bits = token.split_contents() - if len(bits) > 1: - raise TemplateSyntaxError("'%s' takes no arguments" % bits[0]) - return PiwikNode() - - -class PiwikNode(Node): - def __init__(self): - self.domain_path = \ - get_required_setting('PIWIK_DOMAIN_PATH', DOMAINPATH_RE, - "must be a domain name, optionally followed " - "by an URI path, no trailing slash (e.g. " - "piwik.example.com or my.piwik.server/path)") - self.site_id = \ - get_required_setting('PIWIK_SITE_ID', SITEID_RE, - "must be a (string containing a) number") - - def render(self, context): - custom_variables = context.get('piwik_vars', ()) - - complete_variables = (var if len(var) >= 4 else var + (DEFAULT_SCOPE,) - for var in custom_variables) - - variables_code = (VARIABLE_CODE % PiwikVar(*var)._asdict() - for var in complete_variables) - - commands = [] - if getattr(settings, 'PIWIK_DISABLE_COOKIES', False): - commands.append(DISABLE_COOKIES_CODE) - - if getattr(settings, 'PIWIK_ASK_FOR_CONSENT', False): - commands.append(ASK_FOR_CONSENT_CODE) - - userid = get_identity(context, 'piwik') - if userid is not None: - variables_code = chain(variables_code, ( - IDENTITY_CODE % {'userid': userid}, - )) - - html = TRACKING_CODE % { - 'url': self.domain_path, - 'siteid': self.site_id, - 'variables': '\n '.join(variables_code), - 'commands': '\n '.join(commands) - } - if is_internal_ip(context, 'PIWIK'): - html = disable_html(html, 'Piwik') - return html - - -def contribute_to_analytical(add_node): - PiwikNode() # ensure properly configured - add_node('body_bottom', PiwikNode) diff --git a/analytical/templatetags/rating_mailru.py b/analytical/templatetags/rating_mailru.py index ba1da7e..aa61a31 100644 --- a/analytical/templatetags/rating_mailru.py +++ b/analytical/templatetags/rating_mailru.py @@ -2,19 +2,15 @@ Rating@Mail.ru template tags and filters. """ -from __future__ import absolute_import - import re from django.template import Library, Node, TemplateSyntaxError -from analytical.utils import is_internal_ip, disable_html, \ - get_required_setting - +from analytical.utils import disable_html, get_required_setting, is_internal_ip COUNTER_ID_RE = re.compile(r'^\d{7}$') COUNTER_CODE = """ - """ # noqa DOMAIN_CODE = 'SnapABug.setDomain("%s");' SECURE_CONNECTION_CODE = 'SnapABug.setSecureConnexion();' INIT_CODE = 'SnapABug.init("%s");' -ADDBUTTON_CODE = 'SnapABug.addButton("%(id)s","%(location)s","%(offset)s"%(dynamic_tail)s);' +ADDBUTTON_CODE = ( + 'SnapABug.addButton("%(id)s","%(location)s","%(offset)s"%(dynamic_tail)s);' +) SETBUTTON_CODE = 'SnapABug.setButton("%s");' SETEMAIL_CODE = 'SnapABug.setUserEmail("%s"%s);' SETLOCALE_CODE = 'SnapABug.setLocale("%s");' @@ -57,7 +59,7 @@ def snapengage(parser, token): """ SnapEngage set-up template tag. - Renders Javascript code to set-up SnapEngage chat. You must supply + Renders JavaScript code to set-up SnapEngage chat. You must supply your widget ID in the ``SNAPENGAGE_WIDGET_ID`` setting. """ bits = token.split_contents() @@ -69,21 +71,24 @@ 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'") + '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') + domain = self._get_setting(context, '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) + secure_connection = self._get_setting( + context, + 'snapengage_secure_connection', + 'SNAPENGAGE_SECURE_CONNECTION', + False, + ) if secure_connection: settings_code.append(SECURE_CONNECTION_CODE) @@ -91,61 +96,70 @@ class SnapEngageNode(Node): if email is None: 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): + if self._get_setting( + context, 'snapengage_readonly_email', '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') + locale = self._get_setting(context, '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') + form_position = self._get_setting( + context, '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') + form_top_position = self._get_setting( + context, '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) + show_offline = self._get_setting( + context, 'snapengage_show_offline', 'SNAPENGAGE_SHOW_OFFLINE', True + ) if not show_offline: settings_code.append(DISABLE_OFFLINE_CODE) - screenshots = self._get_setting(context, 'snapengage_screenshots', - 'SNAPENGAGE_SCREENSHOTS', True) + screenshots = self._get_setting( + context, 'snapengage_screenshots', '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) + offline_screenshots = self._get_setting( + context, + 'snapengage_offline_screenshots', + 'SNAPENGAGE_OFFLINE_SCREENSHOTS', + True, + ) if not offline_screenshots: settings_code.append(DISABLE_OFFLINE_SCREENSHOT_CODE) if not context.get('snapengage_proactive_chat', True): settings_code.append(DISABLE_PROACTIVE_CHAT_CODE) - sounds = self._get_setting(context, 'snapengage_sounds', - 'SNAPENGAGE_SOUNDS', True) + sounds = self._get_setting( + context, 'snapengage_sounds', 'SNAPENGAGE_SOUNDS', True + ) if not sounds: settings_code.append(DISABLE_SOUNDS_CODE) - button_effect = self._get_setting(context, 'snapengage_button_effect', - 'SNAPENGAGE_BUTTON_EFFECT') + button_effect = self._get_setting( + context, '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) + button = self._get_setting( + context, 'snapengage_button', 'SNAPENGAGE_BUTTON', BUTTON_STYLE_DEFAULT + ) if button == BUTTON_STYLE_NONE: settings_code.append(INIT_CODE % self.widget_id) else: @@ -154,21 +168,28 @@ class SnapEngageNode(Node): settings_code.append(SETBUTTON_CODE % button) button_location = self._get_setting( context, - 'snapengage_button_location', 'SNAPENGAGE_BUTTON_LOCATION', - BUTTON_LOCATION_LEFT) + '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 '', - }) + '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), + 'settings_code': ' '.join(settings_code), } return html diff --git a/analytical/templatetags/spring_metrics.py b/analytical/templatetags/spring_metrics.py index a3093ea..69d93c3 100644 --- a/analytical/templatetags/spring_metrics.py +++ b/analytical/templatetags/spring_metrics.py @@ -2,15 +2,16 @@ Spring Metrics template tags and filters. """ -from __future__ import absolute_import - import re from django.template import Library, Node, TemplateSyntaxError -from analytical.utils import get_identity, is_internal_ip, disable_html, \ - get_required_setting - +from analytical.utils import ( + disable_html, + get_identity, + get_required_setting, + is_internal_ip, +) TRACKING_ID_RE = re.compile(r'^[0-9a-f]+$') TRACKING_CODE = """ @@ -39,7 +40,7 @@ def spring_metrics(parser, token): """ Spring Metrics tracking template tag. - Renders Javascript code to track page visits. You must supply + Renders JavaScript code to track page visits. You must supply your Spring Metrics Tracking ID in the ``SPRING_METRICS_TRACKING_ID`` setting. """ @@ -52,8 +53,8 @@ def spring_metrics(parser, token): class SpringMetricsNode(Node): def __init__(self): self.tracking_id = get_required_setting( - 'SPRING_METRICS_TRACKING_ID', - TRACKING_ID_RE, "must be a hexadecimal string") + 'SPRING_METRICS_TRACKING_ID', TRACKING_ID_RE, 'must be a hexadecimal string' + ) def render(self, context): custom = {} @@ -62,8 +63,7 @@ class SpringMetricsNode(Node): if var.startswith('spring_metrics_'): custom[var[15:]] = val if 'email' not in custom: - identity = get_identity(context, 'spring_metrics', - lambda u: u.email) + identity = get_identity(context, 'spring_metrics', lambda u: u.email) if identity is not None: custom['email'] = identity @@ -80,9 +80,11 @@ class SpringMetricsNode(Node): convert = params.pop('convert', None) if convert is not None: commands.append("_springMetq.push(['convert', '%s'])" % convert) - commands.extend("_springMetq.push(['setdata', {'%s': '%s'}]);" - % (var, val) for var, val in params.items()) - return " ".join(commands) + commands.extend( + "_springMetq.push(['setdata', {'%s': '%s'}]);" % (var, val) + for var, val in params.items() + ) + return ' '.join(commands) def contribute_to_analytical(add_node): diff --git a/analytical/templatetags/uservoice.py b/analytical/templatetags/uservoice.py index 68f59db..5c260f0 100644 --- a/analytical/templatetags/uservoice.py +++ b/analytical/templatetags/uservoice.py @@ -2,19 +2,17 @@ UserVoice template tags. """ -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 get_required_setting, get_identity +from analytical.utils import get_identity, get_required_setting WIDGET_KEY_RE = re.compile(r'^[a-zA-Z0-9]*$') TRACKING_CODE = """ - -""", self.render_tag('gauges', 'gauges')) +""" + ) def test_node(self): - self.assertEqual( - """ - -""", GaugesNode().render(Context())) +""" + ) @override_settings(GAUGES_SITE_ID=None) def test_no_account_number(self): - self.assertRaises(AnalyticalException, GaugesNode) + with pytest.raises(AnalyticalException): + GaugesNode() @override_settings(GAUGES_SITE_ID='123abQ') def test_wrong_account_number(self): @@ -66,6 +73,5 @@ class GaugesTagTestCase(TagTestCase): req.META['REMOTE_ADDR'] = '1.1.1.1' context = Context({'request': req}) r = GaugesNode().render(context) - self.assertTrue(r.startswith( - ''), r) + assert r.startswith('') diff --git a/analytical/tests/test_tag_google_analytics.py b/tests/unit/test_tag_google_analytics.py similarity index 51% rename from analytical/tests/test_tag_google_analytics.py rename to tests/unit/test_tag_google_analytics.py index 67c8360..14aaeb2 100644 --- a/analytical/tests/test_tag_google_analytics.py +++ b/tests/unit/test_tag_google_analytics.py @@ -2,19 +2,28 @@ Tests for the Google Analytics template tags and filters. """ +import pytest from django.http import HttpRequest from django.template import Context from django.test.utils import override_settings +from utils import TagTestCase, TestCase -from analytical.templatetags.google_analytics import GoogleAnalyticsNode, \ - TRACK_SINGLE_DOMAIN, TRACK_MULTIPLE_DOMAINS, TRACK_MULTIPLE_SUBDOMAINS,\ - SCOPE_VISITOR, SCOPE_SESSION, SCOPE_PAGE -from analytical.tests.utils import TestCase, TagTestCase +from analytical.templatetags.google_analytics import ( + SCOPE_PAGE, + SCOPE_SESSION, + SCOPE_VISITOR, + TRACK_MULTIPLE_DOMAINS, + TRACK_MULTIPLE_SUBDOMAINS, + TRACK_SINGLE_DOMAIN, + GoogleAnalyticsNode, +) from analytical.utils import AnalyticalException -@override_settings(GOOGLE_ANALYTICS_PROPERTY_ID='UA-123456-7', - GOOGLE_ANALYTICS_TRACKING_STYLE=TRACK_SINGLE_DOMAIN) +@override_settings( + GOOGLE_ANALYTICS_PROPERTY_ID='UA-123456-7', + GOOGLE_ANALYTICS_TRACKING_STYLE=TRACK_SINGLE_DOMAIN, +) class GoogleAnalyticsTagTestCase(TagTestCase): """ Tests for the ``google_analytics`` template tag. @@ -22,62 +31,70 @@ class GoogleAnalyticsTagTestCase(TagTestCase): def test_tag(self): r = self.render_tag('google_analytics', 'google_analytics') - self.assertTrue("_gaq.push(['_setAccount', 'UA-123456-7']);" in r, r) - self.assertTrue("_gaq.push(['_trackPageview']);" in r, r) + assert "_gaq.push(['_setAccount', 'UA-123456-7']);" in r + assert "_gaq.push(['_trackPageview']);" in r def test_node(self): r = GoogleAnalyticsNode().render(Context()) - self.assertTrue("_gaq.push(['_setAccount', 'UA-123456-7']);" in r, r) - self.assertTrue("_gaq.push(['_trackPageview']);" in r, r) + assert "_gaq.push(['_setAccount', 'UA-123456-7']);" in r + assert "_gaq.push(['_trackPageview']);" in r @override_settings(GOOGLE_ANALYTICS_PROPERTY_ID=None) def test_no_property_id(self): - self.assertRaises(AnalyticalException, GoogleAnalyticsNode) + with pytest.raises(AnalyticalException): + GoogleAnalyticsNode() @override_settings(GOOGLE_ANALYTICS_PROPERTY_ID='wrong') def test_wrong_property_id(self): - self.assertRaises(AnalyticalException, GoogleAnalyticsNode) + with pytest.raises(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) + assert "_gaq.push(['_setDomainName', 'example.com']);" in r + assert "_gaq.push(['_setAllowHash', false]);" in r - @override_settings(GOOGLE_ANALYTICS_TRACKING_STYLE=TRACK_MULTIPLE_DOMAINS, - GOOGLE_ANALYTICS_DOMAIN='example.com') + @override_settings( + GOOGLE_ANALYTICS_TRACKING_STYLE=TRACK_MULTIPLE_DOMAINS, + GOOGLE_ANALYTICS_DOMAIN='example.com', + ) def test_track_multiple_domains(self): r = GoogleAnalyticsNode().render(Context()) - self.assertTrue("_gaq.push(['_setDomainName', 'example.com']);" in r, r) - self.assertTrue("_gaq.push(['_setAllowHash', false]);" in r, r) - self.assertTrue("_gaq.push(['_setAllowLinker', true]);" in r, r) + assert "_gaq.push(['_setDomainName', 'example.com']);" in r + assert "_gaq.push(['_setAllowHash', false]);" in r + assert "_gaq.push(['_setAllowLinker', true]);" in r def test_custom_vars(self): - context = Context({ - 'google_analytics_var1': ('test1', 'foo'), - 'google_analytics_var2': ('test2', 'bar', SCOPE_VISITOR), - 'google_analytics_var4': ('test4', 'baz', SCOPE_SESSION), - 'google_analytics_var5': ('test5', 'qux', SCOPE_PAGE), - }) + context = Context( + { + 'google_analytics_var1': ('test1', 'foo'), + 'google_analytics_var2': ('test2', 'bar', SCOPE_VISITOR), + 'google_analytics_var4': ('test4', 'baz', SCOPE_SESSION), + 'google_analytics_var5': ('test5', 'qux', SCOPE_PAGE), + } + ) r = GoogleAnalyticsNode().render(context) - self.assertTrue("_gaq.push(['_setCustomVar', 1, 'test1', 'foo', 3]);" in r, r) - self.assertTrue("_gaq.push(['_setCustomVar', 2, 'test2', 'bar', 1]);" in r, r) - self.assertTrue("_gaq.push(['_setCustomVar', 4, 'test4', 'baz', 2]);" in r, r) - self.assertTrue("_gaq.push(['_setCustomVar', 5, 'test5', 'qux', 3]);" in r, r) + assert "_gaq.push(['_setCustomVar', 1, 'test1', 'foo', 3]);" in r + assert "_gaq.push(['_setCustomVar', 2, 'test2', 'bar', 1]);" in r + assert "_gaq.push(['_setCustomVar', 4, 'test4', 'baz', 2]);" in r + assert "_gaq.push(['_setCustomVar', 5, 'test5', 'qux', 3]);" in r @override_settings(GOOGLE_ANALYTICS_SITE_SPEED=True) def test_track_page_load_time(self): r = GoogleAnalyticsNode().render(Context()) - self.assertTrue("_gaq.push(['_trackPageLoadTime']);" in r, r) + assert "_gaq.push(['_trackPageLoadTime']);" in r def test_display_advertising(self): with override_settings(GOOGLE_ANALYTICS_DISPLAY_ADVERTISING=False): r = GoogleAnalyticsNode().render(Context()) - self.assertTrue("google-analytics.com/ga.js" in r, r) + assert 'google-analytics.com/ga.js' in r with override_settings(GOOGLE_ANALYTICS_DISPLAY_ADVERTISING=True): r = GoogleAnalyticsNode().render(Context()) - self.assertTrue("stats.g.doubleclick.net/dc.js" in r, r) + assert 'stats.g.doubleclick.net/dc.js' in r @override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1']) def test_render_internal_ip(self): @@ -85,97 +102,105 @@ class GoogleAnalyticsTagTestCase(TagTestCase): req.META['REMOTE_ADDR'] = '1.1.1.1' context = Context({'request': req}) r = GoogleAnalyticsNode().render(context) - self.assertTrue(r.startswith( - ''), r) + assert r.startswith('') @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(r.index('_gat._anonymizeIp') < r.index('_trackPageview'), r) + assert "_gaq.push(['_gat._anonymizeIp']);" in r + assert r.index('_gat._anonymizeIp') < r.index('_trackPageview') @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) + assert "_gaq.push(['_gat._anonymizeIp']);" not in 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) + assert "_gaq.push(['_setSampleRate', '0.00']);" in 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) + assert "_gaq.push(['_setSampleRate', '100.00']);" in 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) + with pytest.raises(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) + with pytest.raises(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) + assert "_gaq.push(['_setSiteSpeedSampleRate', '0.00']);" in 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) + assert "_gaq.push(['_setSiteSpeedSampleRate', '100.00']);" in 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) + with pytest.raises(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) + with pytest.raises(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) + assert "_gaq.push(['_setSessionCookieTimeout', '0']);" in 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) + assert "_gaq.push(['_setSessionCookieTimeout', '10000']);" in 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) + with pytest.raises(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) + assert "_gaq.push(['_setVisitorCookieTimeout', '0']);" in 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) + assert "_gaq.push(['_setVisitorCookieTimeout', '10000']);" in 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) + with pytest.raises(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) +@override_settings( + GOOGLE_ANALYTICS_PROPERTY_ID='UA-123456-7', + GOOGLE_ANALYTICS_TRACKING_STYLE=TRACK_MULTIPLE_DOMAINS, + GOOGLE_ANALYTICS_DOMAIN=None, + ANALYTICAL_DOMAIN=None, +) class NoDomainTestCase(TestCase): def test_exception_without_domain(self): context = Context() - self.assertRaises(AnalyticalException, GoogleAnalyticsNode().render, context) + with pytest.raises(AnalyticalException): + GoogleAnalyticsNode().render(context) diff --git a/tests/unit/test_tag_google_analytics_gtag.py b/tests/unit/test_tag_google_analytics_gtag.py new file mode 100644 index 0000000..c84c327 --- /dev/null +++ b/tests/unit/test_tag_google_analytics_gtag.py @@ -0,0 +1,164 @@ +""" +Tests for the Google Analytics template tags and filters, using the new gtag.js library. +""" + +import pytest +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 utils import TagTestCase + +from analytical.templatetags.google_analytics_gtag import GoogleAnalyticsGTagNode +from analytical.utils import AnalyticalException + + +@override_settings(GOOGLE_ANALYTICS_GTAG_PROPERTY_ID='UA-123456-7') +class GoogleAnalyticsTagTestCase(TagTestCase): + """ + Tests for the ``google_analytics_gtag`` template tag. + """ + + def test_tag(self): + r = self.render_tag('google_analytics_gtag', 'google_analytics_gtag') + assert ( + '' + ) in r + assert "gtag('js', new Date());" in r + assert "gtag('config', 'UA-123456-7', {});" in r + + def test_node(self): + r = GoogleAnalyticsGTagNode().render(Context()) + assert ( + '' + ) in r + assert "gtag('js', new Date());" in r + assert "gtag('config', 'UA-123456-7', {});" in r + + @override_settings(GOOGLE_ANALYTICS_GTAG_PROPERTY_ID=None) + def test_no_property_id(self): + with pytest.raises(AnalyticalException): + GoogleAnalyticsGTagNode() + + @override_settings(GOOGLE_ANALYTICS_GTAG_PROPERTY_ID='wrong') + def test_wrong_property_id(self): + with pytest.raises(AnalyticalException): + GoogleAnalyticsGTagNode() + + @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 = GoogleAnalyticsGTagNode().render(context) + assert r.startswith('') + + @override_settings(ANALYTICAL_AUTO_IDENTIFY=True) + def test_identify(self): + r = GoogleAnalyticsGTagNode().render(Context({'user': User(username='test')})) + assert 'gtag(\'config\', \'UA-123456-7\', {"user_id": "test"});' in r + + def test_identity_context_specific_provider(self): + """ + The user_id variable must be set according to + google_analytics_gtag_identity variable in the context. + """ + r = GoogleAnalyticsGTagNode().render( + Context( + { + 'google_analytics_gtag_identity': 'foo_gtag_identity', + 'user': User(username='test'), + } + ) + ) + assert ( + 'gtag(\'config\', \'UA-123456-7\', {"user_id": "foo_gtag_identity"});' in r + ) + + def test_identity_context_general(self): + """ + The user_id variable must be set according to analytical_identity variable in the context. + """ + r = GoogleAnalyticsGTagNode().render( + Context( + { + 'analytical_identity': 'bar_analytical_identity', + 'user': User(username='test'), + } + ) + ) + assert ( + 'gtag(\'config\', \'UA-123456-7\', {"user_id": "bar_analytical_identity"});' + in r + ) + + @override_settings(GOOGLE_ANALYTICS_GTAG_PROPERTY_ID='G-12345678') + def test_tag_with_measurement_id(self): + r = self.render_tag('google_analytics_gtag', 'google_analytics_gtag') + assert ( + '' + ) in r + assert "gtag('js', new Date());" in r + assert "gtag('config', 'G-12345678', {});" in r + + @override_settings(GOOGLE_ANALYTICS_GTAG_PROPERTY_ID='AW-1234567890') + def test_tag_with_conversion_id(self): + r = self.render_tag('google_analytics_gtag', 'google_analytics_gtag') + assert ( + '' + ) in r + assert "gtag('js', new Date());" in r + assert "gtag('config', 'DC-12345678', {});" in r + + def test_tag_with_custom_dimensions(self): + r = GoogleAnalyticsGTagNode().render( + Context( + { + 'google_analytics_custom_dimensions': { + 'dimension_1': 'foo', + 'dimension_2': 'bar', + 'user_properties': { + 'user_property_1': True, + 'user_property_2': 'xyz', + }, + }, + } + ) + ) + assert ( + "gtag('config', 'UA-123456-7', {" + '"dimension_1": "foo", ' + '"dimension_2": "bar", ' + '"user_properties": {' + '"user_property_1": true, ' + '"user_property_2": "xyz"}});' in r + ) + + def test_tag_with_identity_and_custom_dimensions(self): + r = GoogleAnalyticsGTagNode().render( + Context( + { + 'google_analytics_gtag_identity': 'foo_gtag_identity', + 'google_analytics_custom_dimensions': { + 'dimension_1': 'foo', + 'dimension_2': 'bar', + }, + } + ) + ) + assert ( + "gtag('config', 'UA-123456-7', {" + '"dimension_1": "foo", ' + '"dimension_2": "bar", ' + '"user_id": "foo_gtag_identity"});' in r + ) diff --git a/analytical/tests/test_tag_google_analytics_js.py b/tests/unit/test_tag_google_analytics_js.py similarity index 53% rename from analytical/tests/test_tag_google_analytics_js.py rename to tests/unit/test_tag_google_analytics_js.py index 517eb8b..d0e53ab 100644 --- a/analytical/tests/test_tag_google_analytics_js.py +++ b/tests/unit/test_tag_google_analytics_js.py @@ -2,18 +2,25 @@ Tests for the Google Analytics template tags and filters, using the new analytics.js library. """ +import pytest from django.http import HttpRequest from django.template import Context from django.test.utils import override_settings +from utils import TagTestCase, TestCase -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.templatetags.google_analytics_js import ( + TRACK_MULTIPLE_DOMAINS, + TRACK_MULTIPLE_SUBDOMAINS, + TRACK_SINGLE_DOMAIN, + GoogleAnalyticsJsNode, +) from analytical.utils import AnalyticalException -@override_settings(GOOGLE_ANALYTICS_JS_PROPERTY_ID='UA-123456-7', - GOOGLE_ANALYTICS_TRACKING_STYLE=TRACK_SINGLE_DOMAIN) +@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. @@ -21,64 +28,83 @@ class GoogleAnalyticsTagTestCase(TagTestCase): 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(){ + assert ( + """(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) +})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');""" + in r + ) + assert "ga('create', 'UA-123456-7', 'auto', {});" in r + assert "ga('send', 'pageview');" in 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(){ + assert ( + """(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) +})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');""" + in r + ) + assert "ga('create', 'UA-123456-7', 'auto', {});" in r + assert "ga('send', 'pageview');" in r @override_settings(GOOGLE_ANALYTICS_JS_PROPERTY_ID=None) def test_no_property_id(self): - self.assertRaises(AnalyticalException, GoogleAnalyticsJsNode) + with pytest.raises(AnalyticalException): + GoogleAnalyticsJsNode() @override_settings(GOOGLE_ANALYTICS_JS_PROPERTY_ID='wrong') def test_wrong_property_id(self): - self.assertRaises(AnalyticalException, GoogleAnalyticsJsNode) + with pytest.raises(AnalyticalException): + GoogleAnalyticsJsNode() - @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 = GoogleAnalyticsJsNode().render(Context()) - self.assertTrue( - """ga('create', 'UA-123456-7', 'auto', {"legacyCookieDomain": "example.com"}""" in r, r) + assert ( + """ga('create', 'UA-123456-7', 'auto', {"legacyCookieDomain": "example.com"}""" + in r + ) - @override_settings(GOOGLE_ANALYTICS_TRACKING_STYLE=TRACK_MULTIPLE_DOMAINS, - GOOGLE_ANALYTICS_DOMAIN='example.com') + @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) + assert "ga('create', 'UA-123456-7', 'auto', {" in r + assert '"legacyCookieDomain": "example.com"' in r + assert '"allowLinker": true' in 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), - }) + 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) + assert "ga('set', 'test1', 'foo');" in r + assert "ga('set', 'test2', 'bar');" in r + assert "ga('set', 'test4', 1);" in r + assert "ga('set', 'test5', 2.2);" in 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', {}); + assert ( + """ga('create', 'UA-123456-7', 'auto', {}); ga('require', 'displayfeatures'); -ga('send', 'pageview');""" in r, r) +ga('send', 'pageview');""" + in r + ) @override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1']) def test_render_internal_ip(self): @@ -86,84 +112,93 @@ ga('send', 'pageview');""" in r, r) req.META['REMOTE_ADDR'] = '1.1.1.1' context = Context({'request': req}) r = GoogleAnalyticsJsNode().render(context) - self.assertTrue(r.startswith( - ''), r) + assert r.startswith('') @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) + assert "ga('set', 'anonymizeIp', true);" in 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) + assert "ga('set', 'anonymizeIp', true);" not in 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) + assert """ga('create', 'UA-123456-7', 'auto', {"sampleRate": 0});""" in 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) + assert """ga('create', 'UA-123456-7', 'auto', {"sampleRate": 100});""" in 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) + with pytest.raises(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) + with pytest.raises(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) + assert ( + """ga('create', 'UA-123456-7', 'auto', {"siteSpeedSampleRate": 0});""" in 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) + assert ( + """ga('create', 'UA-123456-7', 'auto', {"siteSpeedSampleRate": 100});""" + in 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) + with pytest.raises(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) + with pytest.raises(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) + assert """ga('create', 'UA-123456-7', 'auto', {"cookieExpires": 0});""" in 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) + assert """ga('create', 'UA-123456-7', 'auto', {"cookieExpires": 10000});""" in 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) + with pytest.raises(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) +@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) + with pytest.raises(AnalyticalException): + GoogleAnalyticsJsNode().render(context) diff --git a/analytical/tests/test_tag_gosquared.py b/tests/unit/test_tag_gosquared.py similarity index 55% rename from analytical/tests/test_tag_gosquared.py rename to tests/unit/test_tag_gosquared.py index 844205c..5c27653 100644 --- a/analytical/tests/test_tag_gosquared.py +++ b/tests/unit/test_tag_gosquared.py @@ -2,13 +2,14 @@ Tests for the GoSquared template tags and filters. """ -from django.contrib.auth.models import User, AnonymousUser +import pytest +from django.contrib.auth.models import AnonymousUser, User from django.http import HttpRequest from django.template import Context from django.test.utils import override_settings +from utils import TagTestCase from analytical.templatetags.gosquared import GoSquaredNode -from analytical.tests.utils import TagTestCase from analytical.utils import AnalyticalException @@ -20,39 +21,49 @@ class GoSquaredTagTestCase(TagTestCase): def test_tag(self): r = self.render_tag('gosquared', 'gosquared') - self.assertTrue('GoSquared.acct = "ABC-123456-D";' in r, r) + assert 'GoSquared.acct = "ABC-123456-D";' in r def test_node(self): r = GoSquaredNode().render(Context({})) - self.assertTrue('GoSquared.acct = "ABC-123456-D";' in r, r) + assert 'GoSquared.acct = "ABC-123456-D";' in r @override_settings(GOSQUARED_SITE_TOKEN=None) def test_no_token(self): - self.assertRaises(AnalyticalException, GoSquaredNode) + with pytest.raises(AnalyticalException): + GoSquaredNode() @override_settings(GOSQUARED_SITE_TOKEN='this is not a token') def test_wrong_token(self): - self.assertRaises(AnalyticalException, GoSquaredNode) + with pytest.raises(AnalyticalException): + GoSquaredNode() @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'), - })) - self.assertTrue('GoSquared.UserName = "Test User";' in r, r) + r = GoSquaredNode().render( + Context( + { + 'user': User(username='test', first_name='Test', last_name='User'), + } + ) + ) + assert 'GoSquared.UserName = "Test User";' in r @override_settings(ANALYTICAL_AUTO_IDENTIFY=True) def test_manual_identify(self): - r = GoSquaredNode().render(Context({ - 'user': User(username='test', first_name='Test', last_name='User'), - 'gosquared_identity': 'test_identity', - })) - self.assertTrue('GoSquared.UserName = "test_identity";' in r, r) + r = GoSquaredNode().render( + Context( + { + 'user': User(username='test', first_name='Test', last_name='User'), + 'gosquared_identity': 'test_identity', + } + ) + ) + assert 'GoSquared.UserName = "test_identity";' in r @override_settings(ANALYTICAL_AUTO_IDENTIFY=True) def test_identify_anonymous_user(self): r = GoSquaredNode().render(Context({'user': AnonymousUser()})) - self.assertFalse('GoSquared.UserName = ' in r, r) + assert 'GoSquared.UserName = ' not in r @override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1']) def test_render_internal_ip(self): @@ -60,6 +71,5 @@ class GoSquaredTagTestCase(TagTestCase): req.META['REMOTE_ADDR'] = '1.1.1.1' context = Context({'request': req}) r = GoSquaredNode().render(context) - self.assertTrue(r.startswith( - ''), r) + assert r.startswith('') diff --git a/tests/unit/test_tag_heap.py b/tests/unit/test_tag_heap.py new file mode 100644 index 0000000..906369c --- /dev/null +++ b/tests/unit/test_tag_heap.py @@ -0,0 +1,50 @@ +""" +Tests for the Heap template tags and filters. +""" + +import pytest +from django.http import HttpRequest +from django.template import Context, Template, TemplateSyntaxError +from django.test.utils import override_settings +from utils import TagTestCase + +from analytical.templatetags.heap import HeapNode +from analytical.utils import AnalyticalException + + +@override_settings(HEAP_TRACKER_ID='123456789') +class HeapTagTestCase(TagTestCase): + """ + Tests for the ``heap`` template tag. + """ + + def test_tag(self): + r = self.render_tag('heap', 'heap') + assert '123456789' in r + + def test_node(self): + r = HeapNode().render(Context({})) + assert '123456789' in r + + def test_tags_take_no_args(self): + with pytest.raises(TemplateSyntaxError, match="'heap' takes no arguments"): + Template('{% load heap %}{% heap "arg" %}').render(Context({})) + + @override_settings(HEAP_TRACKER_ID=None) + def test_no_site_id(self): + with pytest.raises(AnalyticalException): + HeapNode() + + @override_settings(HEAP_TRACKER_ID='abcdefg') + def test_wrong_site_id(self): + with pytest.raises(AnalyticalException): + HeapNode() + + @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 = HeapNode().render(context) + assert r.startswith('') diff --git a/analytical/tests/test_tag_hotjar.py b/tests/unit/test_tag_hotjar.py similarity index 55% rename from analytical/tests/test_tag_hotjar.py rename to tests/unit/test_tag_hotjar.py index c7e656d..ba7f42e 100644 --- a/analytical/tests/test_tag_hotjar.py +++ b/tests/unit/test_tag_hotjar.py @@ -1,61 +1,59 @@ """ Tests for the Hotjar template tags. """ + +import pytest from django.http import HttpRequest from django.template import Context, Template, TemplateSyntaxError from django.test import override_settings +from utils import TagTestCase 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) + assert expected_html == html def test_node(self): html = HotjarNode().render(Context({})) - self.assertEqual(expected_html, html) + assert 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({}))), - ) + with pytest.raises(TemplateSyntaxError, match="'hotjar' takes no arguments"): + 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) + with pytest.raises( + AnalyticalException, match='HOTJAR_SITE_ID setting is not set' + ): + 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) + expected_pattern = r"^HOTJAR_SITE_ID setting: must be \(a string containing\) a number: 'invalid'$" + with pytest.raises(AnalyticalException, match=expected_pattern): + HotjarNode() @override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1']) def test_render_internal_ip(self): @@ -64,21 +62,23 @@ class HotjarTagTestCase(TagTestCase): context = Context({'request': request}) actual_html = HotjarNode().render(context) - disabled_html = '\n'.join([ + disabled_html = '\n'.join( + [ '', - ]) - self.assertEqual(disabled_html, actual_html) + ] + ) + assert 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({ + assert template_nodes == { 'head_top': [], 'head_bottom': [HotjarNode], 'body_top': [], 'body_bottom': [], - }, template_nodes) + } diff --git a/analytical/tests/test_tag_hubspot.py b/tests/unit/test_tag_hubspot.py similarity index 62% rename from analytical/tests/test_tag_hubspot.py rename to tests/unit/test_tag_hubspot.py index ee9d2ff..5b980b5 100644 --- a/analytical/tests/test_tag_hubspot.py +++ b/tests/unit/test_tag_hubspot.py @@ -2,12 +2,13 @@ Tests for the HubSpot template tags and filters. """ +import pytest from django.http import HttpRequest from django.template import Context from django.test.utils import override_settings +from utils import TagTestCase from analytical.templatetags.hubspot import HubSpotNode -from analytical.tests.utils import TagTestCase from analytical.utils import AnalyticalException @@ -19,21 +20,27 @@ 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) + assert ( + "n.id=i;n.src='//js.hs-analytics.net/analytics/'" + "+(Math.ceil(new Date()/r)*r)+'/1234.js';" + ) in 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) + assert ( + "n.id=i;n.src='//js.hs-analytics.net/analytics/'" + "+(Math.ceil(new Date()/r)*r)+'/1234.js';" + ) in r @override_settings(HUBSPOT_PORTAL_ID=None) def test_no_portal_id(self): - self.assertRaises(AnalyticalException, HubSpotNode) + with pytest.raises(AnalyticalException): + HubSpotNode() @override_settings(HUBSPOT_PORTAL_ID='wrong') def test_wrong_portal_id(self): - self.assertRaises(AnalyticalException, HubSpotNode) + with pytest.raises(AnalyticalException): + HubSpotNode() @override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1']) def test_render_internal_ip(self): @@ -41,5 +48,5 @@ class HubSpotTagTestCase(TagTestCase): req.META['REMOTE_ADDR'] = '1.1.1.1' context = Context({'request': req}) r = HubSpotNode().render(context) - self.assertTrue(r.startswith(''), r) + assert r.startswith('') diff --git a/analytical/tests/test_tag_intercom.py b/tests/unit/test_tag_intercom.py similarity index 60% rename from analytical/tests/test_tag_intercom.py rename to tests/unit/test_tag_intercom.py index 2085fcb..8ed7ce7 100644 --- a/analytical/tests/test_tag_intercom.py +++ b/tests/unit/test_tag_intercom.py @@ -4,17 +4,18 @@ Tests for the intercom template tags and filters. import datetime +import pytest 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 utils import TagTestCase -from analytical.templatetags.intercom import IntercomNode, intercom_user_hash, _timestamp -from analytical.tests.utils import TagTestCase +from analytical.templatetags.intercom import IntercomNode, intercom_user_hash from analytical.utils import AnalyticalException -@override_settings(INTERCOM_APP_ID="abc123xyz") +@override_settings(INTERCOM_APP_ID='abc123xyz') class IntercomTagTestCase(TagTestCase): """ Tests for the ``intercom`` template tag. @@ -22,7 +23,9 @@ class IntercomTagTestCase(TagTestCase): def test_tag(self): rendered_tag = self.render_tag('intercom', 'intercom') - self.assertTrue(rendered_tag.strip().startswith(' -""" % {'user_id': user.pk}, rendered_tag) # noqa +""" + % {'user_id': user.pk} + ) # noqa @override_settings(INTERCOM_APP_ID=None) def test_no_account_number(self): - self.assertRaises(AnalyticalException, IntercomNode) + with pytest.raises(AnalyticalException): + IntercomNode() @override_settings(INTERCOM_APP_ID='123abQ') def test_wrong_account_number(self): - self.assertRaises(AnalyticalException, IntercomNode) + with pytest.raises(AnalyticalException): + IntercomNode() def test_identify_name_email_and_created_at(self): now = datetime.datetime(2014, 4, 9, 15, 15, 0) @@ -56,52 +65,61 @@ class IntercomTagTestCase(TagTestCase): username='test', first_name='Firstname', last_name='Lastname', - email="test@example.com", + email='test@example.com', date_joined=now, ) - r = IntercomNode().render(Context({ - 'user': user, - })) - self.assertTrue('window.intercomSettings = {' - '"app_id": "abc123xyz", "created_at": 1397074500, ' - '"email": "test@example.com", "name": "Firstname Lastname", ' - '"user_id": %(user_id)s' - '};' % {'user_id': user.pk} in r, msg=r) + r = IntercomNode().render(Context({'user': user})) + assert ( + 'window.intercomSettings = {"app_id": "abc123xyz", "created_at": 1397074500, ' + f'"email": "test@example.com", "name": "Firstname Lastname", "user_id": {user.pk}}};' + ) in r def test_custom(self): - r = IntercomNode().render(Context({ - 'intercom_var1': 'val1', - 'intercom_var2': 'val2' - })) - self.assertTrue('var1": "val1", "var2": "val2"' in r) + r = IntercomNode().render( + Context({'intercom_var1': 'val1', 'intercom_var2': 'val2'}) + ) + assert 'var1": "val1", "var2": "val2"' in r def test_identify_name_and_email(self): - r = IntercomNode().render(Context({ - 'user': User( - username='test', - first_name='Firstname', - last_name='Lastname', - email="test@example.com"), - })) - self.assertTrue('"email": "test@example.com", "name": "Firstname Lastname"' in r) + r = IntercomNode().render( + Context( + { + 'user': User( + username='test', + first_name='Firstname', + last_name='Lastname', + email='test@example.com', + ), + } + ) + ) + assert '"email": "test@example.com", "name": "Firstname Lastname"' in r def test_identify_username_no_email(self): r = IntercomNode().render(Context({'user': User(username='test')})) - self.assertTrue('"name": "test"' in r, r) + assert '"name": "test"' in r, r def test_no_identify_when_explicit_name(self): - r = IntercomNode().render(Context({ - 'intercom_name': 'explicit', - 'user': User(username='implicit'), - })) - self.assertTrue('"name": "explicit"' in r, r) + r = IntercomNode().render( + Context( + { + 'intercom_name': 'explicit', + 'user': User(username='implicit'), + } + ) + ) + assert '"name": "explicit"' in r, r def test_no_identify_when_explicit_email(self): - r = IntercomNode().render(Context({ - 'intercom_email': 'explicit', - 'user': User(username='implicit'), - })) - self.assertTrue('"email": "explicit"' in r, r) + r = IntercomNode().render( + Context( + { + 'intercom_email': 'explicit', + 'user': User(username='implicit'), + } + ) + ) + assert '"email": "explicit"' in r, r @override_settings(INTERCOM_HMAC_SECRET_KEY='secret') def test_user_hash__without_user_details(self): @@ -109,9 +127,7 @@ class IntercomTagTestCase(TagTestCase): No `user_hash` without `user_id` or `email`. """ attrs = IntercomNode()._get_custom_attrs(Context()) - self.assertEqual({ - 'created_at': None, - }, attrs) + assert {'created_at': None} == attrs @override_settings(INTERCOM_HMAC_SECRET_KEY='secret') def test_user_hash__with_user(self): @@ -122,45 +138,53 @@ class IntercomTagTestCase(TagTestCase): email='test@example.com', ) # type: User attrs = IntercomNode()._get_custom_attrs(Context({'user': user})) - self.assertEqual({ - 'created_at': int(_timestamp(user.date_joined)), + assert attrs == { + 'created_at': int(user.date_joined.timestamp()), 'email': 'test@example.com', 'name': '', 'user_hash': intercom_user_hash(str(user.pk)), 'user_id': user.pk, - }, attrs) + } @override_settings(INTERCOM_HMAC_SECRET_KEY='secret') def test_user_hash__with_explicit_user_id(self): """ 'user_hash' of context-provided `user_id`. """ - attrs = IntercomNode()._get_custom_attrs(Context({ - 'intercom_email': 'test@example.com', - 'intercom_user_id': '5', - })) - self.assertEqual({ + attrs = IntercomNode()._get_custom_attrs( + Context( + { + 'intercom_email': 'test@example.com', + 'intercom_user_id': '5', + } + ) + ) + assert attrs == { 'created_at': None, 'email': 'test@example.com', # HMAC for user_id: 'user_hash': 'd3123a7052b42272d9b520235008c248a5aff3221cc0c530b754702ad91ab102', 'user_id': '5', - }, attrs) + } @override_settings(INTERCOM_HMAC_SECRET_KEY='secret') def test_user_hash__with_explicit_email(self): """ 'user_hash' of context-provided `email`. """ - attrs = IntercomNode()._get_custom_attrs(Context({ - 'intercom_email': 'test@example.com', - })) - self.assertEqual({ + attrs = IntercomNode()._get_custom_attrs( + Context( + { + 'intercom_email': 'test@example.com', + } + ) + ) + assert attrs == { 'created_at': None, 'email': 'test@example.com', # HMAC for email: 'user_hash': '49e43229ee99dca2565241719b8341b04e71dd4de0628f991b5bea30a526e153', - }, attrs) + } @override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1']) def test_render_internal_ip(self): @@ -168,5 +192,5 @@ class IntercomTagTestCase(TagTestCase): req.META['REMOTE_ADDR'] = '1.1.1.1' context = Context({'request': req}) r = IntercomNode().render(context) - self.assertTrue(r.startswith(''), r) + assert r.startswith('') diff --git a/analytical/tests/test_tag_kiss_insights.py b/tests/unit/test_tag_kiss_insights.py similarity index 67% rename from analytical/tests/test_tag_kiss_insights.py rename to tests/unit/test_tag_kiss_insights.py index ffbba16..e2e96fb 100644 --- a/analytical/tests/test_tag_kiss_insights.py +++ b/tests/unit/test_tag_kiss_insights.py @@ -2,12 +2,13 @@ Tests for the KISSinsights template tags and filters. """ -from django.contrib.auth.models import User, AnonymousUser +import pytest +from django.contrib.auth.models import AnonymousUser, User from django.template import Context from django.test.utils import override_settings +from utils import TagTestCase from analytical.templatetags.kiss_insights import KissInsightsNode -from analytical.tests.utils import TagTestCase from analytical.utils import AnalyticalException @@ -19,38 +20,42 @@ class KissInsightsTagTestCase(TagTestCase): def test_tag(self): r = self.render_tag('kiss_insights', 'kiss_insights') - self.assertTrue("//s3.amazonaws.com/ki.js/12345/abc.js" in r, r) + assert '//s3.amazonaws.com/ki.js/12345/abc.js' in r def test_node(self): r = KissInsightsNode().render(Context()) - self.assertTrue("//s3.amazonaws.com/ki.js/12345/abc.js" in r, r) + assert '//s3.amazonaws.com/ki.js/12345/abc.js' in r @override_settings(KISS_INSIGHTS_ACCOUNT_NUMBER=None) def test_no_account_number(self): - self.assertRaises(AnalyticalException, KissInsightsNode) + with pytest.raises(AnalyticalException): + KissInsightsNode() @override_settings(KISS_INSIGHTS_SITE_CODE=None) def test_no_site_code(self): - self.assertRaises(AnalyticalException, KissInsightsNode) + with pytest.raises(AnalyticalException): + KissInsightsNode() @override_settings(KISS_INSIGHTS_ACCOUNT_NUMBER='abcde') def test_wrong_account_number(self): - self.assertRaises(AnalyticalException, KissInsightsNode) + with pytest.raises(AnalyticalException): + KissInsightsNode() @override_settings(KISS_INSIGHTS_SITE_CODE='abc def') def test_wrong_site_id(self): - self.assertRaises(AnalyticalException, KissInsightsNode) + with pytest.raises(AnalyticalException): + KissInsightsNode() @override_settings(ANALYTICAL_AUTO_IDENTIFY=True) def test_identify(self): r = KissInsightsNode().render(Context({'user': User(username='test')})) - self.assertTrue("_kiq.push(['identify', 'test']);" in r, r) + assert "_kiq.push(['identify', 'test']);" in r @override_settings(ANALYTICAL_AUTO_IDENTIFY=True) def test_identify_anonymous_user(self): r = KissInsightsNode().render(Context({'user': AnonymousUser()})) - self.assertFalse("_kiq.push(['identify', " in r, r) + assert "_kiq.push(['identify', " not in r def test_show_survey(self): r = KissInsightsNode().render(Context({'kiss_insights_show_survey': 1234})) - self.assertTrue("_kiq.push(['showSurvey', 1234]);" in r, r) + assert "_kiq.push(['showSurvey', 1234]);" in r diff --git a/tests/unit/test_tag_kiss_metrics.py b/tests/unit/test_tag_kiss_metrics.py new file mode 100644 index 0000000..129b435 --- /dev/null +++ b/tests/unit/test_tag_kiss_metrics.py @@ -0,0 +1,102 @@ +""" +Tests for the KISSmetrics tags and filters. +""" + +import pytest +from django.contrib.auth.models import AnonymousUser, User +from django.http import HttpRequest +from django.template import Context +from django.test.utils import override_settings +from utils import TagTestCase + +from analytical.templatetags.kiss_metrics import KissMetricsNode +from analytical.utils import AnalyticalException + + +@override_settings(KISS_METRICS_API_KEY='0123456789abcdef0123456789abcdef01234567') +class KissMetricsTagTestCase(TagTestCase): + """ + Tests for the ``kiss_metrics`` template tag. + """ + + def test_tag(self): + r = self.render_tag('kiss_metrics', 'kiss_metrics') + assert ( + '//doug1izaerwt3.cloudfront.net/0123456789abcdef0123456789abcdef01234567.1.js' + in r + ) + + def test_node(self): + r = KissMetricsNode().render(Context()) + assert ( + '//doug1izaerwt3.cloudfront.net/0123456789abcdef0123456789abcdef01234567.1.js' + in r + ) + + @override_settings(KISS_METRICS_API_KEY=None) + def test_no_api_key(self): + with pytest.raises(AnalyticalException): + KissMetricsNode() + + @override_settings(KISS_METRICS_API_KEY='0123456789abcdef0123456789abcdef0123456') + def test_api_key_too_short(self): + with pytest.raises(AnalyticalException): + KissMetricsNode() + + @override_settings(KISS_METRICS_API_KEY='0123456789abcdef0123456789abcdef012345678') + def test_api_key_too_long(self): + with pytest.raises(AnalyticalException): + KissMetricsNode() + + @override_settings(ANALYTICAL_AUTO_IDENTIFY=True) + def test_identify(self): + r = KissMetricsNode().render(Context({'user': User(username='test')})) + assert "_kmq.push(['identify', 'test']);" in r + + @override_settings(ANALYTICAL_AUTO_IDENTIFY=True) + def test_identify_anonymous_user(self): + r = KissMetricsNode().render(Context({'user': AnonymousUser()})) + assert "_kmq.push(['identify', " not in r + + def test_event(self): + r = KissMetricsNode().render( + Context( + { + 'kiss_metrics_event': ( + 'test_event', + {'prop1': 'val1', 'prop2': 'val2'}, + ), + } + ) + ) + assert "_kmq.push(['record', 'test_event', " + '{"prop1": "val1", "prop2": "val2"}]);' in r + + def test_property(self): + r = KissMetricsNode().render( + Context( + { + 'kiss_metrics_properties': {'prop1': 'val1', 'prop2': 'val2'}, + } + ) + ) + assert '_kmq.push([\'set\', {"prop1": "val1", "prop2": "val2"}]);' in r + + def test_alias(self): + r = KissMetricsNode().render( + Context( + { + 'kiss_metrics_alias': {'test': 'test_alias'}, + } + ) + ) + assert "_kmq.push(['alias', 'test', 'test_alias']);" in r + + @override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1']) + def test_render_internal_ip(self): + req = HttpRequest() + req.META['REMOTE_ADDR'] = '1.1.1.1' + context = Context({'request': req}) + r = KissMetricsNode().render(context) + assert r.startswith('') diff --git a/tests/unit/test_tag_luckyorange.py b/tests/unit/test_tag_luckyorange.py new file mode 100644 index 0000000..20b56f6 --- /dev/null +++ b/tests/unit/test_tag_luckyorange.py @@ -0,0 +1,86 @@ +""" +Tests for the Lucky Orange template tags. +""" + +import pytest +from django.http import HttpRequest +from django.template import Context, Template, TemplateSyntaxError +from django.test import override_settings +from utils import TagTestCase + +from analytical.templatetags.analytical import _load_template_nodes +from analytical.templatetags.luckyorange import LuckyOrangeNode +from analytical.utils import AnalyticalException + +expected_html = """\ + +""" + + +@override_settings(LUCKYORANGE_SITE_ID='123456') +class LuckyOrangeTagTestCase(TagTestCase): + maxDiff = None + + def test_tag(self): + html = self.render_tag('luckyorange', 'luckyorange') + assert expected_html == html + + def test_node(self): + html = LuckyOrangeNode().render(Context({})) + assert expected_html == html + + def test_tags_take_no_args(self): + with pytest.raises( + TemplateSyntaxError, match="'luckyorange' takes no arguments" + ): + Template('{% load luckyorange %}{% luckyorange "arg" %}').render( + Context({}) + ) + + @override_settings(LUCKYORANGE_SITE_ID=None) + def test_no_id(self): + with pytest.raises( + AnalyticalException, match='LUCKYORANGE_SITE_ID setting is not set' + ): + LuckyOrangeNode() + + @override_settings(LUCKYORANGE_SITE_ID='invalid') + def test_invalid_id(self): + expected_pattern = r"^LUCKYORANGE_SITE_ID setting: must be \(a string containing\) a number: 'invalid'$" + with pytest.raises(AnalyticalException, match=expected_pattern): + LuckyOrangeNode() + + @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 = LuckyOrangeNode().render(context) + disabled_html = '\n'.join( + [ + '', + ] + ) + assert disabled_html == actual_html + + def test_contribute_to_analytical(self): + """ + `luckyorange.contribute_to_analytical` registers the head and body nodes. + """ + template_nodes = _load_template_nodes() + assert template_nodes == { + 'head_top': [], + 'head_bottom': [LuckyOrangeNode], + 'body_top': [], + 'body_bottom': [], + } diff --git a/tests/unit/test_tag_matomo.py b/tests/unit/test_tag_matomo.py new file mode 100644 index 0000000..0765c12 --- /dev/null +++ b/tests/unit/test_tag_matomo.py @@ -0,0 +1,161 @@ +""" +Tests for the Matomo template tags and filters. +""" + +import pytest +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 utils import TagTestCase + +from analytical.templatetags.matomo import MatomoNode +from analytical.utils import AnalyticalException + + +@override_settings(MATOMO_DOMAIN_PATH='example.com', MATOMO_SITE_ID='345') +class MatomoTagTestCase(TagTestCase): + """ + Tests for the ``matomo`` template tag. + """ + + def test_tag(self): + r = self.render_tag('matomo', 'matomo') + assert '"//example.com/"' in r + assert "_paq.push(['setSiteId', 345]);" in r + assert 'img src="//example.com/matomo.php?idsite=345"' in r + + def test_node(self): + r = MatomoNode().render(Context({})) + assert '"//example.com/";' in r + assert "_paq.push(['setSiteId', 345]);" in r + assert 'img src="//example.com/matomo.php?idsite=345"' in r + + @override_settings(MATOMO_DOMAIN_PATH='example.com/matomo', MATOMO_SITE_ID='345') + def test_domain_path_valid(self): + r = self.render_tag('matomo', 'matomo') + assert '"//example.com/matomo/"' in r + + @override_settings(MATOMO_DOMAIN_PATH='example.com:1234', MATOMO_SITE_ID='345') + def test_domain_port_valid(self): + r = self.render_tag('matomo', 'matomo') + assert '"//example.com:1234/";' in r + + @override_settings( + MATOMO_DOMAIN_PATH='example.com:1234/matomo', MATOMO_SITE_ID='345' + ) + def test_domain_port_path_valid(self): + r = self.render_tag('matomo', 'matomo') + assert '"//example.com:1234/matomo/"' in r + + @override_settings(MATOMO_DOMAIN_PATH=None) + def test_no_domain(self): + with pytest.raises(AnalyticalException): + MatomoNode() + + @override_settings(MATOMO_SITE_ID=None) + def test_no_siteid(self): + with pytest.raises(AnalyticalException): + MatomoNode() + + @override_settings(MATOMO_SITE_ID='x') + def test_siteid_not_a_number(self): + with pytest.raises(AnalyticalException): + MatomoNode() + + @override_settings(MATOMO_DOMAIN_PATH='http://www.example.com') + def test_domain_protocol_invalid(self): + with pytest.raises(AnalyticalException): + MatomoNode() + + @override_settings(MATOMO_DOMAIN_PATH='example.com/') + def test_domain_slash_invalid(self): + with pytest.raises(AnalyticalException): + MatomoNode() + + @override_settings(MATOMO_DOMAIN_PATH='example.com:123:456') + def test_domain_multi_port(self): + with pytest.raises(AnalyticalException): + MatomoNode() + + @override_settings(MATOMO_DOMAIN_PATH='example.com:') + def test_domain_incomplete_port(self): + with pytest.raises(AnalyticalException): + MatomoNode() + + @override_settings(MATOMO_DOMAIN_PATH='example.com:/matomo') + def test_domain_uri_incomplete_port(self): + with pytest.raises(AnalyticalException): + MatomoNode() + + @override_settings(MATOMO_DOMAIN_PATH='example.com:12df') + def test_domain_port_invalid(self): + with pytest.raises(AnalyticalException): + MatomoNode() + + @override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1']) + def test_render_internal_ip(self): + req = HttpRequest() + req.META['REMOTE_ADDR'] = '1.1.1.1' + context = Context({'request': req}) + r = MatomoNode().render(context) + assert r.startswith('') + + def test_uservars(self): + context = Context( + { + 'matomo_vars': [ + (1, 'foo', 'foo_val'), + (2, 'bar', 'bar_val', 'page'), + (3, 'spam', 'spam_val', 'visit'), + ] + } + ) + r = MatomoNode().render(context) + for var_code in [ + '_paq.push(["setCustomVariable", 1, "foo", "foo_val", "page"]);', + '_paq.push(["setCustomVariable", 2, "bar", "bar_val", "page"]);', + '_paq.push(["setCustomVariable", 3, "spam", "spam_val", "visit"]);', + ]: + assert var_code in r + + @override_settings(ANALYTICAL_AUTO_IDENTIFY=True) + def test_default_usertrack(self): + context = Context( + {'user': User(username='BDFL', first_name='Guido', last_name='van Rossum')} + ) + r = MatomoNode().render(context) + var_code = '_paq.push(["setUserId", "BDFL"]);' + assert var_code in r + + def test_matomo_usertrack(self): + context = Context({'matomo_identity': 'BDFL'}) + r = MatomoNode().render(context) + var_code = '_paq.push(["setUserId", "BDFL"]);' + assert var_code in r + + def test_analytical_usertrack(self): + context = Context({'analytical_identity': 'BDFL'}) + r = MatomoNode().render(context) + var_code = '_paq.push(["setUserId", "BDFL"]);' + assert var_code in r + + @override_settings(ANALYTICAL_AUTO_IDENTIFY=True) + def test_disable_usertrack(self): + context = Context( + { + 'user': User( + username='BDFL', first_name='Guido', last_name='van Rossum' + ), + 'matomo_identity': None, + } + ) + r = MatomoNode().render(context) + var_code = '_paq.push(["setUserId", "BDFL"]);' + assert var_code not in r + + @override_settings(MATOMO_DISABLE_COOKIES=True) + def test_disable_cookies(self): + r = MatomoNode().render(Context({})) + assert "_paq.push(['disableCookies']);" in r diff --git a/analytical/tests/test_tag_mixpanel.py b/tests/unit/test_tag_mixpanel.py similarity index 58% rename from analytical/tests/test_tag_mixpanel.py rename to tests/unit/test_tag_mixpanel.py index 033c2c7..8b2cb04 100644 --- a/analytical/tests/test_tag_mixpanel.py +++ b/tests/unit/test_tag_mixpanel.py @@ -2,13 +2,14 @@ Tests for the Mixpanel tags and filters. """ -from django.contrib.auth.models import User, AnonymousUser +import pytest +from django.contrib.auth.models import AnonymousUser, User from django.http import HttpRequest from django.template import Context from django.test.utils import override_settings +from utils import TagTestCase from analytical.templatetags.mixpanel import MixpanelNode -from analytical.tests.utils import TagTestCase from analytical.utils import AnalyticalException @@ -20,40 +21,50 @@ class MixpanelTagTestCase(TagTestCase): def test_tag(self): r = self.render_tag('mixpanel', 'mixpanel') - self.assertIn("mixpanel.init('0123456789abcdef0123456789abcdef');", r) + assert "mixpanel.init('0123456789abcdef0123456789abcdef');" in r def test_node(self): r = MixpanelNode().render(Context()) - self.assertIn("mixpanel.init('0123456789abcdef0123456789abcdef');", r) + assert "mixpanel.init('0123456789abcdef0123456789abcdef');" in r @override_settings(MIXPANEL_API_TOKEN=None) def test_no_token(self): - self.assertRaises(AnalyticalException, MixpanelNode) + with pytest.raises(AnalyticalException): + MixpanelNode() @override_settings(MIXPANEL_API_TOKEN='0123456789abcdef0123456789abcdef0') def test_token_too_long(self): - self.assertRaises(AnalyticalException, MixpanelNode) + with pytest.raises(AnalyticalException): + MixpanelNode() @override_settings(MIXPANEL_API_TOKEN='0123456789abcdef0123456789abcde') def test_token_too_short(self): - self.assertRaises(AnalyticalException, MixpanelNode) + with pytest.raises(AnalyticalException): + MixpanelNode() @override_settings(ANALYTICAL_AUTO_IDENTIFY=True) def test_identify(self): r = MixpanelNode().render(Context({'user': User(username='test')})) - self.assertIn("mixpanel.identify('test');", r) + assert "mixpanel.identify('test');" in r @override_settings(ANALYTICAL_AUTO_IDENTIFY=True) def test_identify_anonymous_user(self): r = MixpanelNode().render(Context({'user': AnonymousUser()})) - self.assertFalse("mixpanel.register_once({distinct_id:" in r, r) + assert 'mixpanel.register_once({distinct_id:' not in r def test_event(self): - 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) + r = MixpanelNode().render( + Context( + { + 'mixpanel_event': ( + 'test_event', + {'prop1': 'val1', 'prop2': 'val2'}, + ), + } + ) + ) + assert "mixpanel.track('test_event', " + '{"prop1": "val1", "prop2": "val2"});' in r @override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1']) def test_render_internal_ip(self): @@ -61,6 +72,5 @@ class MixpanelTagTestCase(TagTestCase): req.META['REMOTE_ADDR'] = '1.1.1.1' context = Context({'request': req}) r = MixpanelNode().render(context) - self.assertTrue(r.startswith( - ''), r) + assert r.startswith('') diff --git a/tests/unit/test_tag_olark.py b/tests/unit/test_tag_olark.py new file mode 100644 index 0000000..34ba68b --- /dev/null +++ b/tests/unit/test_tag_olark.py @@ -0,0 +1,105 @@ +""" +Tests for the Olark template tags and filters. +""" + +import pytest +from django.contrib.auth.models import AnonymousUser, User +from django.template import Context +from django.test.utils import override_settings +from utils import TagTestCase + +from analytical.templatetags.olark import OlarkNode +from analytical.utils import AnalyticalException + + +@override_settings(OLARK_SITE_ID='1234-567-89-0123') +class OlarkTestCase(TagTestCase): + """ + Tests for the ``olark`` template tag. + """ + + def test_tag(self): + r = self.render_tag('olark', 'olark') + assert "olark.identify('1234-567-89-0123');" in r + + def test_node(self): + r = OlarkNode().render(Context()) + assert "olark.identify('1234-567-89-0123');" in r + + @override_settings(OLARK_SITE_ID=None) + def test_no_site_id(self): + with pytest.raises(AnalyticalException): + OlarkNode() + + @override_settings(OLARK_SITE_ID='1234-567-8901234') + def test_wrong_site_id(self): + with pytest.raises(AnalyticalException): + OlarkNode() + + @override_settings(ANALYTICAL_AUTO_IDENTIFY=True) + def test_identify(self): + r = OlarkNode().render( + Context( + { + 'user': User(username='test', first_name='Test', last_name='User'), + } + ) + ) + assert ( + "olark('api.chat.updateVisitorNickname', {snippet: 'Test User (test)'});" + in r + ) + + @override_settings(ANALYTICAL_AUTO_IDENTIFY=True) + def test_identify_anonymous_user(self): + r = OlarkNode().render(Context({'user': AnonymousUser()})) + assert "olark('api.chat.updateVisitorNickname', " not in r + + def test_nickname(self): + r = OlarkNode().render(Context({'olark_nickname': 'testnick'})) + assert "olark('api.chat.updateVisitorNickname', {snippet: 'testnick'});" in r + + def test_status_string(self): + r = OlarkNode().render(Context({'olark_status': 'teststatus'})) + assert "olark('api.chat.updateVisitorStatus', " + '{snippet: "teststatus"});' in r + + def test_status_string_list(self): + r = OlarkNode().render( + Context( + { + 'olark_status': ['teststatus1', 'teststatus2'], + } + ) + ) + assert "olark('api.chat.updateVisitorStatus', " + '{snippet: ["teststatus1", "teststatus2"]});' in r + + def test_messages(self): + messages = [ + 'welcome_title', + 'chatting_title', + 'unavailable_title', + 'busy_title', + 'away_message', + 'loading_title', + 'welcome_message', + 'busy_message', + 'chat_input_text', + 'name_input_text', + 'email_input_text', + 'offline_note_message', + 'send_button_text', + 'offline_note_thankyou_text', + 'offline_note_error_text', + 'offline_note_sending_text', + 'operator_is_typing_text', + 'operator_has_stopped_typing_text', + 'introduction_error_text', + 'introduction_messages', + 'introduction_submit_button_text', + ] + vars = {f'olark_{m}': m for m in messages} + r = OlarkNode().render(Context(vars)) + for m in messages: + assert f'olark.configure(\'locale.{m}\', "{m}");' in r diff --git a/analytical/tests/test_tag_optimizely.py b/tests/unit/test_tag_optimizely.py similarity index 59% rename from analytical/tests/test_tag_optimizely.py rename to tests/unit/test_tag_optimizely.py index a1cce26..523c5c0 100644 --- a/analytical/tests/test_tag_optimizely.py +++ b/tests/unit/test_tag_optimizely.py @@ -2,12 +2,13 @@ Tests for the Optimizely template tags and filters. """ +import pytest from django.http import HttpRequest from django.template import Context from django.test.utils import override_settings +from utils import TagTestCase from analytical.templatetags.optimizely import OptimizelyNode -from analytical.tests.utils import TagTestCase from analytical.utils import AnalyticalException @@ -18,22 +19,22 @@ class OptimizelyTagTestCase(TagTestCase): """ def test_tag(self): - self.assertEqual( - '', - self.render_tag('optimizely', 'optimizely')) + expected = '' + assert self.render_tag('optimizely', 'optimizely') == expected def test_node(self): - self.assertEqual( - '', - OptimizelyNode().render(Context())) + expected = '' + assert OptimizelyNode().render(Context()) == expected @override_settings(OPTIMIZELY_ACCOUNT_NUMBER=None) def test_no_account_number(self): - self.assertRaises(AnalyticalException, OptimizelyNode) + with pytest.raises(AnalyticalException): + OptimizelyNode() @override_settings(OPTIMIZELY_ACCOUNT_NUMBER='123abc') def test_wrong_account_number(self): - self.assertRaises(AnalyticalException, OptimizelyNode) + with pytest.raises(AnalyticalException): + OptimizelyNode() @override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1']) def test_render_internal_ip(self): @@ -41,6 +42,5 @@ class OptimizelyTagTestCase(TagTestCase): req.META['REMOTE_ADDR'] = '1.1.1.1' context = Context({'request': req}) r = OptimizelyNode().render(context) - self.assertTrue(r.startswith( - ''), r) + assert r.startswith('') diff --git a/analytical/tests/test_tag_performable.py b/tests/unit/test_tag_performable.py similarity index 64% rename from analytical/tests/test_tag_performable.py rename to tests/unit/test_tag_performable.py index 2b1e16d..a02b401 100644 --- a/analytical/tests/test_tag_performable.py +++ b/tests/unit/test_tag_performable.py @@ -2,13 +2,14 @@ Tests for the Performable template tags and filters. """ -from django.contrib.auth.models import User, AnonymousUser +import pytest +from django.contrib.auth.models import AnonymousUser, User from django.http import HttpRequest from django.template import Context from django.test.utils import override_settings +from utils import TagTestCase from analytical.templatetags.performable import PerformableNode -from analytical.tests.utils import TagTestCase from analytical.utils import AnalyticalException @@ -20,19 +21,21 @@ class PerformableTagTestCase(TagTestCase): def test_tag(self): r = self.render_tag('performable', 'performable') - self.assertTrue('/performable/pax/123ABC.js' in r, r) + assert '/performable/pax/123ABC.js' in r def test_node(self): r = PerformableNode().render(Context()) - self.assertTrue('/performable/pax/123ABC.js' in r, r) + assert '/performable/pax/123ABC.js' in r @override_settings(PERFORMABLE_API_KEY=None) def test_no_api_key(self): - self.assertRaises(AnalyticalException, PerformableNode) + with pytest.raises(AnalyticalException): + PerformableNode() @override_settings(PERFORMABLE_API_KEY='123 ABC') def test_wrong_account_number(self): - self.assertRaises(AnalyticalException, PerformableNode) + with pytest.raises(AnalyticalException): + PerformableNode() @override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1']) def test_render_internal_ip(self): @@ -40,19 +43,18 @@ class PerformableTagTestCase(TagTestCase): req.META['REMOTE_ADDR'] = '1.1.1.1' context = Context({'request': req}) r = PerformableNode().render(context) - self.assertTrue(r.startswith( - ''), r) + assert r.startswith('') @override_settings(ANALYTICAL_AUTO_IDENTIFY=True) def test_identify(self): r = PerformableNode().render(Context({'user': User(username='test')})) - self.assertTrue('_paq.push(["identify", {identity: "test"}]);' in r, r) + assert '_paq.push(["identify", {identity: "test"}]);' in r @override_settings(ANALYTICAL_AUTO_IDENTIFY=True) def test_identify_anonymous_user(self): r = PerformableNode().render(Context({'user': AnonymousUser()})) - self.assertFalse('_paq.push(["identify", ' in r, r) + assert '_paq.push(["identify", ' not in r class PerformableEmbedTagTestCase(TagTestCase): @@ -63,9 +65,5 @@ class PerformableEmbedTagTestCase(TagTestCase): def test_tag(self): domain = 'example.com' page = 'test' - tag = self.render_tag( - 'performable', 'performable_embed "%s" "%s"' % (domain, page) - ) - self.assertIn( - "$f.initialize({'host': 'example.com', 'page': 'test'});", tag - ) + tag = self.render_tag('performable', f'performable_embed "{domain}" "{page}"') + assert "$f.initialize({'host': 'example.com', 'page': 'test'});" in tag diff --git a/analytical/tests/test_tag_rating_mailru.py b/tests/unit/test_tag_rating_mailru.py similarity index 70% rename from analytical/tests/test_tag_rating_mailru.py rename to tests/unit/test_tag_rating_mailru.py index 45d0309..4493b72 100644 --- a/analytical/tests/test_tag_rating_mailru.py +++ b/tests/unit/test_tag_rating_mailru.py @@ -2,12 +2,13 @@ Tests for the Rating@Mail.ru template tags and filters. """ +import pytest from django.http import HttpRequest from django.template import Context from django.test.utils import override_settings +from utils import TagTestCase from analytical.templatetags.rating_mailru import RatingMailruNode -from analytical.tests.utils import TagTestCase from analytical.utils import AnalyticalException @@ -19,19 +20,21 @@ class RatingMailruTagTestCase(TagTestCase): def test_tag(self): r = self.render_tag('rating_mailru', 'rating_mailru') - self.assertTrue("counter?id=1234567;js=na" in r, r) + assert 'counter?id=1234567;js=na' in r def test_node(self): r = RatingMailruNode().render(Context({})) - self.assertTrue("counter?id=1234567;js=na" in r, r) + assert 'counter?id=1234567;js=na' in r @override_settings(RATING_MAILRU_COUNTER_ID=None) def test_no_site_id(self): - self.assertRaises(AnalyticalException, RatingMailruNode) + with pytest.raises(AnalyticalException): + RatingMailruNode() @override_settings(RATING_MAILRU_COUNTER_ID='1234abc') def test_wrong_site_id(self): - self.assertRaises(AnalyticalException, RatingMailruNode) + with pytest.raises(AnalyticalException): + RatingMailruNode() @override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1']) def test_render_internal_ip(self): @@ -39,6 +42,5 @@ class RatingMailruTagTestCase(TagTestCase): req.META['REMOTE_ADDR'] = '1.1.1.1' context = Context({'request': req}) r = RatingMailruNode().render(context) - self.assertTrue(r.startswith( - ''), r) + assert r.startswith('') diff --git a/tests/unit/test_tag_snapengage.py b/tests/unit/test_tag_snapengage.py new file mode 100644 index 0000000..51ac8ef --- /dev/null +++ b/tests/unit/test_tag_snapengage.py @@ -0,0 +1,355 @@ +""" +Tests for the SnapEngage template tags and filters. +""" + +import pytest +from django.contrib.auth.models import AnonymousUser, User +from django.template import Context +from django.test.utils import override_settings +from django.utils import translation +from utils import TagTestCase + +from analytical.templatetags.snapengage import ( + BUTTON_LOCATION_BOTTOM, + BUTTON_LOCATION_LEFT, + BUTTON_LOCATION_RIGHT, + BUTTON_LOCATION_TOP, + BUTTON_STYLE_DEFAULT, + BUTTON_STYLE_LIVE, + BUTTON_STYLE_NONE, + FORM_POSITION_TOP_LEFT, + SnapEngageNode, +) +from analytical.utils import AnalyticalException + +WIDGET_ID = 'ec329c69-0bf0-4db8-9b77-3f8150fb977e' + + +@override_settings( + SNAPENGAGE_WIDGET_ID=WIDGET_ID, + SNAPENGAGE_BUTTON=BUTTON_STYLE_DEFAULT, + SNAPENGAGE_BUTTON_LOCATION=BUTTON_LOCATION_LEFT, + SNAPENGAGE_BUTTON_OFFSET='55%', +) +class SnapEngageTestCase(TagTestCase): + """ + Tests for the ``snapengage`` template tag. + """ + + def test_tag(self): + r = self.render_tag('snapengage', 'snapengage') + assert ( + 'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","0","55%");' in r + ) + + def test_node(self): + r = SnapEngageNode().render(Context()) + assert ( + 'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","0","55%");' in r + ) + + @override_settings(SNAPENGAGE_WIDGET_ID=None) + def test_no_site_id(self): + with pytest.raises(AnalyticalException): + SnapEngageNode() + + @override_settings(SNAPENGAGE_WIDGET_ID='abc') + def test_wrong_site_id(self): + with pytest.raises(AnalyticalException): + SnapEngageNode() + + def test_no_button(self): + r = SnapEngageNode().render( + Context( + { + 'snapengage_button': BUTTON_STYLE_NONE, + } + ) + ) + assert 'SnapABug.init("ec329c69-0bf0-4db8-9b77-3f8150fb977e")' in r + with override_settings(SNAPENGAGE_BUTTON=BUTTON_STYLE_NONE): + r = SnapEngageNode().render(Context()) + assert 'SnapABug.init("ec329c69-0bf0-4db8-9b77-3f8150fb977e")' in r + + def test_live_button(self): + r = SnapEngageNode().render( + Context( + { + 'snapengage_button': BUTTON_STYLE_LIVE, + } + ) + ) + assert ( + 'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","0","55%",true);' + in r + ) + with override_settings(SNAPENGAGE_BUTTON=BUTTON_STYLE_LIVE): + r = SnapEngageNode().render(Context()) + assert ( + 'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","0","55%",true);' + in r + ) + + def test_custom_button(self): + r = SnapEngageNode().render( + Context( + { + 'snapengage_button': 'http://www.example.com/button.png', + } + ) + ) + assert ( + 'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","0","55%");' in r + ) + assert 'SnapABug.setButton("http://www.example.com/button.png");' in r + with override_settings(SNAPENGAGE_BUTTON='http://www.example.com/button.png'): + r = SnapEngageNode().render(Context()) + assert ( + 'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","0","55%");' + in r + ) + assert 'SnapABug.setButton("http://www.example.com/button.png");' in r + + def test_button_location_right(self): + r = SnapEngageNode().render( + Context( + { + 'snapengage_button_location': BUTTON_LOCATION_RIGHT, + } + ) + ) + assert ( + 'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","1","55%");' in r + ) + with override_settings(SNAPENGAGE_BUTTON_LOCATION=BUTTON_LOCATION_RIGHT): + r = SnapEngageNode().render(Context()) + assert ( + 'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","1","55%");' + in r + ) + + def test_button_location_top(self): + r = SnapEngageNode().render( + Context( + { + 'snapengage_button_location': BUTTON_LOCATION_TOP, + } + ) + ) + assert ( + 'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","2","55%");' in r + ) + with override_settings(SNAPENGAGE_BUTTON_LOCATION=BUTTON_LOCATION_TOP): + r = SnapEngageNode().render(Context()) + assert ( + 'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","2","55%");' + in r + ) + + def test_button_location_bottom(self): + r = SnapEngageNode().render( + Context( + { + 'snapengage_button_location': BUTTON_LOCATION_BOTTOM, + } + ) + ) + assert ( + 'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","3","55%");' in r + ) + with override_settings(SNAPENGAGE_BUTTON_LOCATION=BUTTON_LOCATION_BOTTOM): + r = SnapEngageNode().render(Context()) + assert ( + 'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","3","55%");' + in r + ) + + def test_button_offset(self): + r = SnapEngageNode().render( + Context( + { + 'snapengage_button_location_offset': '30%', + } + ) + ) + assert ( + 'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","0","30%");' in r + ) + with override_settings(SNAPENGAGE_BUTTON_LOCATION_OFFSET='30%'): + r = SnapEngageNode().render(Context()) + assert ( + 'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","0","30%");' + in r + ) + + def test_button_effect(self): + r = SnapEngageNode().render( + Context( + { + 'snapengage_button_effect': '-4px', + } + ) + ) + assert 'SnapABug.setButtonEffect("-4px");' in r + with override_settings(SNAPENGAGE_BUTTON_EFFECT='-4px'): + r = SnapEngageNode().render(Context()) + assert 'SnapABug.setButtonEffect("-4px");' in r + + def test_form_position(self): + r = SnapEngageNode().render( + Context( + { + 'snapengage_form_position': FORM_POSITION_TOP_LEFT, + } + ) + ) + assert 'SnapABug.setChatFormPosition("tl");' in r + with override_settings(SNAPENGAGE_FORM_POSITION=FORM_POSITION_TOP_LEFT): + r = SnapEngageNode().render(Context()) + assert 'SnapABug.setChatFormPosition("tl");' in r + + def test_form_top_position(self): + r = SnapEngageNode().render( + Context( + { + 'snapengage_form_top_position': 40, + } + ) + ) + assert 'SnapABug.setFormTopPosition(40);' in r + with override_settings(SNAPENGAGE_FORM_TOP_POSITION=40): + r = SnapEngageNode().render(Context()) + assert 'SnapABug.setFormTopPosition(40);' in r + + def test_domain(self): + r = SnapEngageNode().render(Context({'snapengage_domain': 'example.com'})) + assert 'SnapABug.setDomain("example.com");' in r + with override_settings(SNAPENGAGE_DOMAIN='example.com'): + r = SnapEngageNode().render(Context()) + assert 'SnapABug.setDomain("example.com");' in r + + def test_secure_connection(self): + r = SnapEngageNode().render(Context({'snapengage_secure_connection': True})) + assert 'SnapABug.setSecureConnexion();' in r + with override_settings(SNAPENGAGE_SECURE_CONNECTION=True): + r = SnapEngageNode().render(Context()) + assert 'SnapABug.setSecureConnexion();' in r + + def test_show_offline(self): + r = SnapEngageNode().render( + Context( + { + 'snapengage_show_offline': False, + } + ) + ) + assert 'SnapABug.allowOffline(false);' in r + with override_settings(SNAPENGAGE_SHOW_OFFLINE=False): + r = SnapEngageNode().render(Context()) + assert 'SnapABug.allowOffline(false);' in r + + def test_proactive_chat(self): + r = SnapEngageNode().render(Context({'snapengage_proactive_chat': False})) + assert 'SnapABug.allowProactiveChat(false);' in r + + def test_screenshot(self): + r = SnapEngageNode().render( + Context( + { + 'snapengage_screenshots': False, + } + ) + ) + assert 'SnapABug.allowScreenshot(false);' in r + with override_settings(SNAPENGAGE_SCREENSHOTS=False): + r = SnapEngageNode().render(Context()) + assert 'SnapABug.allowScreenshot(false);' in r + + def test_offline_screenshots(self): + r = SnapEngageNode().render( + Context( + { + 'snapengage_offline_screenshots': False, + } + ) + ) + assert 'SnapABug.showScreenshotOption(false);' in r + with override_settings(SNAPENGAGE_OFFLINE_SCREENSHOTS=False): + r = SnapEngageNode().render(Context()) + assert 'SnapABug.showScreenshotOption(false);' in r + + def test_sounds(self): + r = SnapEngageNode().render(Context({'snapengage_sounds': False})) + assert 'SnapABug.allowChatSound(false);' in r + with override_settings(SNAPENGAGE_SOUNDS=False): + r = SnapEngageNode().render(Context()) + assert 'SnapABug.allowChatSound(false);' in r + + @override_settings(SNAPENGAGE_READONLY_EMAIL=False) + def test_email(self): + r = SnapEngageNode().render( + Context( + { + 'snapengage_email': 'test@example.com', + } + ) + ) + assert 'SnapABug.setUserEmail("test@example.com");' in r + + def test_email_readonly(self): + r = SnapEngageNode().render( + Context( + { + 'snapengage_email': 'test@example.com', + 'snapengage_readonly_email': True, + } + ) + ) + assert 'SnapABug.setUserEmail("test@example.com",true);' in r + with override_settings(SNAPENGAGE_READONLY_EMAIL=True): + r = SnapEngageNode().render( + Context( + { + 'snapengage_email': 'test@example.com', + } + ) + ) + assert 'SnapABug.setUserEmail("test@example.com",true);' in r + + @override_settings(ANALYTICAL_AUTO_IDENTIFY=True) + def test_identify(self): + r = SnapEngageNode().render( + Context( + { + 'user': User(username='test', email='test@example.com'), + } + ) + ) + assert 'SnapABug.setUserEmail("test@example.com");' in r + + @override_settings(ANALYTICAL_AUTO_IDENTIFY=True) + def test_identify_anonymous_user(self): + r = SnapEngageNode().render( + Context( + { + 'user': AnonymousUser(), + } + ) + ) + assert 'SnapABug.setUserEmail(' not in r + + def test_language(self): + r = SnapEngageNode().render(Context({'snapengage_locale': 'fr'})) + assert 'SnapABug.setLocale("fr");' in r + with override_settings(SNAPENGAGE_LOCALE='fr'): + r = SnapEngageNode().render(Context()) + assert 'SnapABug.setLocale("fr");' in r + + def test_automatic_language(self): + real_get_language = translation.get_language + try: + translation.get_language = lambda: 'fr-ca' + r = SnapEngageNode().render(Context()) + assert 'SnapABug.setLocale("fr_CA");' in r + finally: + translation.get_language = real_get_language diff --git a/analytical/tests/test_tag_spring_metrics.py b/tests/unit/test_tag_spring_metrics.py similarity index 53% rename from analytical/tests/test_tag_spring_metrics.py rename to tests/unit/test_tag_spring_metrics.py index 14aeb3c..c145216 100644 --- a/analytical/tests/test_tag_spring_metrics.py +++ b/tests/unit/test_tag_spring_metrics.py @@ -2,13 +2,14 @@ Tests for the Spring Metrics template tags and filters. """ -from django.contrib.auth.models import User, AnonymousUser +import pytest +from django.contrib.auth.models import AnonymousUser, User from django.http import HttpRequest from django.template import Context from django.test.utils import override_settings +from utils import TagTestCase from analytical.templatetags.spring_metrics import SpringMetricsNode -from analytical.tests.utils import TagTestCase from analytical.utils import AnalyticalException @@ -20,39 +21,49 @@ class SpringMetricsTagTestCase(TagTestCase): def test_tag(self): r = self.render_tag('spring_metrics', 'spring_metrics') - self.assertTrue("_springMetq.push(['id', '12345678']);" in r, r) + assert "_springMetq.push(['id', '12345678']);" in r def test_node(self): r = SpringMetricsNode().render(Context({})) - self.assertTrue("_springMetq.push(['id', '12345678']);" in r, r) + assert "_springMetq.push(['id', '12345678']);" in r @override_settings(SPRING_METRICS_TRACKING_ID=None) def test_no_site_id(self): - self.assertRaises(AnalyticalException, SpringMetricsNode) + with pytest.raises(AnalyticalException): + SpringMetricsNode() @override_settings(SPRING_METRICS_TRACKING_ID='123xyz') def test_wrong_site_id(self): - self.assertRaises(AnalyticalException, SpringMetricsNode) + with pytest.raises(AnalyticalException): + SpringMetricsNode() @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'), + } + ) + ) + assert "_springMetq.push(['setdata', {'email': 'test@test.com'}]);" in r @override_settings(ANALYTICAL_AUTO_IDENTIFY=True) def test_identify_anonymous_user(self): r = SpringMetricsNode().render(Context({'user': AnonymousUser()})) - self.assertFalse("_springMetq.push(['setdata', {'email':" in r, r) + assert "_springMetq.push(['setdata', {'email':" not in 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', + } + ) + ) + assert "_springMetq.push(['setdata', {'var1': 'val1'}]);" in r + assert "_springMetq.push(['setdata', {'var2': 'val2'}]);" in r @override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1']) def test_render_internal_ip(self): @@ -60,6 +71,5 @@ class SpringMetricsTagTestCase(TagTestCase): req.META['REMOTE_ADDR'] = '1.1.1.1' context = Context({'request': req}) r = SpringMetricsNode().render(context) - self.assertTrue(r.startswith( - ''), r) + assert r.startswith('') diff --git a/analytical/tests/test_tag_uservoice.py b/tests/unit/test_tag_uservoice.py similarity index 62% rename from analytical/tests/test_tag_uservoice.py rename to tests/unit/test_tag_uservoice.py index 9bce78d..5fa91ee 100644 --- a/analytical/tests/test_tag_uservoice.py +++ b/tests/unit/test_tag_uservoice.py @@ -2,11 +2,12 @@ Tests for the UserVoice tags and filters. """ +import pytest from django.template import Context from django.test.utils import override_settings +from utils import TagTestCase from analytical.templatetags.uservoice import UserVoiceNode -from analytical.tests.utils import TagTestCase from analytical.utils import AnalyticalException @@ -16,58 +17,55 @@ class UserVoiceTagTestCase(TagTestCase): Tests for the ``uservoice`` template tag. """ - def assertIn(self, element, container): - try: - super(TagTestCase, self).assertIn(element, container) - except AttributeError: - self.assertTrue(element in container) - def test_node(self): r = UserVoiceNode().render(Context()) - self.assertIn("widget.uservoice.com/abcdefghijklmnopqrst.js", r) + assert 'widget.uservoice.com/abcdefghijklmnopqrst.js' in r def test_tag(self): r = self.render_tag('uservoice', 'uservoice') - self.assertIn("widget.uservoice.com/abcdefghijklmnopqrst.js", r) + assert 'widget.uservoice.com/abcdefghijklmnopqrst.js' in r @override_settings(USERVOICE_WIDGET_KEY=None) def test_no_key(self): - self.assertRaises(AnalyticalException, UserVoiceNode) + with pytest.raises(AnalyticalException): + UserVoiceNode() @override_settings(USERVOICE_WIDGET_KEY='abcdefgh ijklmnopqrst') def test_invalid_key(self): - self.assertRaises(AnalyticalException, UserVoiceNode) + with pytest.raises(AnalyticalException): + UserVoiceNode() @override_settings(USERVOICE_WIDGET_KEY='') def test_empty_key(self): - self.assertRaises(AnalyticalException, UserVoiceNode) + with pytest.raises(AnalyticalException): + UserVoiceNode() def test_overridden_key(self): vars = {'uservoice_widget_key': 'defghijklmnopqrstuvw'} r = UserVoiceNode().render(Context(vars)) - self.assertIn("widget.uservoice.com/defghijklmnopqrstuvw.js", r) + assert 'widget.uservoice.com/defghijklmnopqrstuvw.js' in r @override_settings(USERVOICE_WIDGET_OPTIONS={'key1': 'val1'}) def test_options(self): r = UserVoiceNode().render(Context()) - self.assertIn("""UserVoice.push(['set', {"key1": "val1"}]);""", r) + assert """UserVoice.push(['set', {"key1": "val1"}]);""" in r @override_settings(USERVOICE_WIDGET_OPTIONS={'key1': 'val1'}) def test_override_options(self): data = {'uservoice_widget_options': {'key1': 'val2'}} r = UserVoiceNode().render(Context(data)) - self.assertIn("""UserVoice.push(['set', {"key1": "val2"}]);""", r) + assert """UserVoice.push(['set', {"key1": "val2"}]);""" in r def test_auto_trigger_default(self): r = UserVoiceNode().render(Context()) - self.assertTrue("UserVoice.push(['addTrigger', {}]);" in r, r) + assert "UserVoice.push(['addTrigger', {}]);" in r @override_settings(USERVOICE_ADD_TRIGGER=False) def test_auto_trigger(self): r = UserVoiceNode().render(Context()) - self.assertFalse("UserVoice.push(['addTrigger', {}]);" in r, r) + assert "UserVoice.push(['addTrigger', {}]);" not in r @override_settings(USERVOICE_ADD_TRIGGER=False) def test_auto_trigger_custom_win(self): r = UserVoiceNode().render(Context({'uservoice_add_trigger': True})) - self.assertTrue("UserVoice.push(['addTrigger', {}]);" in r, r) + assert "UserVoice.push(['addTrigger', {}]);" in r diff --git a/tests/unit/test_tag_woopra.py b/tests/unit/test_tag_woopra.py new file mode 100644 index 0000000..8369323 --- /dev/null +++ b/tests/unit/test_tag_woopra.py @@ -0,0 +1,207 @@ +""" +Tests for the Woopra template tags and filters. +""" + +from datetime import datetime + +import pytest +from django.contrib.auth.models import AnonymousUser, User +from django.http import HttpRequest +from django.template import Context +from django.test.utils import override_settings +from utils import TagTestCase + +from analytical.templatetags.woopra import WoopraNode +from analytical.utils import AnalyticalException + + +@override_settings(WOOPRA_DOMAIN='example.com') +class WoopraTagTestCase(TagTestCase): + """ + Tests for the ``woopra`` template tag. + """ + + def test_tag(self): + r = self.render_tag('woopra', 'woopra') + assert 'var woo_settings = {"domain": "example.com"};' in r + + def test_node(self): + r = WoopraNode().render(Context({})) + assert 'var woo_settings = {"domain": "example.com"};' in r + + @override_settings(WOOPRA_DOMAIN=None) + def test_no_domain(self): + with pytest.raises(AnalyticalException): + WoopraNode() + + @override_settings(WOOPRA_DOMAIN='this is not a domain') + def test_wrong_domain(self): + with pytest.raises(AnalyticalException): + WoopraNode() + + @override_settings(WOOPRA_IDLE_TIMEOUT=1234) + def test_idle_timeout(self): + r = WoopraNode().render(Context({})) + assert ( + 'var woo_settings = {"domain": "example.com", "idle_timeout": 1234};' + ) in r + + @override_settings(WOOPRA_COOKIE_NAME='foo') + def test_cookie_name(self): + r = WoopraNode().render(Context({})) + assert ( + 'var woo_settings = {"cookie_name": "foo", "domain": "example.com"};' + ) in r + + @override_settings(WOOPRA_COOKIE_DOMAIN='.example.com') + def test_cookie_domain(self): + r = WoopraNode().render(Context({})) + assert ( + 'var woo_settings = {"cookie_domain": ".example.com",' + ' "domain": "example.com"};' + ) in r + + @override_settings(WOOPRA_COOKIE_PATH='/foo/cookie/path') + def test_cookie_path(self): + r = WoopraNode().render(Context({})) + assert ( + 'var woo_settings = {"cookie_path": "/foo/cookie/path",' + ' "domain": "example.com"};' + ) in r + + @override_settings(WOOPRA_COOKIE_EXPIRE='Fri Jan 01 2027 15:00:00 GMT+0000') + def test_cookie_expire(self): + r = WoopraNode().render(Context({})) + assert ( + 'var woo_settings = {"cookie_expire":' + ' "Fri Jan 01 2027 15:00:00 GMT+0000", "domain": "example.com"};' + ) in r + + @override_settings(WOOPRA_CLICK_TRACKING=True) + def test_click_tracking(self): + r = WoopraNode().render(Context({})) + assert ( + 'var woo_settings = {"click_tracking": true, "domain": "example.com"};' + ) in r + + @override_settings(WOOPRA_DOWNLOAD_TRACKING=True) + def test_download_tracking(self): + r = WoopraNode().render(Context({})) + assert ( + 'var woo_settings = {"domain": "example.com", "download_tracking": true};' + ) in r + + @override_settings(WOOPRA_OUTGOING_TRACKING=True) + def test_outgoing_tracking(self): + r = WoopraNode().render(Context({})) + assert ( + 'var woo_settings = {"domain": "example.com", "outgoing_tracking": true};' + ) in r + + @override_settings(WOOPRA_OUTGOING_IGNORE_SUBDOMAIN=False) + def test_outgoing_ignore_subdomain(self): + r = WoopraNode().render(Context({})) + assert ( + 'var woo_settings = {"domain": "example.com",' + ' "outgoing_ignore_subdomain": false};' + ) in r + + @override_settings(WOOPRA_IGNORE_QUERY_URL=False) + def test_ignore_query_url(self): + r = WoopraNode().render(Context({})) + assert ( + 'var woo_settings = {"domain": "example.com", "ignore_query_url": false};' + ) in r + + @override_settings(WOOPRA_HIDE_CAMPAIGN=True) + def test_hide_campaign(self): + r = WoopraNode().render(Context({})) + assert ( + 'var woo_settings = {"domain": "example.com", "hide_campaign": true};' + ) in r + + @override_settings(WOOPRA_IDLE_TIMEOUT='1234') + def test_invalid_int_setting(self): + with pytest.raises(AnalyticalException, match=r'must be an int'): + WoopraNode().render(Context({})) + + @override_settings(WOOPRA_HIDE_CAMPAIGN='tomorrow') + def test_invalid_bool_setting(self): + with pytest.raises(AnalyticalException, match=r'must be a boolean'): + WoopraNode().render(Context({})) + + @override_settings(WOOPRA_COOKIE_EXPIRE=datetime.now()) + def test_invalid_str_setting(self): + with pytest.raises(AnalyticalException, match=r'must be a string'): + WoopraNode().render(Context({})) + + def test_custom(self): + r = WoopraNode().render( + Context( + { + 'woopra_var1': 'val1', + 'woopra_var2': 'val2', + } + ) + ) + assert 'var woo_visitor = {"var1": "val1", "var2": "val2"};' in 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', + ), + } + ) + ) + assert 'var woo_visitor = ' + '{"email": "test@example.com", "name": "Firstname Lastname"};' in r + + @override_settings(ANALYTICAL_AUTO_IDENTIFY=True) + def test_identify_username_no_email(self): + r = WoopraNode().render(Context({'user': User(username='test')})) + assert 'var woo_visitor = {"name": "test"};' in r + + @override_settings(ANALYTICAL_AUTO_IDENTIFY=True) + def test_no_identify_when_explicit_name(self): + r = WoopraNode().render( + Context( + { + 'woopra_name': 'explicit', + 'user': User(username='implicit'), + } + ) + ) + assert 'var woo_visitor = {"name": "explicit"};' in 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'), + } + ) + ) + assert 'var woo_visitor = {"email": "explicit"};' in r + + @override_settings(ANALYTICAL_AUTO_IDENTIFY=True) + def test_identify_anonymous_user(self): + r = WoopraNode().render(Context({'user': AnonymousUser()})) + assert 'var woo_visitor = {};' in r + + @override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1']) + def test_render_internal_ip(self): + req = HttpRequest() + req.META['REMOTE_ADDR'] = '1.1.1.1' + context = Context({'request': req}) + r = WoopraNode().render(context) + assert r.startswith('') diff --git a/analytical/tests/test_tag_yandex_metrica.py b/tests/unit/test_tag_yandex_metrica.py similarity index 69% rename from analytical/tests/test_tag_yandex_metrica.py rename to tests/unit/test_tag_yandex_metrica.py index fa287db..b28dd8f 100644 --- a/analytical/tests/test_tag_yandex_metrica.py +++ b/tests/unit/test_tag_yandex_metrica.py @@ -2,13 +2,13 @@ Tests for the Yandex.Metrica template tags and filters. """ - +import pytest from django.http import HttpRequest from django.template import Context from django.test.utils import override_settings +from utils import TagTestCase from analytical.templatetags.yandex_metrica import YandexMetricaNode -from analytical.tests.utils import TagTestCase from analytical.utils import AnalyticalException @@ -20,19 +20,21 @@ class YandexMetricaTagTestCase(TagTestCase): def test_tag(self): r = self.render_tag('yandex_metrica', 'yandex_metrica') - self.assertTrue("w.yaCounter12345678 = new Ya.Metrika" in r, r) + assert 'w.yaCounter12345678 = new Ya.Metrika' in r def test_node(self): r = YandexMetricaNode().render(Context({})) - self.assertTrue("w.yaCounter12345678 = new Ya.Metrika" in r, r) + assert 'w.yaCounter12345678 = new Ya.Metrika' in r @override_settings(YANDEX_METRICA_COUNTER_ID=None) def test_no_site_id(self): - self.assertRaises(AnalyticalException, YandexMetricaNode) + with pytest.raises(AnalyticalException): + YandexMetricaNode() @override_settings(YANDEX_METRICA_COUNTER_ID='1234abcd') def test_wrong_site_id(self): - self.assertRaises(AnalyticalException, YandexMetricaNode) + with pytest.raises(AnalyticalException): + YandexMetricaNode() @override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1']) def test_render_internal_ip(self): @@ -40,6 +42,5 @@ class YandexMetricaTagTestCase(TagTestCase): req.META['REMOTE_ADDR'] = '1.1.1.1' context = Context({'request': req}) r = YandexMetricaNode().render(context) - self.assertTrue(r.startswith( - ''), r) + assert r.startswith('') diff --git a/analytical/tests/test_utils.py b/tests/unit/test_utils.py similarity index 68% rename from analytical/tests/test_utils.py rename to tests/unit/test_utils.py index 83c4cf6..8816d73 100644 --- a/analytical/tests/test_utils.py +++ b/tests/unit/test_utils.py @@ -3,11 +3,13 @@ Tests for the analytical.utils module. """ # import django +import pytest from django.contrib.auth.models import AbstractBaseUser from django.db import models from django.http import HttpRequest from django.template import Context from django.test.utils import override_settings +from utils import TestCase from analytical.utils import ( AnalyticalException, @@ -16,59 +18,75 @@ from analytical.utils import ( get_required_setting, is_internal_ip, ) -from analytical.tests.utils import TestCase class SettingDeletedTestCase(TestCase): - @override_settings(USER_ID=None) def test_get_required_setting(self): """ Make sure using get_required_setting fails in the right place. """ - # available in python >= 3.2 - if hasattr(self, 'assertRaisesRegex'): - with self.assertRaisesRegex(AnalyticalException, "^USER_ID setting is 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 not set$"): - get_required_setting("USER_ID", r"\d+", "invalid USER_ID") - else: - self.assertRaises(AnalyticalException, - get_required_setting, "USER_ID", r"\d+", "invalid USER_ID") + with pytest.raises(AnalyticalException, match='USER_ID setting is not set'): + get_required_setting('USER_ID', r'\d+', 'invalid USER_ID') class MyUser(AbstractBaseUser): identity = models.CharField(max_length=50) USERNAME_FIELD = 'identity' + class Meta: + abstract = False + app_label = 'testapp' + class GetIdentityTestCase(TestCase): def test_custom_username_field(self): get_id = get_identity(Context({}), user=MyUser(identity='fake_id')) - self.assertEqual(get_id, 'fake_id') + assert get_id == 'fake_id' + + def test_custom_identity_specific_provider(self): + get_id = get_identity( + Context( + { + 'foo_provider_identity': 'bar', + 'analytical_identity': 'baz', + } + ), + prefix='foo_provider', + ) + assert get_id == 'bar' + + def test_custom_identity_general(self): + get_id = get_identity( + Context( + { + 'analytical_identity': 'baz', + } + ), + prefix='foo_provider', + ) + assert get_id == 'baz' -@override_settings(ANALYTICAL_DOMAIN="example.org") +@override_settings(ANALYTICAL_DOMAIN='example.org') class GetDomainTestCase(TestCase): def test_get_service_domain_from_context(self): context = Context({'test_domain': 'example.com'}) - self.assertEqual(get_domain(context, 'test'), 'example.com') + assert get_domain(context, 'test') == 'example.com' def test_get_analytical_domain_from_context(self): context = Context({'analytical_domain': 'example.com'}) - self.assertEqual(get_domain(context, 'test'), 'example.com') + assert get_domain(context, 'test') == 'example.com' - @override_settings(TEST_DOMAIN="example.net") + @override_settings(TEST_DOMAIN='example.net') def test_get_service_domain_from_settings(self): context = Context() - self.assertEqual(get_domain(context, 'test'), 'example.net') + assert get_domain(context, 'test') == 'example.net' def test_get_analytical_domain_from_settings(self): context = Context() - self.assertEqual(get_domain(context, 'test'), 'example.org') + assert get_domain(context, 'test') == 'example.org' # FIXME: enable Django apps dynamically and enable test again @@ -83,11 +101,10 @@ class GetDomainTestCase(TestCase): class InternalIpTestCase(TestCase): - @override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1']) def test_render_no_internal_ip(self): context = Context() - self.assertFalse(is_internal_ip(context)) + assert not is_internal_ip(context) @override_settings(INTERNAL_IPS=['1.1.1.1']) @override_settings(ANALYTICAL_INTERNAL_IPS=[]) @@ -95,39 +112,39 @@ class InternalIpTestCase(TestCase): req = HttpRequest() req.META['REMOTE_ADDR'] = '1.1.1.1' context = Context({'request': req}) - self.assertFalse(is_internal_ip(context)) + assert not is_internal_ip(context) @override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1']) def test_render_internal_ip(self): req = HttpRequest() req.META['REMOTE_ADDR'] = '1.1.1.1' context = Context({'request': req}) - self.assertTrue(is_internal_ip(context)) + assert is_internal_ip(context) @override_settings(TEST_INTERNAL_IPS=['1.1.1.1']) def test_render_prefix_internal_ip(self): req = HttpRequest() req.META['REMOTE_ADDR'] = '1.1.1.1' context = Context({'request': req}) - self.assertTrue(is_internal_ip(context, 'TEST')) + assert is_internal_ip(context, 'TEST') @override_settings(INTERNAL_IPS=['1.1.1.1']) def test_render_internal_ip_fallback(self): req = HttpRequest() req.META['REMOTE_ADDR'] = '1.1.1.1' context = Context({'request': req}) - self.assertTrue(is_internal_ip(context)) + assert is_internal_ip(context) @override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1']) def test_render_internal_ip_forwarded_for(self): req = HttpRequest() req.META['HTTP_X_FORWARDED_FOR'] = '1.1.1.1' context = Context({'request': req}) - self.assertTrue(is_internal_ip(context)) + assert is_internal_ip(context) @override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1']) def test_render_different_internal_ip(self): req = HttpRequest() req.META['REMOTE_ADDR'] = '2.2.2.2' context = Context({'request': req}) - self.assertFalse(is_internal_ip(context)) + assert not is_internal_ip(context) diff --git a/analytical/tests/utils.py b/tests/unit/utils.py similarity index 54% rename from analytical/tests/utils.py rename to tests/unit/utils.py index 11d106d..1b3ecd6 100644 --- a/analytical/tests/utils.py +++ b/tests/unit/utils.py @@ -2,32 +2,10 @@ Testing utilities. """ -from __future__ import with_statement - -from django.template import Template, Context, RequestContext +from django.template import Context, RequestContext, Template from django.test.testcases import TestCase -def run_tests(): - """ - Use the Django test runner to run the tests. - - Sets the return code to the number of failed tests. - """ - import sys - import django - try: - django.setup() - except AttributeError: - pass - try: - from django.test.runner import DiscoverRunner as TestRunner - except ImportError: - from django.test.simple import DjangoTestSuiteRunner as TestRunner - runner = TestRunner() - sys.exit(runner.run_tests(["analytical"])) - - class TagTestCase(TestCase): """ Tests for a template tag. @@ -38,7 +16,7 @@ class TagTestCase(TestCase): def render_tag(self, library, tag, vars=None, request=None): if vars is None: vars = {} - t = Template("{%% load %s %%}{%% %s %%}" % (library, tag)) + t = Template('{%% load %s %%}{%% %s %%}' % (library, tag)) if request is not None: context = RequestContext(request, vars) else: diff --git a/tox.ini b/tox.ini index 2eebb33..dfaa89a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,64 +1,97 @@ [tox] envlist = - # Python/Django combinations that are officially supported - py{27,34,35,36}-django111 - py{35,36,37}-django{21,22} - flake8 - bandit - readme + lint + format + audit + # Python/Django combinations that are officially supported (minus end-of-life Pythons) + py{39,310,311,312}-django{42} + py{310,311,312,313}-django{51} + py{310,311,312,313}-django{52} + package docs clean +[gh-actions] +python = + 3.9: py39 + 3.10: py310 + 3.11: py311 + 3.12: py312 + 3.13: py313 + +[gh-actions:env] +DJANGO = + 4.2: django42 + 5.1: django51 + 5.2: django52 + [testenv] -commands = - coverage run setup.py test - sh -c 'coveralls | true' +description = Unit tests deps = - coverage - coveralls - django111: Django>=1.11,<2.0 - django21: Django>=2.1,<2.2 - django22: Django>=2.2,<3.0 -passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH -whitelist_externals = sh + coverage[toml] + pytest-django + django42: Django>=4.2,<5.0 + django51: Django>=5.1,<5.2 + django52: Django>=5.2,<6.0 +commands = + coverage run -m pytest {posargs} + coverage report + coverage xml + +[testenv:audit] +description = Scan for vulnerable dependencies +skip_install = true +deps = + pip-audit + uv +commands = + uv export --no-emit-project --no-hashes -o requirements.txt -q + pip-audit {posargs:-r requirements.txt --progress-spinner off} [testenv:bandit] +description = PyCQA security linter +skip_install = true deps = bandit -commands = bandit -r --ini tox.ini +commands = bandit {posargs:-r analytical} -v [testenv:clean] +description = Clean up bytecode and build artifacts +skip_install = true deps = pyclean -commands = - py3clean -v {toxinidir} - rm -rf .tox/ django_analytical.egg-info/ build/ dist/ docs/_build/ -whitelist_externals = - rm +commands = pyclean {posargs:. --debris cache coverage package pytest mypy --erase requirements.txt uv.lock docs/_build/**/* docs/_build/ tests/unittests-report.xml --yes} [testenv:docs] +description = Build the HTML documentation deps = sphinx commands = sphinx-build -b html -d docs/_build/doctrees docs docs/_build/html -whitelist_externals = make -[testenv:flake8] -deps = flake8 -commands = flake8 +[testenv:format] +description = Ensure consistent code style (Ruff) +skip_install = true +deps = ruff +commands = ruff format {posargs:--check --diff .} -[testenv:readme] -deps = twine +[testenv:lint] +description = Lightening-fast linting (Ruff) +skip_install = true +deps = ruff +commands = ruff check {posargs:--output-format=full .} + +[testenv:mypy] +description = Perform static type checking +deps = mypy +commands = mypy {posargs:.} + +[testenv:package] +description = Build package and check metadata (or upload package) +skip_install = true +deps = + build + twine commands = - {envpython} setup.py -q sdist bdist_wheel - twine check dist/* - -[travis:env] -DJANGO = - 1.11: django111 - 2.1: django21 - 2.2: django22 - -[bandit] -exclude = .cache,.git,.tox,build,dist,docs,tests -targets = . - -[flake8] -exclude = .cache,.git,.tox,build,dist -max-line-length = 100 + python -m build + twine {posargs:check --strict} dist/* +passenv = + TWINE_USERNAME + TWINE_PASSWORD + TWINE_REPOSITORY_URL