├── plausible
├── py.typed
├── __init__.py
├── contrib
│ ├── __init__.py
│ └── wagtail
│ │ ├── __init__.py
│ │ ├── migrations
│ │ ├── __init__.py
│ │ └── 0001_initial.py
│ │ ├── templatetags
│ │ ├── __init__.py
│ │ └── plausible_wagtail.py
│ │ ├── apps.py
│ │ ├── validators.py
│ │ └── models.py
├── templatetags
│ ├── __init__.py
│ └── plausible.py
└── utils.py
├── tests
├── __init__.py
├── templates
│ ├── simple.html
│ └── simple_wagtail.html
├── urls.py
├── test_utils.py
├── settings.py
├── test_wagtail.py
└── test_django.py
├── pyproject.toml
├── dev-requirements.txt
├── renovate.json
├── manage.py
├── scripts
└── test.sh
├── setup.cfg
├── .github
└── workflows
│ ├── deploy.yml
│ └── ci.yml
├── LICENSE
├── setup.py
├── .gitignore
└── README.md
/plausible/py.typed:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/plausible/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/plausible/contrib/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/plausible/contrib/wagtail/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/plausible/templatetags/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/plausible/contrib/wagtail/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/plausible/contrib/wagtail/templatetags/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/templates/simple.html:
--------------------------------------------------------------------------------
1 | {% load plausible %}
2 |
3 | {% plausible %}
4 |
--------------------------------------------------------------------------------
/tests/templates/simple_wagtail.html:
--------------------------------------------------------------------------------
1 | {% load plausible_wagtail %}
2 |
3 | {% plausible %}
4 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.ruff]
2 | select = ["E", "F", "I", "W", "N", "B", "A", "C4", "T20", "DJ"]
3 | ignore = ["E501"]
4 |
--------------------------------------------------------------------------------
/dev-requirements.txt:
--------------------------------------------------------------------------------
1 | -e .[wagtail]
2 |
3 | ruff==0.0.257
4 | mypy==1.1.1
5 | black==22.10.0
6 | pytest==7.2.2
7 | pytest-django==4.5.2
8 | pytest-cov==4.0.0
9 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:base",
5 | ":disableDependencyDashboard"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/tests/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from django.views.generic import TemplateView
3 |
4 | urlpatterns = [
5 | path("simple", TemplateView.as_view(template_name="simple.html")),
6 | path("simple-wagtail", TemplateView.as_view(template_name="simple_wagtail.html")),
7 | ]
8 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == "__main__":
6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings")
7 |
8 | from django.core.management import execute_from_command_line
9 |
10 | execute_from_command_line(sys.argv)
11 |
--------------------------------------------------------------------------------
/plausible/contrib/wagtail/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 | from django.utils.translation import gettext_lazy as _
3 |
4 |
5 | class WagtailPlausibleAppConfig(AppConfig):
6 | name = "plausible.contrib.wagtail"
7 | label = "wagtailplausible"
8 |
9 | verbose_name = _("Wagtail Plausible")
10 |
--------------------------------------------------------------------------------
/scripts/test.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | export PATH=env/bin:${PATH}
4 |
5 | set -ex
6 |
7 | pytest --verbose --cov plausible/ --cov-report term --cov-report html tests/
8 |
9 | if hash black 2>/dev/null;
10 | then
11 | black plausible tests setup.py --check
12 | fi
13 |
14 | ruff check plausible tests setup.py
15 |
16 | mypy plausible tests setup.py
17 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [mypy]
2 | mypy_path = stubs
3 | no_implicit_optional = True
4 | warn_unused_ignores = True
5 | strict_optional = True
6 | check_untyped_defs = True
7 | ignore_missing_imports = True
8 |
9 | [isort]
10 | multi_line_output=3
11 | include_trailing_comma=True
12 | force_grid_wrap=0
13 | use_parentheses=True
14 | line_length=88
15 |
16 | [flake8]
17 | extend_ignore=E128,E501
18 |
19 | [tool:pytest]
20 | DJANGO_SETTINGS_MODULE=tests.settings
21 |
--------------------------------------------------------------------------------
/plausible/contrib/wagtail/templatetags/plausible_wagtail.py:
--------------------------------------------------------------------------------
1 | from django import template
2 |
3 | from plausible.contrib.wagtail.models import PlausibleSettings
4 | from plausible.templatetags.plausible import plausible as plausible_tag
5 |
6 | register = template.Library()
7 |
8 |
9 | @register.simple_tag(takes_context=True)
10 | def plausible(context):
11 | plausible_settings = PlausibleSettings.for_request(context["request"])
12 |
13 | return plausible_tag(
14 | context,
15 | plausible_settings.site_domain
16 | or None, # `None` so it defaults to request hostname
17 | plausible_settings.plausible_domain,
18 | plausible_settings.script_name,
19 | )
20 |
--------------------------------------------------------------------------------
/plausible/contrib/wagtail/validators.py:
--------------------------------------------------------------------------------
1 | from os.path import basename
2 |
3 | from django.core.exceptions import ValidationError
4 | from django.utils.deconstruct import deconstructible
5 | from django.utils.translation import gettext_lazy as _
6 |
7 | from plausible.utils import is_valid_plausible_script
8 |
9 |
10 | @deconstructible
11 | class PlausibleScriptNameValidator:
12 | message = _("Enter a valid value (eg 'plausible.js').")
13 |
14 | def __call__(self, value: str):
15 | if basename(value) != value:
16 | raise ValidationError(self.message, code="invalid")
17 |
18 | if not is_valid_plausible_script(basename(value)):
19 | raise ValidationError(self.message, code="invalid")
20 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: Upload Python Package
2 |
3 | on:
4 | release:
5 | types: [created]
6 |
7 | jobs:
8 | deploy:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v3
12 | - name: Set up Python
13 | uses: actions/setup-python@v4
14 | with:
15 | python-version: '3.11'
16 | - name: Install dependencies
17 | run: |
18 | pip install --upgrade pip wheel
19 | pip install setuptools twine
20 | - name: Build and publish
21 | env:
22 | TWINE_USERNAME: __token__
23 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
24 | run: |
25 | python setup.py sdist bdist_wheel
26 | twine upload dist/*
27 |
--------------------------------------------------------------------------------
/plausible/utils.py:
--------------------------------------------------------------------------------
1 | BASE_FILENAMES = ["plausible", "script", "analytics"]
2 |
3 | BASE_VARIANTS = {
4 | "hash",
5 | "outbound-links",
6 | "exclusions",
7 | "compat",
8 | "local",
9 | "manual",
10 | "file-downloads",
11 | "dimensions",
12 | }
13 |
14 | KNOWN_FILENAMES = ["p.js", "plausible.js"]
15 |
16 |
17 | def is_valid_plausible_script(filename: str) -> bool:
18 | """
19 | Validate a script name against allowed values
20 |
21 |
22 | See also https://plausible.io/docs/script-extensions
23 | """
24 | if filename in KNOWN_FILENAMES:
25 | return True
26 |
27 | try:
28 | base_name, *variants, extension = filename.split(".")
29 | except ValueError:
30 | return False
31 |
32 | if extension != "js" or base_name not in BASE_FILENAMES:
33 | return False
34 |
35 | return BASE_VARIANTS.issuperset(variants)
36 |
--------------------------------------------------------------------------------
/plausible/contrib/wagtail/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from wagtail.contrib.settings.models import register_setting
3 |
4 | # FIXME: Remove after support for Wagtail 5.0 is dropped
5 | try:
6 | from wagtail.contrib.settings.models import BaseSiteSetting
7 | except ImportError:
8 | # Prior to Wagtail 3.0, the only setting available was based on the Site
9 | from wagtail.contrib.settings.models import BaseSetting as BaseSiteSetting
10 |
11 | from .validators import PlausibleScriptNameValidator
12 |
13 |
14 | @register_setting
15 | class PlausibleSettings(BaseSiteSetting):
16 | site_domain = models.CharField(max_length=255, null=False, blank=True)
17 | plausible_domain = models.CharField(
18 | max_length=255,
19 | null=False,
20 | blank=True,
21 | default="plausible.io",
22 | )
23 | script_name = models.CharField(
24 | max_length=255,
25 | validators=[PlausibleScriptNameValidator()],
26 | default="plausible.js",
27 | )
28 |
29 | class Meta:
30 | verbose_name = "Plausible Analytics"
31 |
--------------------------------------------------------------------------------
/tests/test_utils.py:
--------------------------------------------------------------------------------
1 | from itertools import combinations
2 |
3 | import pytest
4 |
5 | from plausible import utils
6 |
7 |
8 | @pytest.mark.parametrize(
9 | "script_name",
10 | utils.KNOWN_FILENAMES,
11 | )
12 | def test_known_filenames(script_name):
13 | assert utils.is_valid_plausible_script(script_name)
14 |
15 |
16 | @pytest.mark.parametrize(
17 | "script_name",
18 | [
19 | "left-pad.js",
20 | "plausible.io",
21 | "plausible..js",
22 | "plausible.nothing.js",
23 | "plausible.hash",
24 | "hash.js",
25 | "file",
26 | "/plausible.js",
27 | ],
28 | )
29 | def test_invalid_filenames(script_name):
30 | assert not utils.is_valid_plausible_script(script_name)
31 |
32 |
33 | @pytest.mark.parametrize(
34 | "variant",
35 | [
36 | ".".join(sorted(v))
37 | for n in range(1, len(utils.BASE_VARIANTS))
38 | for v in combinations(utils.BASE_VARIANTS, n)
39 | ],
40 | )
41 | @pytest.mark.parametrize(
42 | "base_filenames",
43 | utils.BASE_FILENAMES,
44 | )
45 | def test_variants(variant, base_filenames):
46 | assert utils.is_valid_plausible_script(f"{base_filenames}.{variant}.js")
47 |
--------------------------------------------------------------------------------
/plausible/templatetags/plausible.py:
--------------------------------------------------------------------------------
1 | from django import template
2 | from django.conf import settings
3 | from django.forms.utils import flatatt
4 | from django.utils.html import escape
5 | from django.utils.safestring import mark_safe
6 |
7 | from plausible.utils import is_valid_plausible_script
8 |
9 | register = template.Library()
10 |
11 |
12 | @register.simple_tag(takes_context=True)
13 | def plausible(context, site_domain=None, plausible_domain=None, script_name=None):
14 | request = context["request"]
15 |
16 | if plausible_domain is None:
17 | plausible_domain = getattr(settings, "PLAUSIBLE_DOMAIN", "plausible.io")
18 | if script_name is None:
19 | script_name = getattr(settings, "PLAUSIBLE_SCRIPT_NAME", "plausible.js")
20 | if site_domain is None:
21 | site_domain = escape(request.get_host()) # In case of XSS
22 |
23 | if not is_valid_plausible_script(script_name):
24 | raise ValueError(f"Invalid plausible script name: {script_name}")
25 |
26 | attrs = {
27 | "defer": True,
28 | "data-domain": site_domain,
29 | "src": f"https://{plausible_domain}/js/{script_name}",
30 | }
31 |
32 | # Add a target id for use with compat script
33 | if "compat" in script_name:
34 | attrs["id"] = "plausible"
35 |
36 | return mark_safe(f"")
37 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: [pull_request, push]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | strategy:
9 | matrix:
10 | python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
11 | django-version: ["3.2", "4.0", "4.1", "4.2"]
12 | wagtail-version: ["4.0", "4.1", "4.2", "5.0"]
13 | exclude:
14 | - django-version: "4.2"
15 | python-version: "3.7"
16 | - django-version: "4.1"
17 | python-version: "3.7"
18 | - django-version: "4.0"
19 | python-version: "3.7"
20 | steps:
21 | - uses: actions/checkout@v3
22 | - name: Set up Python ${{ matrix.python-version }}
23 | uses: actions/setup-python@v4
24 | with:
25 | python-version: ${{ matrix.python-version }}
26 | - uses: actions/cache@v3
27 | with:
28 | path: ~/.cache/pip
29 | key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }}-${{ hashFiles('dev-requirements.txt') }}
30 | - name: Install dependencies
31 | run: |
32 | pip install --upgrade pip
33 | pip install -r dev-requirements.txt
34 | pip install -U Django==${{ matrix.django-version }}
35 | pip install -U Wagtail==${{ matrix.wagtail-version }}
36 | - name: Run tests
37 | run: scripts/test.sh
38 | - name: Build package
39 | run: python setup.py clean sdist
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2021, Jake Howard
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | 1. Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | 2. Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | 3. Neither the name of the copyright holder nor the names of its
17 | contributors may be used to endorse or promote products derived from
18 | this software without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/tests/settings.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
4 |
5 | ALLOWED_HOSTS = ["*"]
6 |
7 | INSTALLED_APPS = [
8 | "django.contrib.staticfiles",
9 | "plausible",
10 | "tests",
11 | "django.contrib.admin",
12 | "django.contrib.contenttypes",
13 | # Wagtail stuff
14 | "plausible.contrib.wagtail",
15 | "django.contrib.auth",
16 | "django.contrib.messages",
17 | "wagtail.contrib.settings",
18 | "wagtail.contrib.redirects",
19 | "wagtail.sites",
20 | "wagtail.users",
21 | "wagtail.admin",
22 | "wagtail",
23 | ]
24 |
25 | TEMPLATES = [
26 | {
27 | "BACKEND": "django.template.backends.django.DjangoTemplates",
28 | "DIRS": [],
29 | "APP_DIRS": True,
30 | "OPTIONS": {
31 | "context_processors": [
32 | "django.template.context_processors.debug",
33 | "django.template.context_processors.request",
34 | "django.contrib.auth.context_processors.auth",
35 | "django.contrib.messages.context_processors.messages",
36 | ]
37 | },
38 | },
39 | ]
40 |
41 | SECRET_KEY = "abcde12345"
42 |
43 | ROOT_URLCONF = "tests.urls"
44 |
45 | DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
46 |
47 | MIDDLEWARE = [
48 | "django.contrib.sessions.middleware.SessionMiddleware",
49 | "django.contrib.auth.middleware.AuthenticationMiddleware",
50 | "django.contrib.messages.middleware.MessageMiddleware",
51 | ]
52 |
53 | DATABASES = {
54 | "default": {
55 | "ENGINE": "django.db.backends.sqlite3",
56 | "NAME": ":memory:",
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import find_packages, setup
2 |
3 | with open("README.md") as f:
4 | readme = f.read()
5 |
6 | setup(
7 | name="django-plausible",
8 | version="0.5.0",
9 | url="https://github.com/RealOrangeOne/django-plausible",
10 | author="Jake Howard",
11 | description=" Django module to provide easy Plausible integration, with Wagtail support",
12 | long_description=readme,
13 | long_description_content_type="text/markdown",
14 | license="BSD",
15 | packages=find_packages(include=["plausible*"]),
16 | package_data={"plausible": ["py.typed"]},
17 | install_requires=["Django>=3.2"],
18 | python_requires=">=3.7",
19 | extras_require={"wagtail": ["wagtail>=4.0"]},
20 | keywords="django plausible wagtail analytics",
21 | classifiers=[
22 | "Environment :: Web Environment",
23 | "Framework :: Wagtail",
24 | "Framework :: Wagtail :: 4",
25 | "Framework :: Wagtail :: 5",
26 | "Framework :: Django",
27 | "Framework :: Django :: 3.2",
28 | "Framework :: Django :: 4.0",
29 | "Framework :: Django :: 4.1",
30 | "Framework :: Django :: 4.2",
31 | "Intended Audience :: Developers",
32 | "License :: OSI Approved :: BSD License",
33 | "Operating System :: OS Independent",
34 | "Programming Language :: Python :: 3.7",
35 | "Programming Language :: Python :: 3.8",
36 | "Programming Language :: Python :: 3.9",
37 | "Programming Language :: Python :: 3.10",
38 | "Programming Language :: Python :: 3.11",
39 | "Programming Language :: Python :: 3",
40 | "Programming Language :: Python",
41 | "Topic :: Internet :: WWW/HTTP",
42 | "Topic :: Software Development",
43 | "Typing :: Typed",
44 | ],
45 | )
46 |
--------------------------------------------------------------------------------
/plausible/contrib/wagtail/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.7 on 2021-09-05 08:47
2 |
3 | import django.db.models.deletion
4 | from django.db import migrations, models
5 |
6 | import plausible.contrib.wagtail.validators
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | initial = True
12 |
13 | dependencies = [
14 | ("wagtailcore", "0062_comment_models_and_pagesubscription"),
15 | ]
16 |
17 | operations = [
18 | migrations.CreateModel(
19 | name="PlausibleSettings",
20 | fields=[
21 | (
22 | "id",
23 | models.AutoField(
24 | auto_created=True,
25 | primary_key=True,
26 | serialize=False,
27 | verbose_name="ID",
28 | ),
29 | ),
30 | (
31 | "site_domain",
32 | models.CharField(
33 | blank=True,
34 | max_length=255,
35 | ),
36 | ),
37 | (
38 | "plausible_domain",
39 | models.CharField(
40 | blank=True,
41 | default="plausible.io",
42 | max_length=255,
43 | ),
44 | ),
45 | (
46 | "script_name",
47 | models.CharField(
48 | default="plausible.js",
49 | max_length=255,
50 | validators=[
51 | plausible.contrib.wagtail.validators.PlausibleScriptNameValidator()
52 | ],
53 | ),
54 | ),
55 | (
56 | "site",
57 | models.OneToOneField(
58 | editable=False,
59 | on_delete=django.db.models.deletion.CASCADE,
60 | to="wagtailcore.site",
61 | ),
62 | ),
63 | ],
64 | options={
65 | "verbose_name": "Plausible Analytics",
66 | },
67 | ),
68 | ]
69 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 |
--------------------------------------------------------------------------------
/tests/test_wagtail.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from django.core.exceptions import ValidationError
3 | from pytest_django.asserts import assertInHTML
4 | from wagtail.models import Site
5 |
6 | from plausible.contrib.wagtail.models import PlausibleSettings
7 | from plausible.contrib.wagtail.validators import PlausibleScriptNameValidator
8 |
9 |
10 | @pytest.fixture
11 | def plausible_settings():
12 | return PlausibleSettings.for_site(Site.objects.get())
13 |
14 |
15 | @pytest.mark.django_db
16 | def test_simple_template_view(client):
17 | response = client.get("/simple-wagtail", HTTP_HOST="example.com")
18 | assert response.status_code == 200
19 | assertInHTML(
20 | '',
21 | response.content.decode(),
22 | )
23 |
24 |
25 | @pytest.mark.django_db
26 | def test_hostname_from_settings(client, plausible_settings):
27 | plausible_settings.site_domain = "from-settings.com"
28 | plausible_settings.save()
29 | response = client.get("/simple-wagtail")
30 | assert response.status_code == 200
31 | assertInHTML(
32 | '',
33 | response.content.decode(),
34 | )
35 |
36 |
37 | @pytest.mark.django_db
38 | def test_script_name_from_settings(client, plausible_settings):
39 | plausible_settings.script_name = "plausible.hash.js"
40 | plausible_settings.save()
41 | response = client.get("/simple-wagtail")
42 | assert response.status_code == 200
43 | assertInHTML(
44 | '',
45 | response.content.decode(),
46 | )
47 |
48 |
49 | @pytest.mark.django_db
50 | def test_plausible_domain_from_settings(client, plausible_settings):
51 | plausible_settings.plausible_domain = "my-plausible.com"
52 | plausible_settings.save()
53 | response = client.get("/simple-wagtail")
54 | assert response.status_code == 200
55 | assertInHTML(
56 | '',
57 | response.content.decode(),
58 | )
59 |
60 |
61 | @pytest.mark.parametrize(
62 | "script_name", ["plausible.js", "plausible.hash.js", "plausible.hash.compat.js"]
63 | )
64 | def test_validates_script_name(script_name):
65 | PlausibleScriptNameValidator()(script_name)
66 |
67 |
68 | @pytest.mark.parametrize("script_name", ["/js/plausible.js", "left-pad.js"])
69 | def test_invalid_script_name(script_name):
70 | with pytest.raises(ValidationError):
71 | PlausibleScriptNameValidator()(script_name)
72 |
73 |
74 | def test_app_label():
75 | assert PlausibleSettings._meta.app_label == "wagtailplausible"
76 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # django-plausible
2 |
3 | 
4 | 
5 | 
6 | 
7 | 
8 |
9 |
10 | Django module to provide easy [Plausible](https://plausible.io/) integration, with [Wagtail](https://wagtail.io/) support.
11 |
12 | ## Installation
13 |
14 | ```
15 | pip install django-plausible
16 | ```
17 |
18 | Then simply add `plausible` to `INSTALLED_APPS`.
19 |
20 | ## Usage
21 |
22 | `django-plausible` provides a `plausible` template tag, which can be used to output the required [script tag](https://plausible.io/docs/plausible-script) for Plausible.
23 |
24 | ```html
25 | {% load plausible %}
26 |
27 | {% plausible %}
28 | ```
29 |
30 | Will result in:
31 |
32 | ```html
33 |
34 | ```
35 |
36 | ### Configuration
37 |
38 | Configuration can be changed either in `settings.py`, or when calling the `plausible` template tag:
39 |
40 | - `PLAUSIBLE_DOMAIN`: The domain Plausible is running on (defaults to `plausible.io`)
41 | - `PLAUSIBLE_SCRIPT_NAME`: The name of the script to use (defaults to `plausible.js`). See [script extensions](https://plausible.io/docs/script-extensions) for available options.
42 |
43 | These settings will affect all calls to the `plausible` template tag. To override it at call time, you can also pass them into the template tag:
44 |
45 | ```
46 | {% plausible plausible_domain="my-plausible.com" script_name="plausible.hash.js" %}
47 | ```
48 |
49 | By default, the domain (`data-domain`) used will be based on the request's hostname (using [`request.get_host()`](https://docs.djangoproject.com/en/dev/ref/request-response/#django.http.HttpRequest.get_host)). To override this, pass `site_domain` to the template tag.
50 |
51 | If the ["compat" script](https://plausible.io/docs/script-extensions#plausiblecompatjs) is used, `django-plausible` will automatically add the required `id` to the `script` tag. It is excluded by default to help hide Plausible's presence.
52 |
53 | ## Usage with Wagtail
54 |
55 | Additionally, `django-plausible` provides an (optional) deep integration with [Wagtail](https://wagtail.io), allowing configuration through the Wagtail admin. To enable this, additionally add `plausible.contrib.wagtail` to `INSTALLED_APPS`.
56 |
57 | Configuration is done through the "Plausible Analytics" [setting](https://docs.wagtail.io/en/stable/reference/contrib/settings.html#settings):
58 |
59 | - `site_domain`: the value for `data-domain`. If left blank (the default), the request's hostname will be used (as above), **not** the site hostname.
60 | - `plausible_domain`: The domain Plausible is running on (as above)
61 | - `script_name`: The name of the script to use (as above)
62 |
63 | To access the template tag, load `plausible_wagtail`, rather than `plausible`. The template tag itself is still `plausible`. Note that unlike the Django variant, the Wagtail template tag doesn't allow options to be passed.
64 |
65 | ```html
66 | {% load plausible_wagtail %}
67 |
68 | {% plausible %}
69 | ```
70 |
--------------------------------------------------------------------------------
/tests/test_django.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from django.template import RequestContext, Template
3 | from pytest_django.asserts import assertInHTML
4 |
5 |
6 | def _render_string(template, context=None) -> str:
7 | return Template(template).render(context)
8 |
9 |
10 | def test_simple_template_view(client):
11 | response = client.get("/simple", HTTP_HOST="example.com")
12 | assert response.status_code == 200
13 | assertInHTML(
14 | '',
15 | response.content.decode(),
16 | )
17 |
18 |
19 | def test_custom_hostname(rf):
20 | request = rf.get("/", HTTP_HOST="example.com")
21 | rendered = _render_string(
22 | '{% load plausible %}{% plausible site_domain="custom.com" %}',
23 | context=RequestContext(request),
24 | )
25 | assertInHTML(
26 | '',
27 | rendered,
28 | )
29 |
30 |
31 | def test_custom_domain(rf):
32 | request = rf.get("/")
33 | rendered = _render_string(
34 | '{% load plausible %}{% plausible plausible_domain="my-plausible.com" %}',
35 | context=RequestContext(request),
36 | )
37 | assertInHTML(
38 | '',
39 | rendered,
40 | )
41 |
42 |
43 | def test_custom_script_name(rf):
44 | request = rf.get("/")
45 | rendered = _render_string(
46 | '{% load plausible %}{% plausible script_name="plausible.hash.js" %}',
47 | context=RequestContext(request),
48 | )
49 | assertInHTML(
50 | '',
51 | rendered,
52 | )
53 |
54 |
55 | def test_compat_script_has_id(rf):
56 | request = rf.get("/")
57 | rendered = _render_string(
58 | '{% load plausible %}{% plausible script_name="plausible.compat.js" %}',
59 | context=RequestContext(request),
60 | )
61 | assertInHTML(
62 | '',
63 | rendered,
64 | )
65 |
66 |
67 | def test_custom_domain_from_settings(settings, rf):
68 | settings.PLAUSIBLE_DOMAIN = "my-plausible.com"
69 | request = rf.get("/")
70 | rendered = _render_string(
71 | "{% load plausible %}{% plausible %}",
72 | context=RequestContext(request),
73 | )
74 | assertInHTML(
75 | '',
76 | rendered,
77 | )
78 |
79 |
80 | def test_custom_script_name_from_settings(settings, rf):
81 | settings.PLAUSIBLE_SCRIPT_NAME = "plausible.hash.js"
82 | request = rf.get("/")
83 | rendered = _render_string(
84 | "{% load plausible %}{% plausible %}",
85 | context=RequestContext(request),
86 | )
87 | assertInHTML(
88 | '',
89 | rendered,
90 | )
91 |
92 |
93 | def test_invalid_script_name(settings, rf):
94 | settings.PLAUSIBLE_SCRIPT_NAME = "left-pad.js"
95 | request = rf.get("/")
96 | with pytest.raises(ValueError):
97 | _render_string(
98 | "{% load plausible %}{% plausible %}",
99 | context=RequestContext(request),
100 | )
101 |
--------------------------------------------------------------------------------