├── .nvmrc ├── tests ├── __init__.py ├── site │ ├── __init__.py │ ├── pages │ │ ├── __init__.py │ │ ├── migrations │ │ │ ├── __init__.py │ │ │ └── 0001_initial.py │ │ └── models.py │ ├── templates │ │ ├── pages │ │ │ ├── content_page.html │ │ │ └── taggable_content_page.html │ │ ├── 404.html │ │ ├── 500.html │ │ └── base.html │ └── urls.py ├── unit │ ├── __init__.py │ ├── test_widgets.py │ ├── test_context_processors.py │ ├── test_forms.py │ ├── test_views.py │ ├── test_decorators.py │ ├── test_consent.py │ ├── test_wagtail_hooks.py │ └── test_factories.py ├── factories │ ├── __init__.py │ ├── locale.py │ ├── constant.py │ ├── variable.py │ ├── cookie_declaration.py │ ├── site.py │ ├── trigger.py │ ├── page.py │ └── tag.py ├── utils.py ├── conftest.py ├── fixtures.py └── settings.py ├── sandbox ├── sandbox │ ├── __init__.py │ ├── apps │ │ ├── __init__.py │ │ └── home │ │ │ ├── __init__.py │ │ │ ├── migrations │ │ │ ├── __init__.py │ │ │ └── 0001_initial.py │ │ │ ├── templates │ │ │ └── home │ │ │ │ └── home_page.html │ │ │ └── models.py │ ├── static │ │ ├── js │ │ │ ├── sandbox.js │ │ │ └── dummy.js │ │ └── css │ │ │ ├── dummy.css │ │ │ └── sandbox.css │ ├── templates │ │ ├── dummy.html │ │ ├── 404.html │ │ ├── 500.html │ │ └── base.html │ ├── wsgi.py │ └── urls.py ├── requirements.txt ├── manage.py └── exampledata │ ├── users.json │ ├── additional_tags.json │ ├── cms.json │ └── default_tags.json ├── src └── wagtail_tag_manager │ ├── migrations │ ├── __init__.py │ ├── 0017_auto_20201028_0905.py │ ├── 0005_auto_20181227_1123.py │ ├── 0007_auto_20190130_1435.py │ ├── 0016_auto_20200210_1121.py │ ├── 0011_auto_20190501_1049.py │ ├── 0004_auto_20181206_1530.py │ ├── 0003_auto_20181206_1108.py │ ├── 0015_cookieconsent.py │ ├── 0012_auto_20190501_1118.py │ ├── 0008_auto_load_rename.py │ ├── 0009_auto_20190401_1550.py │ ├── 0010_auto_20190403_0639.py │ ├── 0014_auto_20190911_1116.py │ ├── 0019_cookieconsentsettings.py │ ├── 0006_cookiebarsettings.py │ ├── 0002_auto_20181111_1854.py │ ├── 0018_alter_constant_id_alter_cookiebarsettings_id_and_more.py │ ├── 0013_auto_20190506_0705.py │ └── 0001_initial.py │ ├── templatetags │ └── __init__.py │ ├── apps.py │ ├── static │ └── wagtail_tag_manager │ │ ├── checkbox_select_multiple.bundle.js │ │ ├── wtm.bundle.js.LICENSE.txt │ │ ├── index.bundle.css │ │ ├── checkbox_select_multiple.bundle.css │ │ ├── sourcemaps │ │ ├── index.bundle.css.map │ │ ├── checkbox_select_multiple.bundle.css.map │ │ ├── checkbox_select_multiple.bundle.js.map │ │ ├── trigger_form_view.bundle.js.map │ │ ├── variable_form_view.bundle.js.map │ │ ├── index.bundle.js.map │ │ └── tag_form_view.bundle.js.map │ │ ├── trigger_form_view.bundle.js │ │ ├── variable_form_view.bundle.js │ │ ├── index.bundle.js │ │ └── tag_form_view.bundle.js │ ├── __init__.py │ ├── templates │ ├── wagtail_tag_manager │ │ ├── admin │ │ │ ├── tag_index.html │ │ │ ├── constant_index.html │ │ │ ├── trigger_index.html │ │ │ ├── variable_index.html │ │ │ ├── index.html │ │ │ └── cookie_declaration_index.html │ │ ├── templatetags │ │ │ ├── instant_tags.html │ │ │ ├── manage_form.html │ │ │ ├── lazy_manager.html │ │ │ ├── tag_table.html │ │ │ ├── declaration_table.html │ │ │ └── cookie_bar.html │ │ └── manage.html │ ├── admin │ │ └── widgets │ │ │ └── codearea.html │ └── reports │ │ └── cookie_consent_report.html │ ├── models │ ├── __init__.py │ ├── constants.py │ ├── others.py │ └── variables.py │ ├── context_processors.py │ ├── urls.py │ ├── widgets.py │ ├── forms.py │ ├── decorators.py │ ├── managers.py │ ├── options.py │ ├── utils.py │ ├── mixins.py │ ├── endpoints.py │ ├── settings.py │ └── middleware.py ├── .prettierrc ├── .coveragerc ├── frontend ├── admin │ ├── widgets │ │ ├── checkbox_select_multiple.ts │ │ ├── checkbox_select_multiple.scss │ │ ├── codearea.scss │ │ └── codearea.ts │ ├── index.scss │ ├── trigger_form_view.ts │ ├── index.ts │ ├── variable_form_view.ts │ └── tag_form_view.ts ├── client │ ├── wtm.ts │ └── components │ │ ├── cookie_bar.ts │ │ └── tag_manager.ts └── types.d.ts ├── screenshots ├── screenshot.png ├── trigger-admin.png ├── tag-mixin-admin.png ├── cookie-bar-with-form.png ├── summary-panels-admin.png ├── custom-variables-admin.png └── cookie-bar-with-form-and-details.png ├── MANIFEST.in ├── cypress ├── fixtures │ └── example.json ├── plugins │ └── index.js ├── support │ ├── e2e.js │ └── commands.js └── e2e │ ├── sandbox.js │ ├── cookie_consent.js │ ├── manage_page.js │ └── home_page.js ├── .babelrc ├── .codacy.yml ├── tsconfig.json ├── .gitignore ├── .gitpod.yml ├── mypy.ini ├── cypress.config.ts ├── setup.cfg ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── python-publish.yml │ ├── codeql-analysis.yml │ └── cypress.yml ├── tox.ini ├── LICENSE ├── package.json ├── setup.py ├── webpack.config.js ├── Makefile ├── .circleci └── config.yml └── CODE_OF_CONDUCT.md /.nvmrc: -------------------------------------------------------------------------------- 1 | v16 2 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/site/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sandbox/sandbox/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/factories/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/site/pages/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sandbox/sandbox/apps/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /sandbox/sandbox/apps/home/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sandbox/sandbox/static/js/sandbox.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/site/pages/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sandbox/sandbox/apps/home/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100 3 | } 4 | -------------------------------------------------------------------------------- /sandbox/sandbox/static/js/dummy.js: -------------------------------------------------------------------------------- 1 | console.log("dummy.js"); 2 | -------------------------------------------------------------------------------- /tests/site/templates/pages/content_page.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | src/**/migrations/*.py 4 | tests/* 5 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/apps.py: -------------------------------------------------------------------------------- 1 | from .config import WagtailTagManagerConfig 2 | -------------------------------------------------------------------------------- /tests/site/templates/pages/taggable_content_page.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | -------------------------------------------------------------------------------- /sandbox/sandbox/static/css/dummy.css: -------------------------------------------------------------------------------- 1 | body::after { 2 | content: "dummy.css"; 3 | } 4 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/static/wagtail_tag_manager/checkbox_select_multiple.bundle.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/admin/widgets/checkbox_select_multiple.ts: -------------------------------------------------------------------------------- 1 | import "./checkbox_select_multiple.scss"; 2 | -------------------------------------------------------------------------------- /sandbox/sandbox/templates/dummy.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /sandbox/requirements.txt: -------------------------------------------------------------------------------- 1 | Django>=5.0,<5.1 2 | wagtail>=6.0,<6.1 3 | django-debug-toolbar 4 | -e .[docs,test] 5 | -------------------------------------------------------------------------------- /sandbox/sandbox/static/css/sandbox.css: -------------------------------------------------------------------------------- 1 | .container { 2 | margin-top: 30px; 3 | margin-bottom: 30px; 4 | } 5 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = "wagtail_tag_manager.config.WagtailTagManagerConfig" 2 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/static/wagtail_tag_manager/wtm.bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! js-cookie v3.0.5 | MIT */ 2 | -------------------------------------------------------------------------------- /screenshots/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jberghoef/wagtail-tag-manager/HEAD/screenshots/screenshot.png -------------------------------------------------------------------------------- /screenshots/trigger-admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jberghoef/wagtail-tag-manager/HEAD/screenshots/trigger-admin.png -------------------------------------------------------------------------------- /screenshots/tag-mixin-admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jberghoef/wagtail-tag-manager/HEAD/screenshots/tag-mixin-admin.png -------------------------------------------------------------------------------- /src/wagtail_tag_manager/templates/wagtail_tag_manager/admin/tag_index.html: -------------------------------------------------------------------------------- 1 | {% extends "wagtail_tag_manager/admin/index.html" %} 2 | -------------------------------------------------------------------------------- /screenshots/cookie-bar-with-form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jberghoef/wagtail-tag-manager/HEAD/screenshots/cookie-bar-with-form.png -------------------------------------------------------------------------------- /screenshots/summary-panels-admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jberghoef/wagtail-tag-manager/HEAD/screenshots/summary-panels-admin.png -------------------------------------------------------------------------------- /src/wagtail_tag_manager/templates/wagtail_tag_manager/admin/constant_index.html: -------------------------------------------------------------------------------- 1 | {% extends "wagtail_tag_manager/admin/index.html" %} 2 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/templates/wagtail_tag_manager/admin/trigger_index.html: -------------------------------------------------------------------------------- 1 | {% extends "wagtail_tag_manager/admin/index.html" %} 2 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/templates/wagtail_tag_manager/admin/variable_index.html: -------------------------------------------------------------------------------- 1 | {% extends "wagtail_tag_manager/admin/index.html" %} 2 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/templates/wagtail_tag_manager/templatetags/instant_tags.html: -------------------------------------------------------------------------------- 1 | {% for tag in tags %}{{ tag|safe }}{% endfor %} 2 | -------------------------------------------------------------------------------- /screenshots/custom-variables-admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jberghoef/wagtail-tag-manager/HEAD/screenshots/custom-variables-admin.png -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | 3 | recursive-include src * 4 | 5 | recursive-exclude src __pycache__ 6 | recursive-exclude src *.py[co] 7 | -------------------------------------------------------------------------------- /screenshots/cookie-bar-with-form-and-details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jberghoef/wagtail-tag-manager/HEAD/screenshots/cookie-bar-with-form-and-details.png -------------------------------------------------------------------------------- /src/wagtail_tag_manager/static/wagtail_tag_manager/index.bundle.css: -------------------------------------------------------------------------------- 1 | #wtm_help_block{padding-right:6em}#wtm_help_block .close-link{position:absolute;right:2em;top:1em} 2 | 3 | -------------------------------------------------------------------------------- /frontend/admin/index.scss: -------------------------------------------------------------------------------- 1 | #wtm_help_block { 2 | padding-right: 6em; 3 | 4 | .close-link { 5 | position: absolute; 6 | right: 2em; 7 | top: 1em; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["@babel/preset-env", { 3 | "useBuiltIns": "usage" 4 | }]], 5 | "plugins": [ 6 | [ 7 | "@babel/plugin-proposal-object-rest-spread" 8 | ] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .tags import * # noqa 2 | from .others import * # noqa 3 | from .triggers import * # noqa 4 | from .constants import * # noqa 5 | from .variables import * # noqa 6 | -------------------------------------------------------------------------------- /.codacy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | exclude_paths: 3 | - '*.css' 4 | - '*.scss' 5 | - '*.json' 6 | - '*.md' 7 | - '**/*.bundle.*' 8 | - 'tests/**/*' 9 | - '**/tests/**' 10 | - 'migrations/**/*' 11 | - '**/migrations/**' 12 | -------------------------------------------------------------------------------- /tests/factories/locale.py: -------------------------------------------------------------------------------- 1 | import factory 2 | from wagtail.models import Locale 3 | 4 | 5 | class LocaleFactory(factory.django.DjangoModelFactory): 6 | language_code = "en" 7 | 8 | class Meta: 9 | model = Locale 10 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | from django.template import engines 2 | 3 | 4 | def render_template(value, **context): 5 | template = engines["django"].from_string(value) 6 | request = context.pop("request", None) 7 | return template.render(context, request) 8 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/context_processors.py: -------------------------------------------------------------------------------- 1 | from wagtail_tag_manager.strategy import TagStrategy 2 | 3 | 4 | def consent_state(request): 5 | strategy = TagStrategy(request=request) 6 | return {"wtm_consent_state": getattr(strategy, "cookie_state", {})} 7 | -------------------------------------------------------------------------------- /tests/site/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block body_class %}template-404{% endblock %} 4 | 5 | {% block content %} 6 |

Page not found

7 | 8 |

Sorry, this page could not be found.

9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /sandbox/sandbox/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block body_class %}template-404{% endblock %} 4 | 5 | {% block content %} 6 |

Page not found

7 | 8 |

Sorry, this page could not be found.

9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/templates/wagtail_tag_manager/templatetags/manage_form.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 |
4 | {% csrf_token %} 5 | {{ form.as_p }} 6 | 7 |
8 | -------------------------------------------------------------------------------- /tests/factories/constant.py: -------------------------------------------------------------------------------- 1 | import factory 2 | 3 | from wagtail_tag_manager import models 4 | 5 | 6 | class ConstantFactory(factory.django.DjangoModelFactory): 7 | name = "Constant" 8 | key = "key" 9 | value = "value" 10 | 11 | class Meta: 12 | model = models.Constant 13 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/templates/admin/widgets/codearea.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /tests/unit/test_widgets.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wagtail_tag_manager.widgets import VariableSelect 4 | from wagtail_tag_manager.decorators import get_variables 5 | 6 | 7 | @pytest.mark.django_db 8 | def test_widget(): 9 | vs = VariableSelect() 10 | assert len(vs.choices) == len(get_variables()) 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "sourceMap": true, 5 | "noImplicitAny": true, 6 | "module": "es6", 7 | "target": "es5", 8 | "jsx": "react", 9 | "allowJs": true, 10 | "allowSyntheticDefaultImports": true 11 | }, 12 | "exclude": ["node_modules"] 13 | } 14 | -------------------------------------------------------------------------------- /tests/factories/variable.py: -------------------------------------------------------------------------------- 1 | import factory 2 | 3 | from wagtail_tag_manager import models 4 | 5 | 6 | class VariableFactory(factory.django.DjangoModelFactory): 7 | name = "Variable" 8 | key = "key" 9 | variable_type = "_cookie+" 10 | value = "wtm" 11 | 12 | class Meta: 13 | model = models.Variable 14 | -------------------------------------------------------------------------------- /frontend/admin/widgets/checkbox_select_multiple.scss: -------------------------------------------------------------------------------- 1 | .w-field--horizontal_checkbox_select_multiple .w-field__input > div { 2 | display: flex; 3 | flex-wrap: wrap; 4 | 5 | div { 6 | width: calc(100% / 3); 7 | min-width: 200px; 8 | margin-bottom: 6px; 9 | 10 | label { 11 | width: 100%; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /sandbox/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import absolute_import, unicode_literals 3 | 4 | import os 5 | import sys 6 | 7 | if __name__ == "__main__": 8 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sandbox.settings") 9 | 10 | from django.core.management import execute_from_command_line 11 | 12 | execute_from_command_line(sys.argv) 13 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/static/wagtail_tag_manager/checkbox_select_multiple.bundle.css: -------------------------------------------------------------------------------- 1 | .w-field--horizontal_checkbox_select_multiple .w-field__input>div{display:flex;flex-wrap:wrap}.w-field--horizontal_checkbox_select_multiple .w-field__input>div div{width:calc(100% / 3);min-width:200px;margin-bottom:6px}.w-field--horizontal_checkbox_select_multiple .w-field__input>div div label{width:100%} 2 | 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.pyc 3 | *.swo 4 | *.python-version 5 | *.coverage 6 | .coverage.* 7 | 8 | *.egg-info/ 9 | 10 | *_cache/ 11 | .cache/ 12 | .idea/ 13 | .tox/ 14 | .vscode/ 15 | .nova/ 16 | 17 | cypress/screenshots/* 18 | cypress/videos/* 19 | 20 | build 21 | dist 22 | 23 | htmlcov/ 24 | docs/_build 25 | coverage.xml 26 | db.sqlite3 27 | 28 | node_modules 29 | *.log 30 | 31 | .DS_Store 32 | -------------------------------------------------------------------------------- /sandbox/sandbox/apps/home/templates/home/home_page.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% load wagtailcore_tags %} 4 | 5 | {% block body_class %}template-homepage{% endblock %} 6 | 7 | {% block content %} 8 |

{{ self.title }}

9 |
10 | 11 | {{ self.content|richtext }} 12 | 13 | Manage privacy settings 14 | 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/static/wagtail_tag_manager/sourcemaps/index.bundle.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.bundle.css","mappings":"AAAA,gBACE,iBAAkB,CADpB,4BAII,iBAAkB,CAClB,SAAU,CACV,OAAQ","sources":["webpack://wagtail-tag-manager/./frontend/admin/index.scss"],"sourcesContent":["#wtm_help_block {\n padding-right: 6em;\n\n .close-link {\n position: absolute;\n right: 2em;\n top: 1em;\n }\n}\n"],"names":[],"sourceRoot":""} -------------------------------------------------------------------------------- /src/wagtail_tag_manager/templates/wagtail_tag_manager/templatetags/lazy_manager.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | {% if include_style %} 4 | 5 | {% endif %} 6 | 7 | 10 | 11 | {% if include_script %} 12 | 13 | {% endif %} 14 | -------------------------------------------------------------------------------- /frontend/client/wtm.ts: -------------------------------------------------------------------------------- 1 | import Cookies from "js-cookie"; 2 | 3 | import TagManager from "./components/tag_manager"; 4 | 5 | import type { Cookie } from "../types"; 6 | 7 | (function () { 8 | if (window.wtm === undefined) window.wtm = {}; 9 | (window as any).wtm.consent = () => { 10 | const cookie = Cookies.get("wtm"); 11 | return JSON.parse(atob(decodeURIComponent(cookie))) as Cookie; 12 | }; 13 | 14 | new TagManager(); 15 | })(); 16 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytest_plugins = ["tests.fixtures"] 4 | 5 | 6 | @pytest.fixture(scope="session") 7 | def django_db_setup(django_db_setup, django_db_blocker): 8 | from wagtail.models import Page, Site 9 | 10 | with django_db_blocker.unblock(): 11 | # Remove some initial data that is brought by the tests.site module 12 | Site.objects.all().delete() 13 | Page.objects.all().exclude(depth=1).delete() 14 | -------------------------------------------------------------------------------- /tests/factories/cookie_declaration.py: -------------------------------------------------------------------------------- 1 | import factory 2 | 3 | from wagtail_tag_manager.models import CookieDeclaration 4 | 5 | 6 | class CookieDeclarationFactory(factory.django.DjangoModelFactory): 7 | cookie_type = "necessary" 8 | name = "Necessary cookie" 9 | domain = "localhost" 10 | purpose = "Lorem ipsum" 11 | duration = 1 12 | security = CookieDeclaration.SECURE_COOKIE 13 | 14 | class Meta: 15 | model = CookieDeclaration 16 | -------------------------------------------------------------------------------- /tests/factories/site.py: -------------------------------------------------------------------------------- 1 | import factory 2 | from wagtail.models import Site 3 | 4 | from tests.factories.page import ContentPageFactory 5 | 6 | 7 | class SiteFactory(factory.django.DjangoModelFactory): 8 | hostname = "localhost" 9 | port = factory.Sequence(lambda n: 81 + n) 10 | site_name = "Test site" 11 | root_page = factory.SubFactory(ContentPageFactory, parent=None) 12 | is_default_site = False 13 | 14 | class Meta: 15 | model = Site 16 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - init: make clean && make requirements && pip install -r sandbox/requirements.txt 3 | command: sandbox/manage.py migrate && sandbox/manage.py loaddata sandbox/exampledata/users.json && sandbox/manage.py loaddata sandbox/exampledata/cms.json && sandbox/manage.py loaddata sandbox/exampledata/default_tags.json && sandbox/manage.py loaddata sandbox/exampledata/additional_tags.json && sandbox/manage.py runserver 4 | ports: 5 | - port: 8000 6 | onOpen: open-preview 7 | -------------------------------------------------------------------------------- /tests/factories/trigger.py: -------------------------------------------------------------------------------- 1 | import factory 2 | 3 | from wagtail_tag_manager import models 4 | 5 | 6 | class TriggerFactory(factory.django.DjangoModelFactory): 7 | name = "Trigger" 8 | slug = "trigger" 9 | 10 | class Meta: 11 | model = models.Trigger 12 | 13 | 14 | class TriggerConditionFactory(factory.django.DjangoModelFactory): 15 | variable = "navigation_path" 16 | value = "/" 17 | 18 | class Meta: 19 | model = models.TriggerCondition 20 | -------------------------------------------------------------------------------- /tests/unit/test_context_processors.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wagtail_tag_manager.context_processors import consent_state 4 | 5 | 6 | @pytest.mark.django_db 7 | def test_consent_state(rf, site): 8 | request = rf.get(site.root_page.url) 9 | assert consent_state(request) == { 10 | "wtm_consent_state": { 11 | "preferences": True, 12 | "statistics": True, 13 | "necessary": True, 14 | "marketing": False, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from wagtail_tag_manager.views import ConfigView, ManageView, VariableView 4 | from wagtail_tag_manager.endpoints import lazy_endpoint 5 | 6 | app_name = "wtm" 7 | 8 | urlpatterns = [ 9 | path("manage/", ManageView.as_view(), name="manage"), 10 | path("config/", ConfigView.as_view(), name="config"), 11 | path("lazy/", lazy_endpoint, name="lazy"), 12 | path("variables/", VariableView.as_view(), name="variables"), 13 | ] 14 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/templates/wagtail_tag_manager/admin/index.html: -------------------------------------------------------------------------------- 1 | {% extends "modeladmin/index.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block content_main %} 6 | {% if help_text %} 7 | 12 | {% endif %} 13 | {{ block.super }} 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /frontend/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface WTMWindow extends Window { 2 | wtm: { 3 | config_url: string; 4 | lazy_url: string; 5 | }; 6 | } 7 | 8 | export interface Tag { 9 | name: string; 10 | attributes: { 11 | [s: string]: string; 12 | }; 13 | string: string; 14 | } 15 | 16 | export interface Cookie { 17 | meta: Meta; 18 | state: { 19 | [s: string]: string; 20 | }; 21 | } 22 | 23 | export interface Meta { 24 | id?: string; 25 | set_timestamp?: number; 26 | refresh_timestamp?: number; 27 | } 28 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | python_version = 3.6 3 | 4 | 5 | [mypy-bs4.*] 6 | ignore_missing_imports = True 7 | 8 | [mypy-django.*] 9 | ignore_missing_imports = True 10 | 11 | [mypy-modelcluster.*] 12 | ignore_missing_imports = True 13 | 14 | [mypy-wagtail.*] 15 | ignore_missing_imports = True 16 | 17 | [mypy-selenium.*] 18 | ignore_missing_imports = True 19 | 20 | 21 | [mypy-pytest.*] 22 | ignore_missing_imports = True 23 | 24 | [mypy-factory.*] 25 | ignore_missing_imports = True 26 | 27 | [mypy-wagtail_factories.*] 28 | ignore_missing_imports = True -------------------------------------------------------------------------------- /sandbox/sandbox/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for sandbox project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/ 8 | """ 9 | 10 | from __future__ import absolute_import, unicode_literals 11 | 12 | import os 13 | 14 | from django.core.wsgi import get_wsgi_application 15 | 16 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sandbox.settings") 17 | 18 | application = get_wsgi_application() 19 | -------------------------------------------------------------------------------- /tests/factories/page.py: -------------------------------------------------------------------------------- 1 | import factory 2 | from django.utils.text import slugify 3 | from wagtail_factories.factories import PageFactory 4 | 5 | from tests.site.pages.models import ContentPage, TaggableContentPage 6 | 7 | 8 | class ContentPageFactory(PageFactory): 9 | title = "Test page" 10 | slug = factory.LazyAttribute(lambda obj: slugify(obj.title)) 11 | 12 | class Meta: 13 | model = ContentPage 14 | 15 | 16 | class TaggableContentPageFactory(ContentPageFactory): 17 | class Meta: 18 | model = TaggableContentPage 19 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/migrations/0017_auto_20201028_0905.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.1 on 2020-10-28 09:05 2 | 3 | from django.db import models, migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('wagtail_tag_manager', '0016_auto_20200210_1121'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='cookieconsent', 15 | name='location', 16 | field=models.URLField(editable=False, max_length=2048), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /tests/site/urls.py: -------------------------------------------------------------------------------- 1 | from wagtail import urls as wagtail_urls 2 | from django.urls import path, include 3 | from wagtail.admin import urls as wagtailadmin_urls 4 | from django.contrib import admin 5 | from wagtail.documents import urls as wagtaildocs_urls 6 | 7 | from wagtail_tag_manager import urls as wtm_urls 8 | 9 | urlpatterns = [ 10 | path("admin/", admin.site.urls), 11 | path("cms/", include(wagtailadmin_urls)), 12 | path("documents/", include(wagtaildocs_urls)), 13 | path("wtm/", include(wtm_urls)), 14 | path("", include(wagtail_urls)), 15 | ] 16 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/templates/wagtail_tag_manager/manage.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% load i18n wtm_tags %} 4 | 5 | {% block title %}{% trans "Manage your preferences" %}{% endblock title %} 6 | 7 | {% block content %} 8 | Back 9 |

{% trans "Privacy settings" %}

10 | 11 |

{% trans "Tags" %}

12 | {% wtm_tag_table %} 13 | 14 |

{% trans "Cookies" %}

15 | {% wtm_declaration_table %} 16 | 17 |

{% trans "Preferences" %}

18 | {% wtm_manage_form %} 19 | 20 | {% endblock content %} 21 | -------------------------------------------------------------------------------- /tests/site/pages/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from wagtail.fields import RichTextField 3 | from wagtail.models import Page 4 | from wagtail.admin.panels import FieldPanel 5 | 6 | from wagtail_tag_manager.mixins import TagMixin 7 | 8 | 9 | class ContentPage(Page): 10 | subtitle = models.CharField(max_length=255, blank=True, default="") 11 | body = RichTextField(blank=True, default="") 12 | 13 | content_panels = Page.content_panels + [FieldPanel("subtitle"), FieldPanel("body")] 14 | 15 | 16 | class TaggableContentPage(TagMixin, ContentPage): 17 | pass 18 | -------------------------------------------------------------------------------- /sandbox/exampledata/users.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "auth.user", 4 | "pk": 1, 5 | "fields": { 6 | "password": "pbkdf2_sha256$100000$DOLqAkZwtNNR$l7qJMBnT0LKa/z0adwb+JpZoAjWjJvB+vrhWFzfaKAM=", 7 | "last_login": null, 8 | "is_superuser": true, 9 | "username": "superuser", 10 | "first_name": "", 11 | "last_name": "", 12 | "email": "superuser@example.com", 13 | "is_staff": true, 14 | "is_active": true, 15 | "date_joined": "2018-05-26T16:30:37.322Z", 16 | "groups": [], 17 | "user_permissions": [] 18 | } 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/static/wagtail_tag_manager/sourcemaps/checkbox_select_multiple.bundle.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"checkbox_select_multiple.bundle.css","mappings":"AAAA,kEACE,YAAa,CACb,cAAe,CAFjB,sEAKI,oBAAqB,CACrB,eAAgB,CAChB,iBAAkB,CAPtB,4EAUM,UAAW","sources":["webpack://wagtail-tag-manager/./frontend/admin/widgets/checkbox_select_multiple.scss"],"sourcesContent":[".w-field--horizontal_checkbox_select_multiple .w-field__input > div {\n display: flex;\n flex-wrap: wrap;\n\n div {\n width: calc(100% / 3);\n min-width: 200px;\n margin-bottom: 6px;\n\n label {\n width: 100%;\n }\n }\n}\n"],"names":[],"sourceRoot":""} -------------------------------------------------------------------------------- /src/wagtail_tag_manager/migrations/0005_auto_20181227_1123.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.4 on 2018-12-27 16:23 2 | 3 | from django.db import models, migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('wagtail_tag_manager', '0004_auto_20181206_1530'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='trigger', 15 | name='pattern', 16 | field=models.CharField(help_text="The regex pattern to match the full url path with. Groups will be added to the included tag's context.", max_length=255), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress' 2 | 3 | export default defineConfig({ 4 | defaultCommandTimeout: 8000, 5 | requestTimeout: 10000, 6 | chromeWebSecurity: false, 7 | projectId: 'zprv4r', 8 | retries: { 9 | runMode: 2, 10 | openMode: 0, 11 | }, 12 | e2e: { 13 | // We've imported your old cypress plugins here. 14 | // You may want to clean this up later by importing these. 15 | setupNodeEvents(on, config) { 16 | return require('./cypress/plugins/index.js')(on, config) 17 | }, 18 | baseUrl: 'http://127.0.0.1:8000', 19 | specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}', 20 | }, 21 | }) 22 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 2.0.0 3 | commit = true 4 | tag = true 5 | tag_name = {new_version} 6 | 7 | [tool:pytest] 8 | DJANGO_SETTINGS_MODULE = tests.settings 9 | minversion = 3.0 10 | strict = true 11 | django_find_project = false 12 | testpaths = tests 13 | 14 | [isort] 15 | line_length = 80 16 | multi_line_output = 4 17 | length_sort = 1 18 | 19 | [flake8] 20 | ignore = E731, W503 21 | max-line-length = 120 22 | exclude = 23 | src/**/migrations/*.py 24 | 25 | [wheel] 26 | universal = 1 27 | 28 | [coverage] 29 | include = src/**/ 30 | omit = src/**/migrations/*.py 31 | 32 | [bumpversion:file:package.json] 33 | 34 | [bumpversion:file:setup.py] 35 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [jberghoef] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/migrations/0007_auto_20190130_1435.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.4 on 2019-01-30 14:35 2 | 3 | from django.db import models, migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('wagtail_tag_manager', '0006_cookiebarsettings'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='tag', 15 | name='tag_type', 16 | field=models.CharField(choices=[('functional', 'Functional'), ('analytical', 'Analytical'), ('traceable', 'Traceable')], default='functional', help_text='The purpose of this tag. Will decide if and when this tag is loaded on a per-user basis.', max_length=100), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/migrations/0016_auto_20200210_1121.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.3 on 2020-02-10 11:21 2 | 3 | from django.db import models, migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('wagtail_tag_manager', '0015_cookieconsent'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='cookiedeclaration', 15 | name='cookie_type', 16 | field=models.CharField(blank=True, choices=[('necessary', 'Necessary'), ('preferences', 'Preferences'), ('statistics', 'Statistics'), ('marketing', 'Marketing')], help_text='The type of functionality this cookie supports.', max_length=100, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/static/wagtail_tag_manager/trigger_form_view.bundle.js: -------------------------------------------------------------------------------- 1 | (()=>{var e=function(){function e(){this.triggerSelect=document.getElementById("id_trigger_type"),this.valueInput=document.getElementById("id_value"),this.initialize=this.initialize.bind(this),this.handleTriggerChange=this.handleTriggerChange.bind(this),this.triggerSelect.addEventListener("change",this.handleTriggerChange),this.initialize()}return e.prototype.initialize=function(){this.handleTriggerChange()},e.prototype.handleTriggerChange=function(e){void 0===e&&(e=null),"+"!==this.triggerSelect.options[this.triggerSelect.selectedIndex].value.slice(-1)?(this.valueInput.disabled=!0,this.valueInput.value=""):this.valueInput.disabled=!1},e}();document.addEventListener("DOMContentLoaded",(function(t){new e}))})(); -------------------------------------------------------------------------------- /tests/site/templates/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Internal server error 10 | 11 | 12 | 13 |

Internal server error

14 | 15 |

Sorry, there seems to be an error. Please try again soon.

16 | 17 | 18 | -------------------------------------------------------------------------------- /sandbox/sandbox/templates/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Internal server error 10 | 11 | 12 | 13 |

Internal server error

14 | 15 |

Sorry, there seems to be an error. Please try again soon.

16 | 17 | 18 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/migrations/0011_auto_20190501_1049.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.8 on 2019-05-01 10:49 2 | 3 | from django.db import models, migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('wagtail_tag_manager', '0010_auto_20190403_0639'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='variable', 15 | name='variable_type', 16 | field=models.CharField(choices=[('HTTP', (('_repath+', 'Path with regex'),)), ('Other', (('_cookie+', 'Cookie'),))], help_text='Path with regex: the path of the visited page after applying a regex search.
Cookie: the value of a cookie, when available.', max_length=255), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/static/wagtail_tag_manager/variable_form_view.bundle.js: -------------------------------------------------------------------------------- 1 | (()=>{var e=function(){function e(){this.variableSelect=document.getElementById("id_variable_type"),this.valueInput=document.getElementById("id_value"),this.initialize=this.initialize.bind(this),this.handleVariableChange=this.handleVariableChange.bind(this),this.variableSelect.addEventListener("change",this.handleVariableChange),this.initialize()}return e.prototype.initialize=function(){this.handleVariableChange()},e.prototype.handleVariableChange=function(e){void 0===e&&(e=null),"+"!==this.variableSelect.options[this.variableSelect.selectedIndex].value.slice(-1)?(this.valueInput.disabled=!0,this.valueInput.value=""):this.valueInput.disabled=!1},e}();document.addEventListener("DOMContentLoaded",(function(i){new e}))})(); -------------------------------------------------------------------------------- /sandbox/sandbox/apps/home/models.py: -------------------------------------------------------------------------------- 1 | from wagtail.admin.panels import FieldPanel 2 | from wagtail.fields import RichTextField 3 | from wagtail.models import Page 4 | 5 | from wagtail_tag_manager.decorators import register_variable 6 | from wagtail_tag_manager.mixins import TagMixin 7 | from wagtail_tag_manager.options import CustomVariable 8 | 9 | 10 | class HomePage(TagMixin, Page): 11 | content = RichTextField() 12 | 13 | content_panels = Page.content_panels + [ 14 | FieldPanel('content'), 15 | ] 16 | 17 | 18 | @register_variable 19 | class Variable(CustomVariable): 20 | name = "Custom variable" 21 | description = "Returns a custom value." 22 | key = "custom" 23 | 24 | def get_value(self, request): 25 | return "This is a custom variable." 26 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/templates/wagtail_tag_manager/templatetags/tag_table.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | {% regroup tags by get_tag_type_display as tag_list %} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {% for tag_type in tag_list %} 14 | 15 | 16 | 17 | {% for tag in tag_type.list %} 18 | 19 | 20 | 21 | 22 | {% endfor %} 23 | {% endfor %} 24 | 25 |
{% trans "Name" %}{% trans "Description" %}
{{ tag_type.grouper }}
{{ tag.name }}{{ tag.description }}
26 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/templates/wagtail_tag_manager/admin/cookie_declaration_index.html: -------------------------------------------------------------------------------- 1 | {% extends "wagtail_tag_manager/admin/index.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block header_extra %} 6 | {% if user_can_create %} 7 |
8 |
{% csrf_token %} 9 |
10 | {% if scanner_enabled %} 11 | 12 | {% endif %} 13 | {% include 'modeladmin/includes/button.html' with button=view.button_helper.add_button %} 14 |
15 |
16 |
17 | {% endif %} 18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************************** 3 | // This example plugins/index.js can be used to load plugins 4 | // 5 | // You can change the location of this file or turn off loading 6 | // the plugins file with the 'pluginsFile' configuration option. 7 | // 8 | // You can read more here: 9 | // https://on.cypress.io/plugins-guide 10 | // *********************************************************** 11 | 12 | // This function is called when a project is opened or re-opened (e.g. due to 13 | // the project's config changing) 14 | 15 | /** 16 | * @type {Cypress.PluginConfig} 17 | */ 18 | module.exports = (on, config) => { 19 | // `on` is used to hook into various events Cypress emits 20 | // `config` is the resolved Cypress config 21 | } 22 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/migrations/0004_auto_20181206_1530.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.3 on 2018-12-06 15:30 2 | 3 | from django.db import models, migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('wagtail_tag_manager', '0003_auto_20181206_1108'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='cookiedeclaration', 15 | name='duration_period', 16 | ), 17 | migrations.RemoveField( 18 | model_name='cookiedeclaration', 19 | name='duration_value', 20 | ), 21 | migrations.AddField( 22 | model_name='cookiedeclaration', 23 | name='duration', 24 | field=models.DurationField(blank=True, null=True), 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /frontend/client/components/cookie_bar.ts: -------------------------------------------------------------------------------- 1 | import "./cookie_bar.scss"; 2 | 3 | import TagManager from "./tag_manager"; 4 | 5 | export default class CookieBar { 6 | manager: TagManager; 7 | el: HTMLElement; 8 | 9 | constructor(manager: TagManager) { 10 | this.manager = manager; 11 | this.el = document.getElementById("wtm_cookie_bar"); 12 | 13 | this.initialize = this.initialize.bind(this); 14 | this.showCookieBar = this.showCookieBar.bind(this); 15 | this.hideCookieBar = this.hideCookieBar.bind(this); 16 | 17 | if (this.el) { 18 | this.initialize(); 19 | } 20 | } 21 | 22 | initialize() { 23 | this.showCookieBar(); 24 | } 25 | 26 | showCookieBar() { 27 | this.el.classList.remove("hidden"); 28 | } 29 | 30 | hideCookieBar() { 31 | this.el.classList.add("hidden"); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/widgets.py: -------------------------------------------------------------------------------- 1 | from django.forms import widgets 2 | 3 | from wagtail_tag_manager.decorators import get_variables 4 | 5 | 6 | class VariableSelect(widgets.Select): 7 | def __init__(self, attrs=None, choices=()): 8 | super().__init__(attrs) 9 | self.choices = [ 10 | (var.key, "%s - %s" % (var.name, var.description)) 11 | for var in get_variables() 12 | ] 13 | 14 | 15 | class Codearea(widgets.Textarea): 16 | template_name = "admin/widgets/codearea.html" 17 | 18 | class Media: 19 | css = {"all": ("wagtail_tag_manager/codearea.bundle.css",)} 20 | js = ("wagtail_tag_manager/codearea.bundle.js",) 21 | 22 | 23 | class HorizontalCheckboxSelectMultiple(widgets.CheckboxSelectMultiple): 24 | class Media: 25 | css = {"all": ("wagtail_tag_manager/checkbox_select_multiple.bundle.css",)} 26 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/migrations/0003_auto_20181206_1108.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.3 on 2018-12-06 11:08 2 | 3 | from django.db import models, migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('wagtail_tag_manager', '0002_auto_20181111_1854'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='cookiedeclaration', 15 | name='duration_period', 16 | field=models.CharField(blank=True, choices=[('session', 'Session'), ('seconds+', 'Second(s)'), ('minutes+', 'Minute(s)'), ('hours+', 'Hour(s)'), ('days+', 'Day(s)'), ('weeks+', 'Week(s)'), ('months+', 'Month(s)'), ('years+', 'Year(s)')], help_text='The period after which the cookie will expire.
Session: the cookie will expire when the browser is closed.', max_length=10, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/static/wagtail_tag_manager/index.bundle.js: -------------------------------------------------------------------------------- 1 | (()=>{"use strict";var e=function(){function e(){this.el=document.getElementById("wtm_help_block"),this.close_el=this.el.querySelector("a.close-link"),this.showHelpBlock=this.showHelpBlock.bind(this),this.hideHelpBlock=this.hideHelpBlock.bind(this),this.initialize=this.initialize.bind(this),this.close_el.addEventListener("click",this.hideHelpBlock),this.el&&this.initialize()}return e.prototype.initialize=function(){null===localStorage.getItem(this.identifier)&&this.showHelpBlock()},e.prototype.showHelpBlock=function(){this.el.style.display="block"},e.prototype.hideHelpBlock=function(){localStorage.setItem(this.identifier,"hidden"),this.el.style.display="none"},Object.defineProperty(e.prototype,"identifier",{get:function(){return"wtm_help_block:"+location.pathname},enumerable:!1,configurable:!0}),e}();document.addEventListener("DOMContentLoaded",(function(){new e}))})(); -------------------------------------------------------------------------------- /src/wagtail_tag_manager/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from wagtail_tag_manager.settings import ( 4 | SETTING_INITIAL, 5 | SETTING_REQUIRED, 6 | TagTypeSettings, 7 | ) 8 | 9 | 10 | class ConsentForm(forms.Form): 11 | def __init__(self, *args, **kwargs): 12 | super().__init__(*args, **kwargs) 13 | for tag_type, config in TagTypeSettings.all().items(): 14 | value = config.get("value") 15 | initial = value == SETTING_INITIAL or value == SETTING_REQUIRED 16 | 17 | if SETTING_INITIAL in kwargs: 18 | initial = kwargs.get(SETTING_INITIAL)[tag_type] 19 | 20 | self.fields[tag_type] = forms.BooleanField( 21 | label=config.get("verbose_name"), 22 | required=value == SETTING_REQUIRED, 23 | disabled=value == SETTING_REQUIRED, 24 | initial=initial, 25 | ) 26 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/migrations/0015_cookieconsent.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-11 13:16 2 | 3 | import uuid 4 | 5 | from django.db import models, migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('wagtail_tag_manager', '0014_auto_20190911_1116'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='CookieConsent', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('identifier', models.UUIDField(default=uuid.uuid4, editable=False)), 20 | ('consent_state', models.TextField(editable=False)), 21 | ('location', models.URLField(editable=False)), 22 | ('timestamp', models.DateTimeField(auto_now_add=True)), 23 | ], 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/migrations/0012_auto_20190501_1118.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.8 on 2019-05-01 11:18 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('wagtail_tag_manager', '0011_auto_20190501_1049'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='trigger', 15 | name='active', 16 | ), 17 | migrations.RemoveField( 18 | model_name='trigger', 19 | name='description', 20 | ), 21 | migrations.RemoveField( 22 | model_name='trigger', 23 | name='name', 24 | ), 25 | migrations.RemoveField( 26 | model_name='trigger', 27 | name='pattern', 28 | ), 29 | migrations.RemoveField( 30 | model_name='trigger', 31 | name='tags', 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/migrations/0008_auto_load_rename.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.7 on 2019-03-15 14:54 2 | 3 | from django.db import models, migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('wagtail_tag_manager', '0007_auto_20190130_1435'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='tag', 15 | options={'ordering': ['tag_loading', '-auto_load', 'tag_location', '-priority']}, 16 | ), 17 | migrations.RenameField( 18 | model_name='tag', 19 | old_name='active', 20 | new_name='auto_load', 21 | ), 22 | migrations.AlterField( 23 | model_name='tag', 24 | name='auto_load', 25 | field=models.BooleanField(default=True, help_text='Uncheck to disable this tag from being included automatically, or when using a trigger to include this tag.'), 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/decorators.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | 3 | from wagtail_tag_manager.options import CustomVariable 4 | 5 | _variables = {} 6 | 7 | 8 | def register_variable(cls=None): 9 | if inspect.isclass(cls) and not issubclass(cls, CustomVariable): 10 | raise ValueError("Class must subclass CustomVariable.") 11 | 12 | if cls is None: # pragma: no cover 13 | 14 | def decorator(cls): 15 | register_variable(cls) 16 | return cls 17 | 18 | return decorator 19 | 20 | _variables[cls.key] = cls 21 | 22 | 23 | def get_variables(lazy=None): 24 | """Return the variables function sorted by their order.""" 25 | variables = [] 26 | 27 | for key, cls in _variables.items(): 28 | if lazy is not None and cls.lazy_only is not lazy: 29 | continue 30 | 31 | if inspect.isclass(cls): 32 | variables.append(cls()) 33 | else: 34 | variables.append(cls) 35 | 36 | return variables 37 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: '3.x' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install setuptools wheel twine 25 | - name: Build and publish 26 | env: 27 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 28 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 29 | run: | 30 | python setup.py sdist bdist_wheel 31 | twine upload dist/* 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/templates/reports/cookie_consent_report.html: -------------------------------------------------------------------------------- 1 | {% extends 'wagtailadmin/reports/base_report.html' %} 2 | {% load i18n %} 3 | 4 | {% block results %} 5 | {% if object_list %} 6 | 7 | 8 | 9 | 12 | 15 | 18 | 21 | 22 | 23 | 24 | {% for cookie_consent in object_list %} 25 | 26 | 29 | 32 | 35 | 38 | 39 | {% endfor %} 40 | 41 |
10 | {% trans 'Identifier' %} 11 | 13 | {% trans 'Location' %} 14 | 16 | {% trans 'Consent state' %} 17 | 19 | {% trans 'Timestamp' %} 20 |
27 | {{cookie_consent.identifier}} 28 | 30 | {{cookie_consent.location}} 31 | 33 | {{cookie_consent.consent_state}} 34 | 36 | {{cookie_consent.timestamp}} 37 |
42 | {% else %} 43 |

{% trans "No consent has been registered yet." %}

44 | {% endif %} 45 | {% endblock %} 46 | -------------------------------------------------------------------------------- /tests/site/pages/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.1 on 2017-06-02 02:21 3 | from __future__ import unicode_literals 4 | 5 | import wagtail.fields 6 | import django.db.models.deletion 7 | from django.db import models, migrations 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | initial = True 13 | 14 | dependencies = [ 15 | ('wagtailcore', '0001_initial'), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='ContentPage', 21 | fields=[ 22 | ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')), # noqa: E501 23 | ('subtitle', models.CharField(blank=True, default='', max_length=255)), 24 | ('body', wagtail.fields.RichTextField(blank=True, default='')), 25 | ], 26 | options={ 27 | 'abstract': False, 28 | }, 29 | bases='wagtailcore.page', 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /frontend/admin/trigger_form_view.ts: -------------------------------------------------------------------------------- 1 | class TriggerFormView { 2 | triggerSelect: HTMLSelectElement; 3 | valueInput: HTMLInputElement; 4 | 5 | constructor() { 6 | this.triggerSelect = document.getElementById("id_trigger_type") as HTMLSelectElement; 7 | this.valueInput = document.getElementById("id_value") as HTMLInputElement; 8 | 9 | this.initialize = this.initialize.bind(this); 10 | this.handleTriggerChange = this.handleTriggerChange.bind(this); 11 | 12 | this.triggerSelect.addEventListener("change", this.handleTriggerChange); 13 | 14 | this.initialize(); 15 | } 16 | 17 | initialize() { 18 | this.handleTriggerChange(); 19 | } 20 | 21 | handleTriggerChange(event: Event = null) { 22 | const value = this.triggerSelect.options[this.triggerSelect.selectedIndex].value; 23 | 24 | if (value.slice(-1) !== "+") { 25 | this.valueInput.disabled = true; 26 | this.valueInput.value = ""; 27 | } else { 28 | this.valueInput.disabled = false; 29 | } 30 | } 31 | } 32 | 33 | document.addEventListener("DOMContentLoaded", (event) => { 34 | new TriggerFormView(); 35 | }); 36 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/static/wagtail_tag_manager/tag_form_view.bundle.js: -------------------------------------------------------------------------------- 1 | (()=>{var t=function(){function t(){this.loadSelect=document.getElementById("id_tag_loading"),this.locationSelect=document.getElementById("id_tag_location"),this.initialize=this.initialize.bind(this),this.handleLoadChange=this.handleLoadChange.bind(this),this.loadSelect.addEventListener("change",this.handleLoadChange),this.initialize()}return t.prototype.initialize=function(){this.handleLoadChange()},t.prototype.handleLoadChange=function(){"instant_load"!==this.loadSelect.options[this.loadSelect.selectedIndex].value?(this.locationSelect.disabled=!0,[].forEach.call(this.locationSelect,(function(t){"0_top_head"===t.value&&(t.selected=!0)})),this.hiddenInput=document.createElement("input"),this.hiddenInput.id=this.locationSelect.id,this.hiddenInput.name=this.locationSelect.name,this.hiddenInput.type="hidden",this.hiddenInput.value="0_top_head",this.locationSelect.parentNode.insertBefore(this.hiddenInput,this.locationSelect.parentNode.childNodes[0])):(this.locationSelect.disabled=!1,this.hiddenInput&&this.hiddenInput.remove())},t}();document.addEventListener("DOMContentLoaded",(function(){new t}))})(); -------------------------------------------------------------------------------- /src/wagtail_tag_manager/migrations/0009_auto_20190401_1550.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.8 on 2019-04-01 15:50 2 | 3 | from django.db import models, migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('wagtail_tag_manager', '0008_auto_load_rename'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='cookiedeclaration', 15 | name='cookie_type', 16 | field=models.CharField(blank=True, choices=[('functional', 'Functional'), ('analytical', 'Analytical'), ('continue', 'Continue'), ('traceable', 'Traceable')], help_text='The type of functionality this cookie supports.', max_length=10, null=True), 17 | ), 18 | migrations.AlterField( 19 | model_name='tag', 20 | name='tag_type', 21 | field=models.CharField(choices=[('functional', 'Functional'), ('analytical', 'Analytical'), ('continue', 'Continue'), ('traceable', 'Traceable')], default='functional', help_text='The purpose of this tag. Will decide if and when this tag is loaded on a per-user basis.', max_length=100), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/migrations/0010_auto_20190403_0639.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.8 on 2019-04-03 06:39 2 | 3 | from django.db import models, migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('wagtail_tag_manager', '0009_auto_20190401_1550'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='cookiedeclaration', 15 | name='cookie_type', 16 | field=models.CharField(blank=True, choices=[('functional', 'Functional'), ('analytical', 'Analytical'), ('delayed', 'Delayed'), ('traceable', 'Traceable')], help_text='The type of functionality this cookie supports.', max_length=10, null=True), 17 | ), 18 | migrations.AlterField( 19 | model_name='tag', 20 | name='tag_type', 21 | field=models.CharField(choices=[('functional', 'Functional'), ('analytical', 'Analytical'), ('delayed', 'Delayed'), ('traceable', 'Traceable')], default='functional', help_text='The purpose of this tag. Will decide if and when this tag is loaded on a per-user basis.', max_length=100), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /frontend/admin/index.ts: -------------------------------------------------------------------------------- 1 | import "./index.scss"; 2 | 3 | class IndexView { 4 | el: HTMLElement; 5 | close_el: HTMLAnchorElement; 6 | 7 | constructor() { 8 | this.el = document.getElementById("wtm_help_block"); 9 | this.close_el = this.el.querySelector("a.close-link"); 10 | 11 | this.showHelpBlock = this.showHelpBlock.bind(this); 12 | this.hideHelpBlock = this.hideHelpBlock.bind(this); 13 | this.initialize = this.initialize.bind(this); 14 | 15 | this.close_el.addEventListener("click", this.hideHelpBlock); 16 | 17 | if (this.el) { 18 | this.initialize(); 19 | } 20 | } 21 | 22 | initialize() { 23 | if (localStorage.getItem(this.identifier) === null) { 24 | this.showHelpBlock(); 25 | } 26 | } 27 | 28 | showHelpBlock() { 29 | this.el.style.display = "block"; 30 | } 31 | 32 | hideHelpBlock() { 33 | localStorage.setItem(this.identifier, "hidden"); 34 | this.el.style.display = "none"; 35 | } 36 | 37 | get identifier() { 38 | return "wtm_help_block:" + location.pathname; 39 | } 40 | } 41 | 42 | document.addEventListener("DOMContentLoaded", () => { 43 | new IndexView(); 44 | }); 45 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/migrations/0014_auto_20190911_1116.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-11 11:16 2 | 3 | from django.db import models, migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('wagtail_tag_manager', '0013_auto_20190506_0705'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='cookiedeclaration', 15 | name='cookie_type', 16 | field=models.CharField(blank=True, choices=[('necessary', 'Necessary'), ('preferences', 'Preferences'), ('statistics', 'Statistics'), ('marketing', 'Marketing')], help_text='The type of functionality this cookie supports.', max_length=10, null=True), 17 | ), 18 | migrations.AlterField( 19 | model_name='tag', 20 | name='tag_type', 21 | field=models.CharField(choices=[('necessary', 'Necessary'), ('preferences', 'Preferences'), ('statistics', 'Statistics'), ('marketing', 'Marketing')], default='necessary', help_text='The purpose of this tag. Will decide if and when this tag is loaded on a per-user basis.', max_length=100), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/managers.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class TagQuerySet(models.QuerySet): 5 | def auto_load(self): 6 | return self.filter(auto_load=True) 7 | 8 | def passive(self): 9 | return self.filter(auto_load=False) 10 | 11 | def instant(self): 12 | from wagtail_tag_manager.models import Tag 13 | 14 | return self.filter(tag_loading=Tag.INSTANT_LOAD) 15 | 16 | def lazy(self): 17 | from wagtail_tag_manager.models import Tag 18 | 19 | return self.filter(tag_loading=Tag.LAZY_LOAD) 20 | 21 | def sorted(self): 22 | from wagtail_tag_manager.models import Tag 23 | 24 | order = [*Tag.get_types(), None] 25 | return sorted(self, key=lambda x: order.index(x.tag_type)) 26 | 27 | 28 | class TriggerQuerySet(models.QuerySet): 29 | def active(self): 30 | return self.filter(active=True) 31 | 32 | 33 | class CookieDeclarationQuerySet(models.QuerySet): 34 | def sorted(self): 35 | from wagtail_tag_manager.models import Tag 36 | 37 | order = [*Tag.get_types(), None] 38 | return sorted(self, key=lambda x: order.index(x.cookie_type)) 39 | -------------------------------------------------------------------------------- /frontend/admin/variable_form_view.ts: -------------------------------------------------------------------------------- 1 | // TODO: Turn this into something reusable. 2 | 3 | class VariableFormView { 4 | variableSelect: HTMLSelectElement; 5 | valueInput: HTMLInputElement; 6 | 7 | constructor() { 8 | this.variableSelect = document.getElementById("id_variable_type") as HTMLSelectElement; 9 | this.valueInput = document.getElementById("id_value") as HTMLInputElement; 10 | 11 | this.initialize = this.initialize.bind(this); 12 | this.handleVariableChange = this.handleVariableChange.bind(this); 13 | 14 | this.variableSelect.addEventListener("change", this.handleVariableChange); 15 | 16 | this.initialize(); 17 | } 18 | 19 | initialize() { 20 | this.handleVariableChange(); 21 | } 22 | 23 | handleVariableChange(event: Event = null) { 24 | const value = this.variableSelect.options[this.variableSelect.selectedIndex].value; 25 | 26 | if (value.slice(-1) !== "+") { 27 | this.valueInput.disabled = true; 28 | this.valueInput.value = ""; 29 | } else { 30 | this.valueInput.disabled = false; 31 | } 32 | } 33 | } 34 | 35 | document.addEventListener("DOMContentLoaded", (event) => { 36 | new VariableFormView(); 37 | }); 38 | -------------------------------------------------------------------------------- /sandbox/exampledata/additional_tags.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "wagtail_tag_manager.tag", 4 | "pk": 13, 5 | "fields": { 6 | "name": "Thank You", 7 | "description": "This logs a \"thank you\" message using a constant.", 8 | "auto_load": true, 9 | "tag_type": "necessary", 10 | "tag_location": "3_bottom_body", 11 | "tag_loading": "instant_load", 12 | "content": "" 13 | } 14 | }, 15 | { 16 | "model": "wagtail_tag_manager.tag", 17 | "pk": 14, 18 | "fields": { 19 | "name": "A random number", 20 | "description": "This will log a random number.", 21 | "auto_load": true, 22 | "tag_type": "preferences", 23 | "tag_location": "0_top_head", 24 | "tag_loading": "lazy_load", 25 | "content": "" 26 | } 27 | }, 28 | { 29 | "model": "wagtail_tag_manager.constant", 30 | "pk": 1, 31 | "fields": { 32 | "name": "Thank You", 33 | "description": "This is a thank you message!", 34 | "key": "thnx", 35 | "value": "Thank you for using WTM!" 36 | } 37 | } 38 | ] 39 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/options.py: -------------------------------------------------------------------------------- 1 | import django 2 | 3 | __version__ = django.get_version() 4 | if __version__.startswith("2"): 5 | from django.utils.translation import ugettext_lazy as _ 6 | else: 7 | from django.utils.translation import gettext_lazy as _ 8 | 9 | 10 | class CustomVariable(object): 11 | name = "" 12 | description = "" 13 | key = "" 14 | group = _("Various") 15 | lazy_only = False 16 | 17 | def __init__(self, *args, **kwargs): 18 | for key, value in kwargs.items(): 19 | setattr(self, key, value) 20 | 21 | for field in ["name", "description", "key"]: 22 | if not getattr(self, field, None): 23 | raise ValueError( 24 | "A CustomVariable class has to provide a '{}' value.".format(field) 25 | ) 26 | 27 | def as_dict(self): 28 | return { 29 | "name": self.name, 30 | "description": self.description, 31 | "key": self.key, 32 | "group": self.group, 33 | "lazy_only": self.lazy_only, 34 | "variable_type": "custom", 35 | "value": "not available", 36 | } 37 | 38 | def get_value(self, request): 39 | return "" 40 | -------------------------------------------------------------------------------- /frontend/admin/widgets/codearea.scss: -------------------------------------------------------------------------------- 1 | @import "~codemirror/lib/codemirror.css"; 2 | 3 | .w-panel.code { 4 | .w-field--codearea { 5 | .w-field__input { 6 | & > div { 7 | display: flex; 8 | width: 100%; 9 | height: 280px; 10 | 11 | ul.panel { 12 | width: 200px; 13 | height: 100%; 14 | padding: 10px; 15 | overflow-x: hidden; 16 | overflow-y: auto; 17 | border: 1px solid var(--w-color-grey-150); 18 | border-right: unset; 19 | 20 | h3 { 21 | margin: 0.5em 0; 22 | } 23 | 24 | .listing { 25 | margin-bottom: 1em; 26 | 27 | li { 28 | padding: 0.5em 0; 29 | } 30 | } 31 | } 32 | } 33 | } 34 | 35 | .CodeMirror { 36 | height: 280px; 37 | min-height: 280px; 38 | width: 100%; 39 | padding: 1em 20px; 40 | background-color: var(--w-color-grey-50); 41 | border: 1px solid var(--w-color-grey-150); 42 | transition: background-color 0.2s ease; 43 | overflow: hidden; 44 | 45 | &-focused { 46 | border-color: var(--w-color-grey-200); 47 | outline: none; 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/migrations/0019_cookieconsentsettings.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-05-09 12:26 2 | 3 | import django.db.models.deletion 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('wagtailcore', '0066_collection_management_permissions'), 11 | ('wagtail_tag_manager', '0018_alter_constant_id_alter_cookiebarsettings_id_and_more'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='CookieConsentSettings', 17 | fields=[ 18 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('conditions_page', models.ForeignKey(blank=True, help_text='Set the page describing your privacy policy. Every time it changes, the consent given before will be invalidated.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailcore.page')), 20 | ('site', models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, to='wagtailcore.site')), 21 | ], 22 | options={ 23 | 'abstract': False, 24 | }, 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/migrations/0006_cookiebarsettings.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.4 on 2019-01-30 09:31 2 | 3 | import wagtail.fields 4 | import django.db.models.deletion 5 | from django.db import models, migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('wagtailcore', '0041_group_collection_permissions_verbose_name_plural'), 12 | ('wagtail_tag_manager', '0005_auto_20181227_1123'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='CookieBarSettings', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('title', models.CharField(blank=True, help_text='The title that should appear on the cookie bar. Leave empty for the default value.', max_length=50, null=True)), 21 | ('text', wagtail.fields.RichTextField(blank=True, help_text='The text that should appear on the cookie bar. Leave empty for the default value.', null=True)), 22 | ('site', models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, to='wagtailcore.Site')), 23 | ], 24 | options={ 25 | 'abstract': False, 26 | }, 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/templates/wagtail_tag_manager/templatetags/declaration_table.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | {% regroup declarations by get_cookie_type_display as declaration_list %} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {% for cookie_type in declaration_list %} 17 | 18 | 25 | 26 | {% for cookie in cookie_type.list %} 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | {% endfor %} 35 | {% endfor %} 36 | 37 |
{% trans "Name" %}{% trans "Domain" %}{% trans "Purpose" %}{% trans "Expiration" %}{% trans "Security" %}
19 | {% if cookie_type.grouper %} 20 | {{ cookie_type.grouper }} 21 | {% else %} 22 | {% trans "Unclassified" %} 23 | {% endif %} 24 |
{{ cookie.name }}{{ cookie.domain }}{{ cookie.purpose }}{{ cookie.expiration }}{{ cookie.get_security_display }}
38 | -------------------------------------------------------------------------------- /cypress/support/e2e.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import "./commands"; 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | 22 | Cypress.Cookies.debug(true, { verbose: false }); 23 | 24 | Cypress.on("uncaught:exception", (err, runnable) => { 25 | if (err.message.includes("fetch is not a function")) { 26 | return false; 27 | } 28 | }); 29 | 30 | before("login to admin", () => { 31 | cy.clearCookies(); 32 | cy.visit("/cms/login/"); 33 | cy.get("#id_username").type("superuser"); 34 | cy.get("#id_password").type("testing"); 35 | cy.get("button[type='submit']").click(); 36 | }); 37 | 38 | beforeEach("configure cookies", () => { 39 | window.localStorage.setItem("djdt.show", "false"); 40 | Cypress.Cookies.preserveOnce("csrftoken", "sessionid"); 41 | }); 42 | -------------------------------------------------------------------------------- /tests/site/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static wagtailuserbar wtm_tags %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | {% block title %} 13 | {% if self.seo_title %}{{ self.seo_title }}{% else %}{{ self.title }}{% endif %} 14 | {% endblock %} 15 | {% block title_suffix %} 16 | {% with self.get_site.site_name as site_name %} 17 | {% if site_name %}- {{ site_name }}{% endif %} 18 | {% endwith %} 19 | {% endblock %} 20 | 21 | 22 | 23 | 24 | 25 | 26 | {% wtm_cookie_bar %} 27 | {% wagtailuserbar %} 28 | 29 |
30 | {% block content %}{% endblock %} 31 |
32 | 33 | {% block extra_js %}{% endblock %} 34 | 35 | 36 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | 27 | Cypress.on("window:before:load", (win) => { 28 | win.fetch = null; 29 | }); 30 | 31 | Cypress.Commands.add("setConsent", (content) => { 32 | return cy.setCookie("wtm", encodeURIComponent(btoa(JSON.stringify(content)))); 33 | }); 34 | 35 | Cypress.Commands.add("getConsent", () => { 36 | return cy.getCookie("wtm").then((cookie) => { 37 | if (cookie && cookie.value) { 38 | return JSON.parse(atob(decodeURIComponent(cookie.value))); 39 | } 40 | return {}; 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /sandbox/exampledata/cms.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "wagtailcore.site", 4 | "pk": 1, 5 | "fields": { 6 | "hostname": "localhost", 7 | "port": 8000, 8 | "site_name": "Wagtail Tag Manager", 9 | "root_page": 2, 10 | "is_default_site": true 11 | } 12 | }, 13 | { 14 | "model": "contenttypes.contenttype", 15 | "pk": 54, 16 | "fields": { 17 | "app_label": "home", 18 | "model": "homepage" 19 | } 20 | }, 21 | { 22 | "model": "wagtailcore.page", 23 | "pk": 2, 24 | "fields": { 25 | "path": "00010001", 26 | "depth": 2, 27 | "numchild": 0, 28 | "title": "Wagtail Tag Manager", 29 | "draft_title": "Wagtail Tag Manager", 30 | "slug": "wagtail-tag-manager", 31 | "content_type": 54, 32 | "live": true, 33 | "has_unpublished_changes": false, 34 | "url_path": "/wagtail-tag-manager/", 35 | "owner": 1, 36 | "seo_title": "", 37 | "show_in_menus": false, 38 | "search_description": "", 39 | "go_live_at": null, 40 | "expire_at": null, 41 | "expired": false, 42 | "locked": false 43 | } 44 | }, 45 | { 46 | "model": "home.homepage", 47 | "pk": 2, 48 | "fields": { 49 | "content": "

Wagtail Tag Manager (WTM for short) is a Wagtail addon that allows for easier and GDPR compliant administration of scripts and tags.

" 50 | } 51 | } 52 | ] 53 | -------------------------------------------------------------------------------- /sandbox/sandbox/apps/home/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.8 on 2019-05-21 09:35 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | import modelcluster.fields 6 | import wagtail.fields 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ('wagtailcore', '0041_group_collection_permissions_verbose_name_plural'), 15 | ('wagtail_tag_manager', '0013_auto_20190506_0705'), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='HomePage', 21 | fields=[ 22 | ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')), 23 | ('wtm_include_children', models.BooleanField(default=False, help_text='Also include these tags on all children of this page.', verbose_name='Include children')), 24 | ('content', wagtail.fields.RichTextField()), 25 | ('wtm_tags', modelcluster.fields.ParentalManyToManyField(blank=True, help_text='The tags to include when this page is loaded.', related_name='pages', to='wagtail_tag_manager.Tag', verbose_name='Tags')), 26 | ], 27 | options={ 28 | 'abstract': False, 29 | }, 30 | bases=('wagtailcore.page', models.Model), 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | import base64 3 | import binascii 4 | from urllib.parse import quote, unquote 5 | 6 | from django.http import HttpRequest 7 | 8 | 9 | def get_page_for_request(request: HttpRequest): 10 | site = get_site_for_request(request) 11 | if site: 12 | path = request.path 13 | path_components = [component for component in path.split("/") if component] 14 | page, args, kwargs = site.root_page.specific.route(request, path_components) 15 | return page 16 | 17 | return None 18 | 19 | 20 | def get_site_for_request(request: HttpRequest): 21 | try: 22 | from wagtail.models import Site 23 | 24 | return Site.find_for_request(request) 25 | except: # noqa: E722 26 | return getattr(request, "site") 27 | 28 | 29 | def dict_to_base64(input: dict) -> str: 30 | content = json.dumps(input, separators=(",", ":"), sort_keys=True) 31 | encoded_bytes = base64.b64encode(content.encode("utf-8")) 32 | encoded_string = encoded_bytes.decode("utf-8") 33 | return quote(encoded_string) 34 | 35 | 36 | def base64_to_dict(input: str) -> dict: 37 | try: 38 | original_string = unquote(input) 39 | encoded_bytes = original_string.encode("utf-8") 40 | decoded_bytes = base64.b64decode(encoded_bytes) 41 | decoded_string = decoded_bytes.decode("utf-8") 42 | return json.loads(decoded_string) 43 | except binascii.Error: 44 | return {} 45 | -------------------------------------------------------------------------------- /sandbox/sandbox/urls.py: -------------------------------------------------------------------------------- 1 | import debug_toolbar 2 | from django.conf import settings 3 | from django.contrib import admin 4 | from django.urls import include, path 5 | from wagtail.admin import urls as wagtailadmin_urls 6 | from wagtail import urls as wagtail_urls 7 | from wagtail.documents import urls as wagtaildocs_urls 8 | 9 | from wagtail_tag_manager import urls as wtm_urls 10 | 11 | urlpatterns = [ 12 | path('admin/', admin.site.urls), 13 | 14 | path('cms/', include(wagtailadmin_urls)), 15 | path('documents/', include(wagtaildocs_urls)), 16 | 17 | path('wtm/', include(wtm_urls)), 18 | 19 | # For anything not caught by a more specific rule above, hand over to 20 | # Wagtail's page serving mechanism. This should be the last pattern in 21 | # the list: 22 | path('', include(wagtail_urls)), 23 | 24 | # Alternatively, if you want Wagtail pages to be served from a subpath 25 | # of your site, rather than the site root: 26 | # url(r'^pages/', include(wagtail_urls)), 27 | ] 28 | 29 | 30 | if settings.DEBUG: 31 | from django.conf.urls.static import static 32 | from django.contrib.staticfiles.urls import staticfiles_urlpatterns 33 | 34 | # Serve static and media files from development server 35 | urlpatterns += staticfiles_urlpatterns() 36 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 37 | 38 | urlpatterns = [ 39 | path('__debug__/', include(debug_toolbar.urls)), 40 | ] + urlpatterns 41 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/mixins.py: -------------------------------------------------------------------------------- 1 | import django 2 | from django.db import models 3 | from modelcluster.fields import ParentalManyToManyField 4 | from modelcluster.models import ClusterableModel 5 | from wagtail.admin.panels import FieldPanel, MultiFieldPanel, PublishingPanel 6 | 7 | from wagtail_tag_manager.models import Tag 8 | from wagtail_tag_manager.widgets import ( 9 | HorizontalCheckboxSelectMultiple as CheckboxSelectMultiple, 10 | ) 11 | 12 | __version__ = django.get_version() 13 | if __version__.startswith("2"): 14 | from django.utils.translation import ugettext_lazy as _ 15 | else: 16 | from django.utils.translation import gettext_lazy as _ 17 | 18 | 19 | class TagMixin(ClusterableModel): 20 | wtm_tags = ParentalManyToManyField( 21 | Tag, 22 | blank=True, 23 | related_name="pages", 24 | verbose_name=_("Tags"), 25 | help_text=_("The tags to include when this page is loaded."), 26 | ) 27 | wtm_include_children = models.BooleanField( 28 | default=False, 29 | verbose_name=_("Include children"), 30 | help_text=_("Also include these tags on all children of this page."), 31 | ) 32 | 33 | settings_panels = [ 34 | PublishingPanel(), 35 | MultiFieldPanel( 36 | [ 37 | FieldPanel("wtm_tags", widget=CheckboxSelectMultiple), 38 | FieldPanel("wtm_include_children"), 39 | ], 40 | heading=_("Tags"), 41 | ), 42 | ] 43 | 44 | class Meta: 45 | abstract = True 46 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{37,38,39,310,311,312,313}-django{22,30,31,32,40,41,42,50}-wagtail{211,212,213,214,215,216,30,40,41,42,50,51,52,60},lint 3 | 4 | [testenv] 5 | basepython = 6 | py37: python3.7 7 | py38: python3.8 8 | py39: python3.9 9 | py310: python3.10 10 | py311: python3.11 11 | py312: python3.12 12 | py313: python3.13 13 | commands = coverage run --parallel -m pytest -rs {posargs} 14 | extras = test 15 | deps = 16 | django22: django>=2.2,<2.3 17 | django30: django>=3.0,<3.1 18 | django31: django>=3.1,<3.2 19 | django32: django>=3.2,<3.3 20 | django40: django>=4.0,<4.1 21 | django41: django>=4.1,<4.2 22 | django42: django>=4.2,<4.3 23 | django50: django>=5.0,<5.1 24 | wagtail211: wagtail>=2.11,<2.12 25 | wagtail212: wagtail>=2.12,<2.13 26 | wagtail213: wagtail>=2.13,<2.14 27 | wagtail214: wagtail>=2.14,<2.15 28 | wagtail215: wagtail>=2.15,<2.16 29 | wagtail216: wagtail>=2.16,<2.17 30 | wagtail30: wagtail>=3.0,<3.1 31 | wagtail40: wagtail>=4.0,<4.1 32 | wagtail41: wagtail>=4.1,<4.2 33 | wagtail42: wagtail>=4.2,<4.3 34 | wagtail50: wagtail>=5.0,<5.1 35 | wagtail51: wagtail>=5.1,<5.2 36 | wagtail52: wagtail>=5.2,<5.3 37 | wagtail60: wagtail>=6.0,<6.1 38 | 39 | [testenv:coverage-report] 40 | basepython = python3.6 41 | deps = coverage 42 | pip_pre = true 43 | skip_install = true 44 | commands = 45 | coverage report --include="src/**/" --omit="src/**/migrations/*.py" 46 | 47 | [testenv:lint] 48 | basepython = python3.11 49 | deps = flake8==3.7.8 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018, Jasper Berghoef 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 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * 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 | * 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 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/endpoints.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.http import JsonResponse, HttpResponseBadRequest 4 | 5 | from wagtail_tag_manager.consent import Consent 6 | from wagtail_tag_manager.strategy import TagStrategy 7 | 8 | 9 | def lazy_endpoint(request): 10 | data = {"tags": []} 11 | response = JsonResponse(data) 12 | 13 | if getattr(request, "method", None) == "POST" and hasattr(request, "body"): 14 | try: 15 | payload = json.loads(request.body) 16 | except json.JSONDecodeError: 17 | return HttpResponseBadRequest() 18 | 19 | request.path = payload.get("pathname", request.path) 20 | request.META["QUERY_STRING"] = payload.get("search", "") 21 | 22 | strategy = TagStrategy(request, payload) 23 | consent = Consent(request) 24 | consent.apply_state( 25 | {key: value for key, value in strategy.consent_state.items()} 26 | ) 27 | consent.refresh_consent(response) 28 | 29 | for tag in strategy.result: 30 | element = tag.get("element") 31 | 32 | for content in element.contents: 33 | if content.name: 34 | data["tags"].append( 35 | { 36 | "name": content.name, 37 | "attributes": getattr(content, "attrs", {}), 38 | "string": content.string, 39 | } 40 | ) 41 | 42 | response.content = json.dumps(data) 43 | return response 44 | 45 | return HttpResponseBadRequest() 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wagtail-tag-manager", 3 | "version": "2.0.0", 4 | "description": "A Wagtail add-on for managing tags.", 5 | "main": "webpack.config.js", 6 | "browserslist": "> 0.25%, not dead", 7 | "scripts": { 8 | "build": "webpack --mode production --progress", 9 | "dev": "webpack --mode development --progress", 10 | "watch": "webpack --mode development --watch", 11 | "cypress:open": "CYPRESS_CRASH_REPORTS=0 cypress open --browser electron --e2e", 12 | "cypress:run": "CYPRESS_CRASH_REPORTS=0 wait-on http://127.0.0.1:8000/ && cypress run --browser electron --e2e", 13 | "format": "prettier --write './frontend/**/*'" 14 | }, 15 | "repository": "github.com/jberghoef/wagtail-tag-manager", 16 | "author": "Jasper Berghoef", 17 | "license": "BSD-3-Clause", 18 | "devDependencies": { 19 | "@babel/core": "^7.17.10", 20 | "@babel/plugin-proposal-object-rest-spread": "^7.17.3", 21 | "@babel/polyfill": "^7.12.1", 22 | "@babel/preset-env": "^7.17.10", 23 | "@types/codemirror": "^5.60.5", 24 | "@types/js-cookie": "^3.0.2", 25 | "babel-loader": "^8.2.5", 26 | "css-loader": "^6.7.1", 27 | "cypress": "^10.0.3", 28 | "mini-css-extract-plugin": "^2.6.0", 29 | "node-sass": "^7.0.1", 30 | "prettier": "^2.6.2", 31 | "sass-loader": "^13.0.0", 32 | "serialize-javascript": "^6.0.0", 33 | "style-loader": "^3.3.1", 34 | "ts-loader": "^9.3.0", 35 | "typescript": "^4.6.4", 36 | "wait-on": "^7.0.1", 37 | "webpack": "^5.72.0", 38 | "webpack-cli": "^4.9.2" 39 | }, 40 | "dependencies": { 41 | "codemirror": "^5.65.3", 42 | "js-cookie": "^3.0.1", 43 | "whatwg-fetch": "^3.6.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/static/wagtail_tag_manager/sourcemaps/checkbox_select_multiple.bundle.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"checkbox_select_multiple.bundle.js","mappings":";;;;;;;;;;;AAAA;;;;;;;UCAA;UACA;;UAEA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;;UAEA;UACA;;UAEA;UACA;UACA;;;;;WCtBA;WACA;WACA;WACA,uDAAuD,iBAAiB;WACxE;WACA,gDAAgD,aAAa;WAC7D;;;;;;;;;;;;ACNyC","sources":["webpack://wagtail-tag-manager/./frontend/admin/widgets/checkbox_select_multiple.scss?a6f2","webpack://wagtail-tag-manager/webpack/bootstrap","webpack://wagtail-tag-manager/webpack/runtime/make namespace object","webpack://wagtail-tag-manager/./frontend/admin/widgets/checkbox_select_multiple.ts"],"sourcesContent":["// extracted by mini-css-extract-plugin\nexport {};","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","// define __esModule on exports\n__webpack_require__.r = function(exports) {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","import \"./checkbox_select_multiple.scss\";\n"],"names":[],"sourceRoot":""} -------------------------------------------------------------------------------- /frontend/admin/tag_form_view.ts: -------------------------------------------------------------------------------- 1 | class TagFormView { 2 | loadSelect: HTMLSelectElement; 3 | locationSelect: HTMLSelectElement; 4 | hiddenInput: HTMLInputElement; 5 | 6 | constructor() { 7 | this.loadSelect = document.getElementById("id_tag_loading") as HTMLSelectElement; 8 | this.locationSelect = document.getElementById("id_tag_location") as HTMLSelectElement; 9 | 10 | this.initialize = this.initialize.bind(this); 11 | this.handleLoadChange = this.handleLoadChange.bind(this); 12 | 13 | this.loadSelect.addEventListener("change", this.handleLoadChange); 14 | 15 | this.initialize(); 16 | } 17 | 18 | initialize() { 19 | this.handleLoadChange(); 20 | } 21 | 22 | handleLoadChange() { 23 | const value = this.loadSelect.options[this.loadSelect.selectedIndex].value; 24 | 25 | if (value !== "instant_load") { 26 | this.locationSelect.disabled = true; 27 | [].forEach.call(this.locationSelect, (option: HTMLOptionElement) => { 28 | if (option.value === "0_top_head") { 29 | option.selected = true; 30 | } 31 | }); 32 | 33 | this.hiddenInput = document.createElement("input"); 34 | this.hiddenInput.id = this.locationSelect.id; 35 | this.hiddenInput.name = this.locationSelect.name; 36 | this.hiddenInput.type = "hidden"; 37 | this.hiddenInput.value = "0_top_head"; 38 | this.locationSelect.parentNode.insertBefore( 39 | this.hiddenInput, 40 | this.locationSelect.parentNode.childNodes[0] 41 | ); 42 | } else { 43 | this.locationSelect.disabled = false; 44 | if (this.hiddenInput) { 45 | this.hiddenInput.remove(); 46 | } 47 | } 48 | } 49 | } 50 | 51 | document.addEventListener("DOMContentLoaded", () => { 52 | new TagFormView(); 53 | }); 54 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | 4 | install_requires = [ 5 | "wagtail>=4.0,<7.0", 6 | "wagtail_modeladmin>=2.0.0,<2.1.0", 7 | "selenium>=3.141.0,<3.142.0", 8 | ] 9 | 10 | tests_require = [ 11 | "factory_boy", 12 | "Faker", 13 | "flake8-blind-except", 14 | "flake8-debugger", 15 | "flake8-imports", 16 | "flake8", 17 | "freezegun", 18 | "pycodestyle", 19 | "pytest-cov", 20 | "pytest-django", 21 | "pytest-randomly", 22 | "pytest-sugar", 23 | "pytest", 24 | "wagtail_factories", 25 | ] 26 | 27 | docs_require = ["sphinx>=2.4.0"] 28 | 29 | with open('README.md') as fh: 30 | long_description = fh.read() 31 | 32 | setup( 33 | name="wagtail-tag-manager", 34 | version="2.0.0", 35 | description="A Wagtail add-on for managing tags.", 36 | author="Jasper Berghoef", 37 | author_email="j.berghoef@me.com", 38 | url="https://github.com/jberghoef/wagtail-tag-manager", 39 | install_requires=install_requires, 40 | tests_require=tests_require, 41 | extras_require={"docs": docs_require, "test": tests_require}, 42 | packages=find_packages("src"), 43 | package_dir={"": "src"}, 44 | include_package_data=True, 45 | license="BSD 3-Clause", 46 | long_description=long_description, 47 | long_description_content_type='text/markdown', 48 | classifiers=[ 49 | "Development Status :: 4 - Beta", 50 | "Environment :: Web Environment", 51 | "Intended Audience :: Developers", 52 | "License :: OSI Approved :: BSD License", 53 | "Operating System :: OS Independent", 54 | "Programming Language :: Python :: 3.11", 55 | "Framework :: Django :: 4.0", 56 | "Framework :: Django :: 5.0", 57 | "Framework :: Wagtail :: 5", 58 | "Framework :: Wagtail :: 6", 59 | ], 60 | ) 61 | -------------------------------------------------------------------------------- /tests/unit/test_forms.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wagtail_tag_manager.forms import ConsentForm 4 | from wagtail_tag_manager.utils import dict_to_base64 5 | from wagtail_tag_manager.strategy import TagStrategy 6 | 7 | 8 | @pytest.mark.django_db 9 | def test_consent_form(): 10 | form = ConsentForm() 11 | 12 | assert "necessary" in form.fields 13 | assert "preferences" in form.fields 14 | assert "marketing" in form.fields 15 | 16 | assert form.fields["necessary"].required is True 17 | assert form.fields["necessary"].disabled is True 18 | assert form.fields["necessary"].initial is True 19 | 20 | assert form.fields["preferences"].required is False 21 | assert form.fields["preferences"].disabled is False 22 | assert form.fields["preferences"].initial is True 23 | 24 | assert form.fields["marketing"].required is False 25 | assert form.fields["marketing"].disabled is False 26 | assert form.fields["marketing"].initial is False 27 | 28 | 29 | @pytest.mark.django_db 30 | def test_consent_form_initial(rf, site): 31 | request = rf.get(site.root_page.url) 32 | request.COOKIES = { 33 | **request.COOKIES, 34 | "wtm": dict_to_base64( 35 | { 36 | "meta": {}, 37 | "state": { 38 | "necessary": "true", 39 | "preferences": "false", 40 | "marketing": "true", 41 | }, 42 | } 43 | ), 44 | } 45 | 46 | cookie_state = TagStrategy(request).cookie_state 47 | form = ConsentForm(initial=cookie_state) 48 | 49 | assert "necessary" in form.fields 50 | assert "preferences" in form.fields 51 | assert "marketing" in form.fields 52 | 53 | assert form.fields["necessary"].initial is True 54 | assert form.fields["preferences"].initial is False 55 | assert form.fields["marketing"].initial is True 56 | -------------------------------------------------------------------------------- /tests/unit/test_views.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | from django.urls import reverse 5 | 6 | from tests.factories.constant import ConstantFactory 7 | from tests.factories.variable import VariableFactory 8 | from wagtail_tag_manager.views import CookieDeclarationIndexView 9 | from wagtail_tag_manager.settings import TagTypeSettings 10 | from wagtail_tag_manager.wagtail_hooks import CookieDeclarationModelAdmin 11 | 12 | 13 | @pytest.mark.skip 14 | @pytest.mark.django_db 15 | def test_manage_view(client): 16 | url = reverse("wtm:manage") 17 | 18 | response = client.get(url) 19 | assert response.status_code == 200 20 | 21 | response = client.post(url) 22 | assert response.status_code == 302 23 | 24 | 25 | @pytest.mark.django_db 26 | def test_state_view(client): 27 | url = reverse("wtm:config") 28 | 29 | response = client.get(url) 30 | assert response.status_code == 200 31 | 32 | content = json.loads(response.content) 33 | for tag_type, config in TagTypeSettings.all().items(): 34 | assert tag_type in content["tag_types"] 35 | assert content["tag_types"][tag_type] == config.get("value") 36 | 37 | 38 | @pytest.mark.django_db 39 | def test_variable_view(client, admin_user): 40 | url = reverse("wtm:variables") 41 | 42 | ConstantFactory(key="constant1") 43 | VariableFactory(key="variable1") 44 | 45 | response = client.get(url) 46 | assert response.status_code == 404 47 | 48 | client.login(username="admin", password="password") 49 | response = client.get(url) 50 | assert response.status_code == 200 51 | 52 | 53 | @pytest.mark.django_db 54 | def test_cookie_declaration_index_view(client, admin_user): 55 | model_admin = CookieDeclarationModelAdmin() 56 | url = CookieDeclarationIndexView(model_admin=model_admin).index_url 57 | 58 | response = client.get(url) 59 | assert response.status_code == 302 60 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/static/wagtail_tag_manager/sourcemaps/trigger_form_view.bundle.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"trigger_form_view.bundle.js","mappings":"MAAA,iBAIE,aACEA,KAAKC,cAAgBC,SAASC,eAAe,mBAC7CH,KAAKI,WAAaF,SAASC,eAAe,YAE1CH,KAAKK,WAAaL,KAAKK,WAAWC,KAAKN,MACvCA,KAAKO,oBAAsBP,KAAKO,oBAAoBD,KAAKN,MAEzDA,KAAKC,cAAcO,iBAAiB,SAAUR,KAAKO,qBAEnDP,KAAKK,YACP,CAgBF,OAdE,YAAAA,WAAA,WACEL,KAAKO,qBACP,EAEA,YAAAA,oBAAA,SAAoBE,QAAA,IAAAA,IAAAA,EAAA,MAGM,MAFVT,KAAKC,cAAcS,QAAQV,KAAKC,cAAcU,eAAeC,MAEjEC,OAAO,IACfb,KAAKI,WAAWU,UAAW,EAC3Bd,KAAKI,WAAWQ,MAAQ,IAExBZ,KAAKI,WAAWU,UAAW,CAE/B,EACF,EA9BA,GAgCAZ,SAASM,iBAAiB,oBAAoB,SAACC,GAC7C,IAAIM,CACN,G","sources":["webpack://wagtail-tag-manager/./frontend/admin/trigger_form_view.ts"],"sourcesContent":["class TriggerFormView {\n triggerSelect: HTMLSelectElement;\n valueInput: HTMLInputElement;\n\n constructor() {\n this.triggerSelect = document.getElementById(\"id_trigger_type\") as HTMLSelectElement;\n this.valueInput = document.getElementById(\"id_value\") as HTMLInputElement;\n\n this.initialize = this.initialize.bind(this);\n this.handleTriggerChange = this.handleTriggerChange.bind(this);\n\n this.triggerSelect.addEventListener(\"change\", this.handleTriggerChange);\n\n this.initialize();\n }\n\n initialize() {\n this.handleTriggerChange();\n }\n\n handleTriggerChange(event: Event = null) {\n const value = this.triggerSelect.options[this.triggerSelect.selectedIndex].value;\n\n if (value.slice(-1) !== \"+\") {\n this.valueInput.disabled = true;\n this.valueInput.value = \"\";\n } else {\n this.valueInput.disabled = false;\n }\n }\n}\n\ndocument.addEventListener(\"DOMContentLoaded\", (event) => {\n new TriggerFormView();\n});\n"],"names":["this","triggerSelect","document","getElementById","valueInput","initialize","bind","handleTriggerChange","addEventListener","event","options","selectedIndex","value","slice","disabled","TriggerFormView"],"sourceRoot":""} -------------------------------------------------------------------------------- /src/wagtail_tag_manager/static/wagtail_tag_manager/sourcemaps/variable_form_view.bundle.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"variable_form_view.bundle.js","mappings":"MAEA,iBAIE,aACEA,KAAKC,eAAiBC,SAASC,eAAe,oBAC9CH,KAAKI,WAAaF,SAASC,eAAe,YAE1CH,KAAKK,WAAaL,KAAKK,WAAWC,KAAKN,MACvCA,KAAKO,qBAAuBP,KAAKO,qBAAqBD,KAAKN,MAE3DA,KAAKC,eAAeO,iBAAiB,SAAUR,KAAKO,sBAEpDP,KAAKK,YACP,CAgBF,OAdE,YAAAA,WAAA,WACEL,KAAKO,sBACP,EAEA,YAAAA,qBAAA,SAAqBE,QAAA,IAAAA,IAAAA,EAAA,MAGK,MAFVT,KAAKC,eAAeS,QAAQV,KAAKC,eAAeU,eAAeC,MAEnEC,OAAO,IACfb,KAAKI,WAAWU,UAAW,EAC3Bd,KAAKI,WAAWQ,MAAQ,IAExBZ,KAAKI,WAAWU,UAAW,CAE/B,EACF,EA9BA,GAgCAZ,SAASM,iBAAiB,oBAAoB,SAACC,GAC7C,IAAIM,CACN,G","sources":["webpack://wagtail-tag-manager/./frontend/admin/variable_form_view.ts"],"sourcesContent":["// TODO: Turn this into something reusable.\n\nclass VariableFormView {\n variableSelect: HTMLSelectElement;\n valueInput: HTMLInputElement;\n\n constructor() {\n this.variableSelect = document.getElementById(\"id_variable_type\") as HTMLSelectElement;\n this.valueInput = document.getElementById(\"id_value\") as HTMLInputElement;\n\n this.initialize = this.initialize.bind(this);\n this.handleVariableChange = this.handleVariableChange.bind(this);\n\n this.variableSelect.addEventListener(\"change\", this.handleVariableChange);\n\n this.initialize();\n }\n\n initialize() {\n this.handleVariableChange();\n }\n\n handleVariableChange(event: Event = null) {\n const value = this.variableSelect.options[this.variableSelect.selectedIndex].value;\n\n if (value.slice(-1) !== \"+\") {\n this.valueInput.disabled = true;\n this.valueInput.value = \"\";\n } else {\n this.valueInput.disabled = false;\n }\n }\n}\n\ndocument.addEventListener(\"DOMContentLoaded\", (event) => {\n new VariableFormView();\n});\n"],"names":["this","variableSelect","document","getElementById","valueInput","initialize","bind","handleVariableChange","addEventListener","event","options","selectedIndex","value","slice","disabled","VariableFormView"],"sourceRoot":""} -------------------------------------------------------------------------------- /tests/unit/test_decorators.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from wagtail_tag_manager.options import CustomVariable 4 | from wagtail_tag_manager.decorators import get_variables, register_variable 5 | 6 | 7 | @pytest.mark.django_db 8 | def test_register_variable(): 9 | @register_variable 10 | class Variable(CustomVariable): 11 | name = "Custom variable" 12 | description = "Returns a custom value." 13 | key = "custom1" 14 | 15 | def get_value(self, request): 16 | return "This is a custom variable." 17 | 18 | variables = get_variables() 19 | assert next( 20 | item is not None for item in variables if getattr(item, "key") == "custom1" 21 | ) 22 | 23 | 24 | @pytest.mark.django_db 25 | def test_register_variable_after(): 26 | class Variable(CustomVariable): 27 | name = "Custom variable" 28 | description = "Returns a custom value." 29 | key = "custom2" 30 | 31 | def get_value(self, request): 32 | return "This is a custom variable." 33 | 34 | register_variable(Variable) 35 | 36 | variables = get_variables() 37 | assert next( 38 | item is not None for item in variables if getattr(item, "key") == "custom2" 39 | ) 40 | 41 | 42 | @pytest.mark.django_db 43 | def test_register_variable_invalid(): 44 | class Variable(CustomVariable): 45 | name = "Custom variable" 46 | key = "custom3" 47 | 48 | def get_value(self, request): 49 | return "This is a custom variable." 50 | 51 | with pytest.raises(Exception): 52 | Variable() 53 | 54 | 55 | @pytest.mark.django_db 56 | def test_register_variable_subclass(): 57 | class Variable(object): 58 | name = "Custom variable" 59 | description = "Returns a custom value." 60 | key = "custom4" 61 | 62 | def get_value(self, request): 63 | return "This is a custom variable." 64 | 65 | with pytest.raises(Exception): 66 | register_variable(Variable) 67 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/static/wagtail_tag_manager/sourcemaps/index.bundle.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.bundle.js","mappings":"mBAEA,iBAIE,aACEA,KAAKC,GAAKC,SAASC,eAAe,kBAClCH,KAAKI,SAAWJ,KAAKC,GAAGI,cAAc,gBAEtCL,KAAKM,cAAgBN,KAAKM,cAAcC,KAAKP,MAC7CA,KAAKQ,cAAgBR,KAAKQ,cAAcD,KAAKP,MAC7CA,KAAKS,WAAaT,KAAKS,WAAWF,KAAKP,MAEvCA,KAAKI,SAASM,iBAAiB,QAASV,KAAKQ,eAEzCR,KAAKC,IACPD,KAAKS,YAET,CAoBF,OAlBE,YAAAA,WAAA,WACgD,OAA1CE,aAAaC,QAAQZ,KAAKa,aAC5Bb,KAAKM,eAET,EAEA,YAAAA,cAAA,WACEN,KAAKC,GAAGa,MAAMC,QAAU,OAC1B,EAEA,YAAAP,cAAA,WACEG,aAAaK,QAAQhB,KAAKa,WAAY,UACtCb,KAAKC,GAAGa,MAAMC,QAAU,MAC1B,EAEA,sBAAI,yBAAU,C,IAAd,WACE,MAAO,kBAAoBE,SAASC,QACtC,E,gCACF,EArCA,GAuCAhB,SAASQ,iBAAiB,oBAAoB,WAC5C,IAAIS,CACN,G","sources":["webpack://wagtail-tag-manager/./frontend/admin/index.ts"],"sourcesContent":["import \"./index.scss\";\n\nclass IndexView {\n el: HTMLElement;\n close_el: HTMLAnchorElement;\n\n constructor() {\n this.el = document.getElementById(\"wtm_help_block\");\n this.close_el = this.el.querySelector(\"a.close-link\");\n\n this.showHelpBlock = this.showHelpBlock.bind(this);\n this.hideHelpBlock = this.hideHelpBlock.bind(this);\n this.initialize = this.initialize.bind(this);\n\n this.close_el.addEventListener(\"click\", this.hideHelpBlock);\n\n if (this.el) {\n this.initialize();\n }\n }\n\n initialize() {\n if (localStorage.getItem(this.identifier) === null) {\n this.showHelpBlock();\n }\n }\n\n showHelpBlock() {\n this.el.style.display = \"block\";\n }\n\n hideHelpBlock() {\n localStorage.setItem(this.identifier, \"hidden\");\n this.el.style.display = \"none\";\n }\n\n get identifier() {\n return \"wtm_help_block:\" + location.pathname;\n }\n}\n\ndocument.addEventListener(\"DOMContentLoaded\", () => {\n new IndexView();\n});\n"],"names":["this","el","document","getElementById","close_el","querySelector","showHelpBlock","bind","hideHelpBlock","initialize","addEventListener","localStorage","getItem","identifier","style","display","setItem","location","pathname","IndexView"],"sourceRoot":""} -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 3 | 4 | module.exports = (env, options) => ({ 5 | entry: { 6 | index: ["./frontend/admin/index.ts"], 7 | tag_form_view: ["./frontend/admin/tag_form_view.ts"], 8 | trigger_form_view: ["./frontend/admin/trigger_form_view.ts"], 9 | variable_form_view: ["./frontend/admin/variable_form_view.ts"], 10 | checkbox_select_multiple: ["./frontend/admin/widgets/checkbox_select_multiple.ts"], 11 | codearea: ["./frontend/admin/widgets/codearea.ts"], 12 | wtm: ["./frontend/client/wtm.ts"] 13 | }, 14 | output: { 15 | path: path.resolve(__dirname, "src/wagtail_tag_manager/static/wagtail_tag_manager"), 16 | filename: "[name].bundle.js", 17 | sourceMapFilename: "sourcemaps/[file].map" 18 | }, 19 | devtool: options.mode == "production" ? "hidden-source-map" : "source-map", 20 | module: { 21 | rules: [ 22 | { 23 | test: /\.tsx?$/, 24 | exclude: /(node_modules|bower_components)/, 25 | use: { 26 | loader: "ts-loader" 27 | } 28 | }, 29 | { 30 | test: /\.m?jsx?$/, 31 | exclude: /(node_modules|bower_components)/, 32 | use: { 33 | loader: "babel-loader", 34 | options: { 35 | presets: [ 36 | [ 37 | "@babel/preset-env", 38 | { 39 | useBuiltIns: "usage" 40 | } 41 | ] 42 | ], 43 | plugins: [require("@babel/plugin-proposal-object-rest-spread")] 44 | } 45 | } 46 | }, 47 | { 48 | test: /\.scss$/, 49 | use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"] 50 | } 51 | ] 52 | }, 53 | resolve: { 54 | extensions: [".tsx", ".ts", ".jsx", ".js"] 55 | }, 56 | plugins: [ 57 | new MiniCssExtractPlugin({ 58 | filename: "[name].bundle.css", 59 | chunkFilename: "[id].chunk.css" 60 | }) 61 | ] 62 | }); 63 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/migrations/0002_auto_20181111_1854.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.3 on 2018-11-11 18:54 2 | 3 | from django.db import models, migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('wagtail_tag_manager', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='CookieDeclaration', 15 | fields=[ 16 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 17 | ('cookie_type', models.CharField(blank=True, choices=[('functional', 'Functional'), ('analytical', 'Analytical'), ('traceable', 'Traceable')], help_text='The type of functionality this cookie supports.', max_length=10, null=True)), 18 | ('name', models.CharField(help_text='The name of this cookie.', max_length=255)), 19 | ('domain', models.CharField(help_text='The domain (including subdomain if applicable) of the cookie.
For example: .wagtail.io.', max_length=255)), 20 | ('purpose', models.TextField(help_text='What this cookie is being used for.')), 21 | ('duration_value', models.PositiveSmallIntegerField(blank=True, null=True)), 22 | ('duration_period', models.CharField(choices=[('session', 'Session'), ('seconds+', 'Second(s)'), ('minutes+', 'Minute(s)'), ('hours+', 'Hour(s)'), ('days+', 'Day(s)'), ('weeks+', 'Week(s)'), ('months+', 'Month(s)'), ('years+', 'Year(s)')], help_text='The period after which the cookie will expire.
Session: the cookie will expire when the browser is closed.', max_length=10, null=True)), 23 | ('security', models.CharField(choices=[('http', 'HTTP'), ('https', 'HTTPS')], default='http', help_text='Whether this cookie is secure or not.', max_length=5)), 24 | ], 25 | options={ 26 | 'ordering': ['domain', 'cookie_type', 'name'], 27 | }, 28 | ), 29 | migrations.AlterUniqueTogether( 30 | name='cookiedeclaration', 31 | unique_together={('name', 'domain')}, 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/migrations/0018_alter_constant_id_alter_cookiebarsettings_id_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-05-06 11:19 2 | 3 | from django.db import models, migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('wagtail_tag_manager', '0017_auto_20201028_0905'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='constant', 15 | name='id', 16 | field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), 17 | ), 18 | migrations.AlterField( 19 | model_name='cookiebarsettings', 20 | name='id', 21 | field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), 22 | ), 23 | migrations.AlterField( 24 | model_name='cookieconsent', 25 | name='id', 26 | field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), 27 | ), 28 | migrations.AlterField( 29 | model_name='cookiedeclaration', 30 | name='id', 31 | field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), 32 | ), 33 | migrations.AlterField( 34 | model_name='tag', 35 | name='id', 36 | field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), 37 | ), 38 | migrations.AlterField( 39 | model_name='trigger', 40 | name='id', 41 | field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), 42 | ), 43 | migrations.AlterField( 44 | model_name='triggercondition', 45 | name='id', 46 | field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), 47 | ), 48 | migrations.AlterField( 49 | model_name='variable', 50 | name='id', 51 | field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), 52 | ), 53 | ] 54 | -------------------------------------------------------------------------------- /tests/factories/tag.py: -------------------------------------------------------------------------------- 1 | import factory 2 | 3 | from wagtail_tag_manager.models import Tag 4 | 5 | 6 | class TagFactory(factory.django.DjangoModelFactory): 7 | name = "necessary instant" 8 | content = '' 9 | 10 | class Meta: 11 | model = Tag 12 | 13 | 14 | def tag_instant_necessary(**kwargs): 15 | return TagFactory(**kwargs) 16 | 17 | 18 | def tag_instant_preferences(**kwarg): 19 | return TagFactory( 20 | name="preferences instant", 21 | tag_type="preferences", 22 | content='', 23 | **kwarg, 24 | ) 25 | 26 | 27 | def tag_instant_statistics(**kwarg): 28 | return TagFactory( 29 | name="statistics instant", 30 | tag_type="statistics", 31 | content='', 32 | **kwarg, 33 | ) 34 | 35 | 36 | def tag_instant_marketing(**kwarg): 37 | return TagFactory( 38 | name="marketing instant", 39 | tag_type="marketing", 40 | content='', 41 | **kwarg, 42 | ) 43 | 44 | 45 | def tag_lazy_necessary(**kwarg): 46 | return TagFactory( 47 | name="necessary lazy", 48 | tag_loading=Tag.LAZY_LOAD, 49 | content='', 50 | **kwarg, 51 | ) 52 | 53 | 54 | def tag_lazy_preferences(**kwarg): 55 | return TagFactory( 56 | name="preferences lazy", 57 | tag_loading=Tag.LAZY_LOAD, 58 | tag_type="preferences", 59 | content='', 60 | **kwarg, 61 | ) 62 | 63 | 64 | def tag_lazy_statistics(**kwarg): 65 | return TagFactory( 66 | name="statistics lazy", 67 | tag_loading=Tag.LAZY_LOAD, 68 | tag_type="statistics", 69 | content='', 70 | **kwarg, 71 | ) 72 | 73 | 74 | def tag_lazy_marketing(**kwarg): 75 | return TagFactory( 76 | name="marketing lazy", 77 | tag_loading=Tag.LAZY_LOAD, 78 | tag_type="marketing", 79 | content='', 80 | **kwarg, 81 | ) 82 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: install clean requirements test retest lint flake8 isort sandbox bundle watch git-reset 2 | 3 | default: install 4 | 5 | install: clean requirements bundle 6 | 7 | clean: 8 | find src -name '*.pyc' -delete 9 | find tests -name '*.pyc' -delete 10 | find . -name '*.egg-info' |xargs rm -rf 11 | 12 | requirements: 13 | yarn install 14 | pip install -U -e .[docs,test] 15 | 16 | test: 17 | py.test --nomigrations --reuse-db tests/ 18 | 19 | retest: 20 | py.test --nomigrations --reuse-db tests/ -vvv 21 | 22 | coverage: 23 | py.test --nomigrations --reuse-db tests/ --cov=wagtail_tag_manager --cov-report=term-missing --cov-report=html 24 | 25 | lint: flake8 isort mypy 26 | 27 | mypy: 28 | pip install -U mypy 29 | mypy src/ tests/ 30 | 31 | flake8: 32 | pip install -U flake8 33 | flake8 src/ tests/ 34 | 35 | isort: 36 | pip install -U isort 37 | isort --recursive src tests 38 | 39 | format: black prettier 40 | 41 | BLACK_EXCLUDE="/(\.git|\.hg|\.mypy_cache|\.tox|\.venv|_build|buck-out|build|dist|migrations)/" 42 | 43 | black: isort 44 | pip install -U black 45 | black --target-version py37 --verbose --exclude $(BLACK_EXCLUDE) ./src 46 | black --target-version py37 --verbose --exclude $(BLACK_EXCLUDE) ./tests 47 | 48 | prettier: 49 | yarn install 50 | yarn format 51 | 52 | sandbox: bundle 53 | pip install -U -r sandbox/requirements.txt 54 | rm -rf db.sqlite3 55 | sandbox/manage.py migrate 56 | sandbox/manage.py loaddata sandbox/exampledata/users.json 57 | sandbox/manage.py loaddata sandbox/exampledata/cms.json 58 | sandbox/manage.py loaddata sandbox/exampledata/default_tags.json 59 | sandbox/manage.py loaddata sandbox/exampledata/additional_tags.json 60 | sandbox/manage.py runserver 61 | 62 | test_sandbox: 63 | pip install -U -r sandbox/requirements.txt 64 | rm -rf db.sqlite3 65 | sandbox/manage.py migrate 66 | sandbox/manage.py loaddata sandbox/exampledata/users.json 67 | sandbox/manage.py loaddata sandbox/exampledata/cms.json 68 | sandbox/manage.py loaddata sandbox/exampledata/default_tags.json 69 | ENVIRONMENT=test sandbox/manage.py runserver --verbosity 0 70 | 71 | run_test_sandbox: 72 | sandbox/manage.py runserver --verbosity 0 > /dev/null 2>&1 73 | 74 | bundle: prettier 75 | yarn install --force 76 | yarn build 77 | 78 | watch: 79 | yarn dev 80 | yarn watch 81 | 82 | release: 83 | pip install -U twine wheel 84 | rm -rf build/* 85 | rm -rf dist/* 86 | python setup.py sdist bdist_wheel 87 | twine upload --repository pypi dist/* 88 | -------------------------------------------------------------------------------- /tests/fixtures.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from selenium import webdriver 3 | from django.test.client import RequestFactory as BaseRequestFactory 4 | from django.contrib.auth.models import AnonymousUser 5 | from django.contrib.sessions.backends.db import SessionStore 6 | from django.contrib.messages.storage.fallback import FallbackStorage 7 | from selenium.webdriver.common.desired_capabilities import DesiredCapabilities 8 | 9 | from tests.factories.page import ContentPageFactory, TaggableContentPageFactory 10 | from tests.factories.site import SiteFactory 11 | 12 | 13 | @pytest.fixture(scope="function") 14 | def site(): 15 | try: 16 | from wagtail.models import Locale # noqa 17 | 18 | from tests.factories.locale import LocaleFactory 19 | 20 | LocaleFactory() 21 | except: # noqa: E722 22 | pass 23 | 24 | root_page = ContentPageFactory(parent=None, slug="") 25 | site = SiteFactory(is_default_site=True, root_page=root_page) 26 | 27 | page1 = ContentPageFactory(parent=root_page, slug="content-page") 28 | page2 = TaggableContentPageFactory(parent=root_page, slug="taggable-content-page") 29 | ContentPageFactory(parent=page1, slug="content-page-child") 30 | TaggableContentPageFactory(parent=page2, slug="taggable-content-page-child") 31 | 32 | return site 33 | 34 | 35 | @pytest.fixture() 36 | def rf(): 37 | """RequestFactory instance""" 38 | return RequestFactory() 39 | 40 | 41 | class RequestFactory(BaseRequestFactory): 42 | def request(self, **request): 43 | request["user"] = None 44 | request = super(RequestFactory, self).request(**request) 45 | request.user = AnonymousUser() 46 | request.session = SessionStore() 47 | request._messages = FallbackStorage(request) 48 | return request 49 | 50 | 51 | @pytest.fixture 52 | def user(django_user_model): 53 | return django_user_model.objects.create(username="user") 54 | 55 | 56 | @pytest.fixture(scope="function") 57 | def driver(): 58 | options = webdriver.ChromeOptions() 59 | options.add_argument("disable-gpu") 60 | options.add_argument("headless") 61 | options.add_argument("no-default-browser-check") 62 | options.add_argument("no-first-run") 63 | options.add_argument("no-sandbox") 64 | 65 | d = DesiredCapabilities.CHROME 66 | d["loggingPrefs"] = {"browser": "ALL"} 67 | 68 | driver = webdriver.Chrome(options=options, desired_capabilities=d) 69 | driver.implicitly_wait(30) 70 | 71 | yield driver 72 | driver.quit() 73 | -------------------------------------------------------------------------------- /cypress/e2e/sandbox.js: -------------------------------------------------------------------------------- 1 | beforeEach("clear wtm cookies", () => { 2 | cy.clearCookie("wtm", { timeout: 1000 }); 3 | }); 4 | 5 | describe("The website", () => { 6 | it("contains base configuration", () => { 7 | cy.visit("/"); 8 | 9 | cy.getConsent().should((consent) => { 10 | expect(consent).to.deep.contain({ 11 | state: { 12 | necessary: "true", 13 | preferences: "unset", 14 | statistics: "pending", 15 | marketing: "false", 16 | }, 17 | }); 18 | }); 19 | 20 | cy.get("body") 21 | .should("have.attr", "data-wtm-config", "/wtm/config/") 22 | .should("have.attr", "data-wtm-lazy", "/wtm/lazy/"); 23 | 24 | cy.get("link[href='/static/wagtail_tag_manager/wtm.bundle.css']"); 25 | cy.get("script[src='/static/wagtail_tag_manager/wtm.bundle.js']"); 26 | }); 27 | 28 | it("has a configured cookie bar", () => { 29 | cy.visit("/"); 30 | 31 | cy.get("#wtm_cookie_bar") 32 | .should("be.visible") 33 | .should("have.class", "cleanslate") 34 | .contains("This website uses cookies"); 35 | 36 | cy.get("#wtm_cookie_bar") 37 | .find("form") 38 | .should("have.class", "form") 39 | .should("have.attr", "method", "POST") 40 | .should("have.attr", "action", "/wtm/manage/"); 41 | 42 | cy.get("#wtm_cookie_bar") 43 | .find("form") 44 | .find("input[type='checkbox']") 45 | .then((results) => { 46 | expect(results).to.have.length(4); 47 | expect(results[0]).to.be.disabled; 48 | expect(results[1]).to.be.checked; 49 | expect(results[2]).to.be.checked; 50 | expect(results[3]).to.not.be.checked; 51 | }); 52 | 53 | cy.get("#wtm_cookie_bar") 54 | .find(".manage-link") 55 | .find("a") 56 | .should("have.attr", "href", "/wtm/manage/") 57 | .contains("Manage settings"); 58 | 59 | cy.get("#wtm_cookie_bar").find("input[type='submit']").should("have.value", "Save"); 60 | }); 61 | }); 62 | 63 | describe("The API", () => { 64 | it("returns a valid config", () => { 65 | cy.server(); 66 | cy.route("GET", "/wtm/config/*").as("config"); 67 | cy.visit("/"); 68 | 69 | cy.wait("@config").should((xhr) => { 70 | expect(xhr.status, "successful GET").to.equal(200); 71 | }); 72 | }); 73 | 74 | it("returns a valid lazy state", () => { 75 | cy.server(); 76 | cy.route("POST", "/wtm/lazy/").as("lazy"); 77 | cy.visit("/"); 78 | 79 | cy.wait("@lazy").should((xhr) => { 80 | expect(xhr.status, "successful POST").to.equal(200); 81 | }); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /tests/unit/test_consent.py: -------------------------------------------------------------------------------- 1 | import json 2 | from http.cookies import SimpleCookie 3 | 4 | import pytest 5 | 6 | from wagtail_tag_manager.consent import ResponseConsent 7 | 8 | 9 | @pytest.mark.django_db 10 | def test_handles_malformed_consent(client, site): 11 | client.cookies = SimpleCookie({"wtm": "malformed"}) 12 | 13 | response = client.post( 14 | "/wtm/lazy/", json.dumps({}), content_type="application/json" 15 | ) 16 | data = response.json() 17 | 18 | assert response.status_code == 200 19 | assert "tags" in data 20 | assert len(data["tags"]) == 0 21 | 22 | assert "wtm" in response.cookies 23 | 24 | consent = ResponseConsent(response) 25 | consent_state = consent.get_state() 26 | assert consent_state.get("necessary", "") == "true" 27 | assert consent_state.get("preferences", "") == "unset" 28 | assert consent_state.get("statistics", "") == "unset" 29 | assert consent_state.get("marketing", "") == "false" 30 | 31 | 32 | @pytest.mark.django_db 33 | def test_upgrades_legacy_consent_state(client, site): 34 | client.cookies = SimpleCookie( 35 | {"wtm": "necessary:true|preferences:unset|statistics:pending|marketing:false"}, 36 | ) 37 | 38 | response = client.post( 39 | "/wtm/lazy/", json.dumps({}), content_type="application/json" 40 | ) 41 | data = response.json() 42 | 43 | assert response.status_code == 200 44 | assert "tags" in data 45 | assert len(data["tags"]) == 0 46 | 47 | assert "wtm" in response.cookies 48 | 49 | consent = ResponseConsent(response) 50 | consent_state = consent.get_state() 51 | assert consent_state.get("necessary", "") == "true" 52 | assert consent_state.get("preferences", "") == "unset" 53 | assert consent_state.get("statistics", "") == "pending" 54 | assert consent_state.get("marketing", "") == "false" 55 | 56 | 57 | @pytest.mark.django_db 58 | def test_upgrades_legacy_consent_meta(client, site): 59 | client.cookies = SimpleCookie( 60 | { 61 | "wtm": "necessary:true|preferences:unset|statistics:pending|marketing:false", 62 | "wtm_id": "123", 63 | }, 64 | ) 65 | 66 | response = client.post( 67 | "/wtm/lazy/", json.dumps({}), content_type="application/json" 68 | ) 69 | data = response.json() 70 | 71 | assert response.status_code == 200 72 | assert "tags" in data 73 | assert len(data["tags"]) == 0 74 | 75 | assert "wtm" in response.cookies 76 | 77 | consent = ResponseConsent(response) 78 | consent_meta = consent.get_meta() 79 | assert consent_meta.get("id", "") == "123" 80 | -------------------------------------------------------------------------------- /tests/unit/test_wagtail_hooks.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from tests.factories.tag import TagFactory 4 | from tests.factories.trigger import TriggerFactory 5 | from tests.factories.constant import ConstantFactory 6 | from tests.factories.variable import VariableFactory 7 | from wagtail_tag_manager.wagtail_hooks import ( 8 | TagModelAdmin, 9 | TriggerModelAdmin, 10 | ConstantModelAdmin, 11 | VariableModelAdmin, 12 | ) 13 | 14 | 15 | @pytest.mark.django_db 16 | def test_name_display(): 17 | constant = ConstantFactory(description="Test") 18 | constant_model_admin = ConstantModelAdmin() 19 | 20 | description = constant_model_admin.name_display(constant) 21 | assert constant.description in description 22 | 23 | variable = VariableFactory(key="var", description="Test") 24 | variable_model_admin = VariableModelAdmin() 25 | 26 | description = variable_model_admin.name_display(variable) 27 | assert variable.description in description 28 | 29 | tag = TagFactory(description="Test") 30 | tag_model_admin = TagModelAdmin() 31 | 32 | description = tag_model_admin.name_display(tag) 33 | assert tag.description in description 34 | 35 | trigger = TriggerFactory(description="Test") 36 | trigger_model_admin = TriggerModelAdmin() 37 | 38 | description = trigger_model_admin.name_display(trigger) 39 | assert tag.description in description 40 | 41 | 42 | @pytest.mark.django_db 43 | def test_no_description_display(): 44 | constant = ConstantFactory() 45 | constant_model_admin = ConstantModelAdmin() 46 | 47 | description = constant_model_admin.name_display(constant) 48 | assert constant.name == description 49 | 50 | variable = VariableFactory(key="var") 51 | variable_model_admin = VariableModelAdmin() 52 | 53 | description = variable_model_admin.name_display(variable) 54 | assert variable.name == description 55 | 56 | tag = TagFactory() 57 | tag_model_admin = TagModelAdmin() 58 | 59 | description = tag_model_admin.name_display(tag) 60 | assert tag.name == description 61 | 62 | trigger = TriggerFactory() 63 | trigger_model_admin = TriggerModelAdmin() 64 | 65 | description = trigger_model_admin.name_display(trigger) 66 | assert trigger.name == description 67 | 68 | 69 | @pytest.mark.django_db 70 | def test_tag_count(): 71 | tag1 = TagFactory(name="tag1") 72 | tag2 = TagFactory(name="tag2") 73 | 74 | trigger = TriggerFactory() 75 | trigger.tags.add(tag1, tag2) 76 | trigger_model_admin = TriggerModelAdmin() 77 | 78 | result = trigger_model_admin.tags_count(trigger) 79 | assert result == "2 tag(s)" 80 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '43 19 * * 5' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript', 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v2 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v2 71 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Python CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-python/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | # use `-browsers` prefix for selenium tests, e.g. `3.6.1-browsers` 11 | - image: cimg/python:3.11 12 | 13 | # Specify service dependencies here if necessary 14 | # CircleCI maintains a library of pre-built images 15 | # documented at https://circleci.com/docs/2.0/circleci-images/ 16 | # - image: circleci/postgres:9.4 17 | 18 | working_directory: ~/repo 19 | 20 | steps: 21 | - run: 22 | name: Setup environment 23 | command: | 24 | sudo apt-get update 25 | sudo apt-get install -y libgtk2.0-0 libgtk-3-0 libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb 26 | curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/fossas/fossa-cli/master/install.sh | bash 27 | 28 | - checkout 29 | 30 | # Download and cache dependencies 31 | - restore_cache: 32 | keys: 33 | - v1-deps-{{ .Branch }}-{{ checksum "setup.py" }}-{{ checksum "sandbox/requirements.txt" }} 34 | - v1-deps-{{ .Branch }} 35 | # fallback to using the latest cache if no exact match is found 36 | - v1-deps- 37 | 38 | - run: 39 | name: Install Dependencies 40 | command: | 41 | python3 -m venv venv 42 | . venv/bin/activate 43 | pip install -r sandbox/requirements.txt 44 | 45 | - save_cache: 46 | paths: 47 | - ./venv 48 | - ~/.cache 49 | key: v1-deps-{{ .Branch }}-{{ checksum "setup.py" }}-{{ checksum "sandbox/requirements.txt" }} 50 | 51 | # - run: 52 | # command: | 53 | # . venv/bin/activate 54 | # fossa 55 | 56 | # run tests! 57 | # this example uses Django's built-in test-runner 58 | # other common Python testing frameworks include pytest and nose 59 | # https://pytest.org 60 | # https://nose.readthedocs.io 61 | - run: 62 | name: run tests 63 | command: | 64 | . venv/bin/activate 65 | py.test --nomigrations --reuse-db tests/ --cov=wagtail_tag_manager 66 | 67 | - run: 68 | name: coverage 69 | command: | 70 | . venv/bin/activate 71 | pip install codecov 72 | codecov 73 | 74 | - store_artifacts: 75 | path: test-reports 76 | destination: test-reports 77 | -------------------------------------------------------------------------------- /frontend/admin/widgets/codearea.ts: -------------------------------------------------------------------------------- 1 | import "./codearea.scss"; 2 | 3 | import "whatwg-fetch"; 4 | import * as CodeMirror from "codemirror"; 5 | import "codemirror/mode/django/django"; 6 | import "codemirror/addon/display/panel"; 7 | 8 | declare global { 9 | interface Window { 10 | wtm: any; 11 | } 12 | } 13 | 14 | interface VariableGroup { 15 | verbose_name: string; 16 | items: [VariableItem]; 17 | } 18 | 19 | interface VariableItem { 20 | name: string; 21 | description: string; 22 | key: string; 23 | } 24 | 25 | interface Editor extends CodeMirror.EditorFromTextArea { 26 | doc: CodeMirror.Doc; 27 | addPanel(el: HTMLElement, options: object): HTMLElement; 28 | } 29 | 30 | class Codearea { 31 | el: HTMLTextAreaElement; 32 | editor: Editor; 33 | 34 | constructor(el: HTMLTextAreaElement) { 35 | this.el = el; 36 | 37 | this.initialize(); 38 | } 39 | 40 | initialize() { 41 | this.editor = CodeMirror.fromTextArea(this.el, { mode: "django" }) as Editor; 42 | 43 | fetch("/wtm/variables/") 44 | .then((response) => { 45 | return response.json(); 46 | }) 47 | .then((data) => { 48 | this.addPanel(data); 49 | }); 50 | } 51 | 52 | addPanel(data: Array) { 53 | const panelEl = document.createElement("ul"); 54 | panelEl.classList.add("panel"); 55 | 56 | for (const group of data) { 57 | const groupEl = document.createElement("ul"); 58 | groupEl.classList.add("listing"); 59 | 60 | const h3 = document.createElement("h3"); 61 | h3.appendChild(document.createTextNode(group.verbose_name)); 62 | panelEl.appendChild(h3); 63 | 64 | for (let item of group.items) { 65 | const a = document.createElement("a"); 66 | a.appendChild(document.createTextNode(item.name)); 67 | a.href = "#"; 68 | a.title = item.description; 69 | a.dataset.key = item.key; 70 | 71 | a.addEventListener("click", (event) => { 72 | event.preventDefault(); 73 | const target = event.currentTarget as HTMLElement; 74 | this.editor.doc.replaceSelection(`{{ ${target.dataset.key} }}`, "end"); 75 | this.editor.focus(); 76 | }); 77 | 78 | const li = document.createElement("li"); 79 | li.appendChild(a); 80 | 81 | groupEl.appendChild(li); 82 | } 83 | 84 | panelEl.appendChild(groupEl); 85 | } 86 | 87 | this.editor.addPanel(panelEl, { position: "top" }); 88 | } 89 | } 90 | 91 | window.wtm = window.wtm || {}; 92 | window.wtm.initCodearea = (selector: string, currentScript: HTMLScriptElement) => { 93 | const context = currentScript ? currentScript.parentNode : document.body; 94 | const field = context.querySelector(selector) || document.body.querySelector(selector); 95 | new Codearea(field as HTMLTextAreaElement); 96 | }; 97 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/static/wagtail_tag_manager/sourcemaps/tag_form_view.bundle.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"tag_form_view.bundle.js","mappings":"MAAA,iBAKE,aACEA,KAAKC,WAAaC,SAASC,eAAe,kBAC1CH,KAAKI,eAAiBF,SAASC,eAAe,mBAE9CH,KAAKK,WAAaL,KAAKK,WAAWC,KAAKN,MACvCA,KAAKO,iBAAmBP,KAAKO,iBAAiBD,KAAKN,MAEnDA,KAAKC,WAAWO,iBAAiB,SAAUR,KAAKO,kBAEhDP,KAAKK,YACP,CAiCF,OA/BE,YAAAA,WAAA,WACEL,KAAKO,kBACP,EAEA,YAAAA,iBAAA,WAGgB,iBAFAP,KAAKC,WAAWQ,QAAQT,KAAKC,WAAWS,eAAeC,OAGnEX,KAAKI,eAAeQ,UAAW,EAC/B,GAAGC,QAAQC,KAAKd,KAAKI,gBAAgB,SAACW,GACf,eAAjBA,EAAOJ,QACTI,EAAOC,UAAW,EAEtB,IAEAhB,KAAKiB,YAAcf,SAASgB,cAAc,SAC1ClB,KAAKiB,YAAYE,GAAKnB,KAAKI,eAAee,GAC1CnB,KAAKiB,YAAYG,KAAOpB,KAAKI,eAAegB,KAC5CpB,KAAKiB,YAAYI,KAAO,SACxBrB,KAAKiB,YAAYN,MAAQ,aACzBX,KAAKI,eAAekB,WAAWC,aAC7BvB,KAAKiB,YACLjB,KAAKI,eAAekB,WAAWE,WAAW,MAG5CxB,KAAKI,eAAeQ,UAAW,EAC3BZ,KAAKiB,aACPjB,KAAKiB,YAAYQ,SAGvB,EACF,EAhDA,GAkDAvB,SAASM,iBAAiB,oBAAoB,WAC5C,IAAIkB,CACN,G","sources":["webpack://wagtail-tag-manager/./frontend/admin/tag_form_view.ts"],"sourcesContent":["class TagFormView {\n loadSelect: HTMLSelectElement;\n locationSelect: HTMLSelectElement;\n hiddenInput: HTMLInputElement;\n\n constructor() {\n this.loadSelect = document.getElementById(\"id_tag_loading\") as HTMLSelectElement;\n this.locationSelect = document.getElementById(\"id_tag_location\") as HTMLSelectElement;\n\n this.initialize = this.initialize.bind(this);\n this.handleLoadChange = this.handleLoadChange.bind(this);\n\n this.loadSelect.addEventListener(\"change\", this.handleLoadChange);\n\n this.initialize();\n }\n\n initialize() {\n this.handleLoadChange();\n }\n\n handleLoadChange() {\n const value = this.loadSelect.options[this.loadSelect.selectedIndex].value;\n\n if (value !== \"instant_load\") {\n this.locationSelect.disabled = true;\n [].forEach.call(this.locationSelect, (option: HTMLOptionElement) => {\n if (option.value === \"0_top_head\") {\n option.selected = true;\n }\n });\n\n this.hiddenInput = document.createElement(\"input\");\n this.hiddenInput.id = this.locationSelect.id;\n this.hiddenInput.name = this.locationSelect.name;\n this.hiddenInput.type = \"hidden\";\n this.hiddenInput.value = \"0_top_head\";\n this.locationSelect.parentNode.insertBefore(\n this.hiddenInput,\n this.locationSelect.parentNode.childNodes[0]\n );\n } else {\n this.locationSelect.disabled = false;\n if (this.hiddenInput) {\n this.hiddenInput.remove();\n }\n }\n }\n}\n\ndocument.addEventListener(\"DOMContentLoaded\", () => {\n new TagFormView();\n});\n"],"names":["this","loadSelect","document","getElementById","locationSelect","initialize","bind","handleLoadChange","addEventListener","options","selectedIndex","value","disabled","forEach","call","option","selected","hiddenInput","createElement","id","name","type","parentNode","insertBefore","childNodes","remove","TagFormView"],"sourceRoot":""} -------------------------------------------------------------------------------- /sandbox/sandbox/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static wagtailuserbar wtm_tags %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | {% block title %} 13 | {% if self.seo_title %}{{ self.seo_title }}{% else %}{{ self.title }}{% endif %} 14 | {% endblock %} 15 | {% block title_suffix %} 16 | {% with self.get_site.site_name as site_name %} 17 | {% if site_name %}- {{ site_name }}{% endif %} 18 | {% endwith %} 19 | {% endblock %} 20 | 21 | 22 | 23 | 24 | {# Global stylesheets #} 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {% block extra_css %} 33 | {% wtm_include "necessary" "css/dummy.css" %} 34 | {% endblock %} 35 | 36 | 37 | 38 | {% wagtailuserbar %} 39 | {% wtm_cookie_bar %} 40 | 41 |
42 | {% block content %}{% endblock %} 43 | 44 |
45 | 46 | {% wtm_include "necessary" %} 47 |

Necessary tags are being loaded.

48 | {% wtm_endinclude %} 49 | 50 | {% wtm_include "preferences" %} 51 |

Preference tags are being loaded.

52 | {% wtm_endinclude %} 53 | 54 | {% wtm_include "statistics" %} 55 |

Statistics tags are being loaded.

56 | {% wtm_endinclude %} 57 | 58 | {% wtm_include "marketing" %} 59 |

Marketing tags are being loaded.

60 | {% wtm_endinclude %} 61 |
62 | 63 | {% wtm_include "statistics" "dummy.html" %} 64 | 65 | {# Global javascript #} 66 | 67 | 68 | {% block extra_js %} 69 | {% wtm_include "marketing" "js/dummy.js" %} 70 | {% endblock %} 71 | 72 | 73 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/models/constants.py: -------------------------------------------------------------------------------- 1 | import django 2 | from django.db import models 3 | from django.conf import settings 4 | from django.dispatch import receiver 5 | from django.core.cache import cache 6 | from django.utils.html import mark_safe 7 | from wagtail.admin.panels import FieldPanel, FieldRowPanel, MultiFieldPanel 8 | from django.core.exceptions import ValidationError 9 | 10 | __version__ = django.get_version() 11 | if __version__.startswith("2"): 12 | from django.utils.translation import ugettext_lazy as _ 13 | else: 14 | from django.utils.translation import gettext_lazy as _ 15 | 16 | 17 | class Constant(models.Model): 18 | name = models.CharField(max_length=100, unique=True) 19 | description = models.TextField(null=True, blank=True) 20 | 21 | key = models.SlugField( 22 | max_length=255, 23 | unique=True, 24 | help_text=mark_safe( 25 | _( 26 | "The key that can be used in tags to include the value.
" 27 | "For example: {{ ga_id }}." 28 | ) 29 | ), 30 | ) 31 | value = models.CharField( 32 | max_length=255, 33 | help_text=_("The value to be rendered when this constant is included."), 34 | ) 35 | 36 | panels = [ 37 | FieldPanel("name", classname="full title"), 38 | FieldPanel("description", classname="full"), 39 | MultiFieldPanel( 40 | [FieldRowPanel([FieldPanel("key"), FieldPanel("value")])], heading=_("Data") 41 | ), 42 | ] 43 | 44 | def as_dict(self): 45 | return { 46 | "name": self.name, 47 | "description": self.description, 48 | "key": self.key, 49 | "value": self.value, 50 | } 51 | 52 | def get_value(self): 53 | return self.value 54 | 55 | @classmethod 56 | def create_context(cls): 57 | context = cache.get("wtm_constant_cache", {}) 58 | 59 | if not context: 60 | for constant in cls.objects.all(): 61 | context[constant.key] = constant.get_value() 62 | 63 | timeout = getattr(settings, "WTM_CACHE_TIMEOUT", 60 * 30) 64 | cache.set("wtm_constant_cache", context, timeout) 65 | 66 | return context 67 | 68 | def clean(self): 69 | from wagtail_tag_manager.models.variables import Variable 70 | 71 | if Variable.objects.filter(key=self.key).exists(): 72 | raise ValidationError( 73 | "A variable with the key '{}' already exists.".format(self.key) 74 | ) 75 | else: 76 | super().clean() 77 | 78 | def save( 79 | self, force_insert=False, force_update=False, using=None, update_fields=None 80 | ): 81 | self.full_clean() 82 | return super().save(force_insert, force_update, using, update_fields) 83 | 84 | def __str__(self): 85 | return self.name 86 | 87 | 88 | @receiver(models.signals.post_save, sender=Constant) 89 | def handle_constant_save(sender, **kwargs): 90 | sender.create_context() # Update the cache 91 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/models/others.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | import django 4 | from django.db import models 5 | from django.utils.html import mark_safe 6 | from wagtail.admin.panels import FieldPanel, FieldRowPanel, MultiFieldPanel 7 | 8 | from wagtail_tag_manager.managers import CookieDeclarationQuerySet 9 | from wagtail_tag_manager.settings import TagTypeSettings 10 | 11 | __version__ = django.get_version() 12 | if __version__.startswith("2"): 13 | from django.utils.translation import ugettext_lazy as _ 14 | else: 15 | from django.utils.translation import gettext_lazy as _ 16 | 17 | 18 | class CookieDeclaration(models.Model): 19 | INSECURE_COOKIE = "http" 20 | SECURE_COOKIE = "https" 21 | SECURITY_CHOICES = ((INSECURE_COOKIE, _("HTTP")), (SECURE_COOKIE, _("HTTPS"))) 22 | 23 | cookie_type = models.CharField( 24 | max_length=100, 25 | choices=[ 26 | (tag_type, config.get("verbose_name")) 27 | for tag_type, config in TagTypeSettings.all().items() 28 | ], 29 | help_text=_("The type of functionality this cookie supports."), 30 | null=True, 31 | blank=True, 32 | ) 33 | name = models.CharField(max_length=255, help_text=_("The name of this cookie.")) 34 | domain = models.CharField( 35 | max_length=255, 36 | help_text=mark_safe( 37 | _( 38 | "The domain (including subdomain if applicable) of the cookie.
" 39 | "For example: .wagtail.io." 40 | ) 41 | ), 42 | ) 43 | purpose = models.TextField(help_text=_("What this cookie is being used for.")) 44 | duration = models.DurationField(null=True, blank=True) 45 | security = models.CharField( 46 | max_length=5, 47 | choices=SECURITY_CHOICES, 48 | default=INSECURE_COOKIE, 49 | help_text=_("Whether this cookie is secure or not."), 50 | ) 51 | 52 | objects = CookieDeclarationQuerySet.as_manager() 53 | 54 | panels = [ 55 | FieldPanel("name", classname="full title"), 56 | MultiFieldPanel( 57 | [ 58 | FieldPanel("cookie_type"), 59 | FieldPanel("purpose"), 60 | FieldPanel("duration"), 61 | FieldRowPanel([FieldPanel("domain"), FieldPanel("security")]), 62 | ], 63 | heading=_("General"), 64 | ), 65 | ] 66 | 67 | class Meta: 68 | ordering = ["domain", "cookie_type", "name"] 69 | unique_together = ("name", "domain") 70 | 71 | def save( 72 | self, force_insert=False, force_update=False, using=None, update_fields=None 73 | ): 74 | self.full_clean() 75 | return super().save(force_insert, force_update, using, update_fields) 76 | 77 | @property 78 | def expiration(self): 79 | return self.duration 80 | 81 | def __str__(self): 82 | return self.name 83 | 84 | 85 | class CookieConsent(models.Model): 86 | identifier = models.UUIDField(default=uuid.uuid4, editable=False) 87 | consent_state = models.TextField(editable=False) 88 | location = models.URLField(editable=False, max_length=2048) 89 | timestamp = models.DateTimeField(auto_now=False, auto_now_add=True, editable=False) 90 | -------------------------------------------------------------------------------- /.github/workflows/cypress.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Cypress 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: 8 | push: 9 | branches: [master] 10 | pull_request: 11 | branches: [master] 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v2 21 | 22 | - name: Setup Python 23 | uses: actions/setup-python@v3 24 | with: 25 | python-version: "3.10" 26 | cache: 'pip' 27 | 28 | - name: Cypress install 29 | uses: cypress-io/github-action@v4 30 | with: 31 | runTests: false 32 | 33 | - name: Install dependencies 34 | run: | 35 | make clean 36 | make requirements 37 | pip install -U -r sandbox/requirements.txt 38 | 39 | - name: Prepare database 40 | run: | 41 | sandbox/manage.py migrate 42 | sandbox/manage.py loaddata sandbox/exampledata/users.json 43 | sandbox/manage.py loaddata sandbox/exampledata/cms.json 44 | sandbox/manage.py loaddata sandbox/exampledata/default_tags.json 45 | 46 | - uses: actions/upload-artifact@v3 47 | with: 48 | name: database 49 | path: db.sqlite3 50 | 51 | run: 52 | runs-on: ubuntu-latest 53 | needs: build 54 | strategy: 55 | fail-fast: false 56 | matrix: 57 | browser: 58 | - chrome 59 | - firefox 60 | spec: 61 | - cypress/e2e/admin.js 62 | - cypress/e2e/cookie_bar.js 63 | - cypress/e2e/cookie_consent.js 64 | - cypress/e2e/home_page.js 65 | - cypress/e2e/manage_page.js 66 | - cypress/e2e/sandbox.js 67 | - cypress/e2e/tags.js 68 | - cypress/e2e/triggers.js 69 | 70 | steps: 71 | - name: Checkout 72 | uses: actions/checkout@v2 73 | 74 | - name: Setup Python 75 | uses: actions/setup-python@v3 76 | with: 77 | python-version: "3.10" 78 | cache: 'pip' 79 | 80 | - name: Install dependencies 81 | run: | 82 | make clean 83 | make requirements 84 | pip install -U -r sandbox/requirements.txt 85 | 86 | - uses: actions/download-artifact@v3 87 | with: 88 | name: database 89 | 90 | - name: Cypress run 91 | uses: cypress-io/github-action@v4 92 | with: 93 | install: false 94 | start: make run_test_sandbox 95 | wait-on: "http://localhost:8000" 96 | record: false 97 | browser: ${{ matrix.browser }} 98 | spec: ${{ matrix.spec }} 99 | env: 100 | ENVIRONMENT: test 101 | ACTIONS_ALLOW_UNSECURE_COMMANDS: true 102 | 103 | # Kills the sandbox 104 | - name: Kill sandbox 105 | run: kill $(jobs -p) || true 106 | 107 | - uses: actions/upload-artifact@v2 108 | if: failure() 109 | with: 110 | name: cypress-screenshots 111 | path: cypress/screenshots 112 | 113 | - uses: actions/upload-artifact@v2 114 | if: always() 115 | with: 116 | name: cypress-videos 117 | path: cypress/videos 118 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at jasper.berghoef@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /cypress/e2e/cookie_consent.js: -------------------------------------------------------------------------------- 1 | describe("Cookie consent", () => { 2 | it("will be registered", () => { 3 | cy.visit("/"); 4 | 5 | cy.get("#wtm_cookie_bar").should("be.visible"); 6 | cy.get("#wtm_cookie_bar input#id_marketing").click(); 7 | cy.get("#wtm_cookie_bar input[type='submit']").click(); 8 | 9 | cy.visit("/cms/reports/cookie-consent/"); 10 | cy.getConsent().should((consent) => { 11 | cy.get(".listing tbody tr:first td > b").contains(consent.meta.id); 12 | }); 13 | }); 14 | 15 | it("will be invalidated", () => { 16 | cy.on("uncaught:exception", (err, runnable) => { 17 | return false; 18 | }); 19 | 20 | // Configure condition page 21 | cy.visit("/cms/settings/wagtail_tag_manager/cookieconsentsettings/1/"); 22 | cy.get("button[data-chooser-action-choose]").click({ force: true, multiple: true }); 23 | cy.get(".modal-content").should("be.visible"); 24 | cy.get("a[data-title='Wagtail Tag Manager']").click({ 25 | force: true, 26 | multiple: true, 27 | }); 28 | cy.get(".actions button[type='submit']").click(); 29 | 30 | // Register consent 31 | cy.visit("/"); 32 | cy.get("#wtm_cookie_bar input[type='submit']").click(); 33 | cy.getConsent().should((consent) => { 34 | expect(consent).to.deep.contain({ 35 | state: { 36 | necessary: "true", 37 | preferences: "true", 38 | statistics: "true", 39 | marketing: "false", 40 | }, 41 | }); 42 | }); 43 | 44 | // Change homepage 45 | cy.visit("/cms/pages/2/edit/"); 46 | cy.get("[data-w-dropdown-target='toggle']").click({ force: true, multiple: true }); 47 | cy.get("[name='action-publish']").click({ force: true }); 48 | 49 | // Visit homepage 50 | cy.visit("/"); 51 | cy.getConsent().should((consent) => { 52 | expect(consent).to.deep.contain({ 53 | state: { 54 | necessary: "true", 55 | preferences: "unset", 56 | statistics: "pending", 57 | marketing: "false", 58 | }, 59 | }); 60 | }); 61 | }); 62 | 63 | it("will not be invalidated", () => { 64 | cy.on("uncaught:exception", (err, runnable) => { 65 | return false; 66 | }); 67 | 68 | // Remove condition page 69 | cy.visit("/cms/settings/wagtail_tag_manager/cookieconsentsettings/1/"); 70 | cy.get("button[aria-label='Actions']").click({ force: true }); 71 | cy.get("button[data-chooser-action-clear]").click({ force: true }); 72 | cy.get(".actions button[type='submit']").click(); 73 | 74 | // Register consent 75 | cy.visit("/"); 76 | cy.get("#wtm_cookie_bar input[type='submit']").click(); 77 | cy.getConsent().should((consent) => { 78 | expect(consent).to.deep.contain({ 79 | state: { 80 | necessary: "true", 81 | preferences: "true", 82 | statistics: "true", 83 | marketing: "false", 84 | }, 85 | }); 86 | }); 87 | 88 | // Change homepage 89 | cy.visit("/cms/pages/2/edit/"); 90 | cy.get("[data-w-dropdown-target='toggle']").click({ force: true, multiple: true }); 91 | cy.get("[name='action-publish']").click({ force: true }); 92 | 93 | // Visit homepage 94 | cy.visit("/"); 95 | cy.getConsent().should((consent) => { 96 | expect(consent).to.deep.contain({ 97 | state: { 98 | necessary: "true", 99 | preferences: "true", 100 | statistics: "true", 101 | marketing: "false", 102 | }, 103 | }); 104 | }); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import django 4 | 5 | __version__ = django.get_version() 6 | if __version__.startswith("2"): 7 | from django.utils.translation import ugettext_lazy as _ 8 | else: 9 | from django.utils.translation import gettext_lazy as _ 10 | 11 | DATABASES = { 12 | "default": { 13 | "ENGINE": os.environ.get("DATABASE_ENGINE", "django.db.backends.sqlite3"), 14 | "NAME": os.environ.get("DATABASE_NAME", "db.sqlite3"), 15 | } 16 | } 17 | 18 | ALLOWED_HOSTS = ["localhost"] 19 | 20 | CACHES = { 21 | "default": { 22 | "BACKEND": "django.core.cache.backends.locmem.LocMemCache", 23 | "LOCATION": "unique-snowflake", 24 | } 25 | } 26 | 27 | SECRET_KEY = "not needed" 28 | SESSION_ENGINE = "django.contrib.sessions.backends.cached_db" 29 | 30 | ROOT_URLCONF = "tests.site.urls" 31 | 32 | STATIC_URL = "/static/" 33 | 34 | STATICFILES_FINDERS = ("django.contrib.staticfiles.finders.AppDirectoriesFinder",) 35 | 36 | USE_TZ = False 37 | 38 | TESTS_ROOT = os.path.dirname(os.path.abspath(__file__)) 39 | TEMPLATES = [ 40 | { 41 | "BACKEND": "django.template.backends.django.DjangoTemplates", 42 | "DIRS": [os.path.join(TESTS_ROOT, "site", "templates")], 43 | "APP_DIRS": True, 44 | "OPTIONS": { 45 | "context_processors": [ 46 | "django.template.context_processors.debug", 47 | "django.template.context_processors.request", 48 | "django.contrib.auth.context_processors.auth", 49 | "django.contrib.messages.context_processors.messages", 50 | "django.template.context_processors.request", 51 | ], 52 | "debug": True, 53 | }, 54 | } 55 | ] 56 | 57 | MIDDLEWARE = [ 58 | "django.middleware.common.CommonMiddleware", 59 | "django.contrib.sessions.middleware.SessionMiddleware", 60 | "django.middleware.csrf.CsrfViewMiddleware", 61 | "django.contrib.auth.middleware.AuthenticationMiddleware", 62 | "django.contrib.messages.middleware.MessageMiddleware", 63 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 64 | ] 65 | 66 | try: 67 | from wagtail.middleware import SiteMiddleware # noqa 68 | 69 | MIDDLEWARE = MIDDLEWARE + [ 70 | "wagtail.middleware.SiteMiddleware", 71 | "wagtail_tag_manager.middleware.CookieConsentMiddleware", 72 | "wagtail_tag_manager.middleware.TagManagerMiddleware", 73 | ] 74 | except: # noqa: E722 75 | MIDDLEWARE = MIDDLEWARE + [ 76 | "wagtail.contrib.legacy.sitemiddleware.SiteMiddleware", 77 | "wagtail_tag_manager.middleware.CookieConsentMiddleware", 78 | "wagtail_tag_manager.middleware.TagManagerMiddleware", 79 | ] 80 | 81 | INSTALLED_APPS = ( 82 | "django.contrib.admin", 83 | "django.contrib.auth", 84 | "django.contrib.contenttypes", 85 | "django.contrib.messages", 86 | "django.contrib.sessions", 87 | "django.contrib.sites", 88 | "django.contrib.staticfiles", 89 | "wagtail.contrib.forms", 90 | "wagtail.contrib.redirects", 91 | "wagtail.embeds", 92 | "wagtail.sites", 93 | "wagtail.users", 94 | "wagtail.snippets", 95 | "wagtail.documents", 96 | "wagtail.images", 97 | "wagtail.search", 98 | "wagtail.admin", 99 | "wagtail", 100 | "wagtail_modeladmin", 101 | "modelcluster", 102 | "taggit", 103 | "wagtail_tag_manager", 104 | "tests.site.pages", 105 | ) 106 | 107 | PASSWORD_HASHERS = ( 108 | "django.contrib.auth.hashers.MD5PasswordHasher", # don't use the intentionally slow default password hasher 109 | ) 110 | 111 | WAGTAIL_SITE_NAME = "wagtail-tag-manager test" 112 | 113 | WTM_TAG_TYPES = { 114 | # key, verbose name, setting 115 | "necessary": (_("Necessary"), "required"), 116 | "preferences": (_("Preferences"), "initial"), 117 | "statistics": (_("Statistics"), "delayed"), 118 | "marketing": (_("Marketing"), ""), 119 | } 120 | 121 | WTM_SUMMARY_PANELS = True 122 | -------------------------------------------------------------------------------- /frontend/client/components/tag_manager.ts: -------------------------------------------------------------------------------- 1 | import Cookies from "js-cookie"; 2 | import "whatwg-fetch"; 3 | 4 | import CookieBar from "./cookie_bar"; 5 | import TriggerMonitor from "./trigger_monitor"; 6 | 7 | import { Trigger } from "./trigger_monitor"; 8 | import type { WTMWindow, Tag, Meta, Cookie } from "../../types"; 9 | 10 | export default class TagManager { 11 | window: WTMWindow = window as WTMWindow; 12 | 13 | configUrl: string; 14 | lazyUrl: string; 15 | 16 | showCookiebar: boolean; 17 | requestInit: RequestInit; 18 | data: { [s: string]: any } = {}; 19 | config: { [s: string]: any } = {}; 20 | meta: Meta = {}; 21 | state: { [s: string]: any } = {}; 22 | 23 | constructor() { 24 | const { body } = document; 25 | 26 | this.configUrl = body.getAttribute("data-wtm-config") || this.window.wtm.config_url; 27 | this.lazyUrl = body.getAttribute("data-wtm-lazy") || this.window.wtm.lazy_url; 28 | 29 | this.requestInit = { 30 | method: "GET", 31 | mode: "cors", 32 | cache: "no-cache", 33 | credentials: "same-origin", 34 | headers: { 35 | "Content-Type": "application/json; charset=utf-8", 36 | "X-CSRFToken": Cookies.get("csrftoken"), 37 | }, 38 | redirect: "follow", 39 | referrer: "no-referrer", 40 | }; 41 | 42 | this.showCookiebar = false; 43 | this.initialize(); 44 | } 45 | 46 | initialize() { 47 | this.loadConfig(() => { 48 | const cookie = Cookies.get("wtm"); 49 | if (cookie) { 50 | const { meta, state } = window.wtm.consent() as Cookie; 51 | this.meta = meta; 52 | this.state = state; 53 | 54 | this.validate(); 55 | this.loadData(null); 56 | } 57 | }); 58 | } 59 | 60 | validate() { 61 | // Verify the browser allows cookies. 62 | let enabled = navigator.cookieEnabled; 63 | if (!enabled) { 64 | Cookies.set("wtm_verification", "verification"); 65 | enabled = Cookies.get("wtm_verification") !== undefined; 66 | } 67 | 68 | if (enabled) { 69 | Object.keys(this.config.tag_types).forEach((tagType) => { 70 | if ( 71 | this.state[tagType] === "unset" || 72 | this.state[tagType] == "none" || 73 | this.state[tagType] == "pending" 74 | ) { 75 | this.showCookiebar = true; 76 | } 77 | }); 78 | } 79 | 80 | if (this.showCookiebar) { 81 | new CookieBar(this); 82 | } 83 | 84 | if (this.config.triggers && this.config.triggers.length > 0) { 85 | const { triggers } = this.config; 86 | new TriggerMonitor(this, triggers); 87 | } 88 | } 89 | 90 | loadConfig(callback?: Function) { 91 | fetch(this.configUrl, this.requestInit) 92 | .then((response) => response.json()) 93 | .then((json) => { 94 | this.config = json; 95 | if (callback) callback(); 96 | }) 97 | .catch((error) => { 98 | console.error(error); 99 | }); 100 | } 101 | 102 | loadData(trigger: Trigger, callback?: Function) { 103 | fetch(this.lazyUrl, { 104 | ...this.requestInit, 105 | method: "POST", 106 | body: JSON.stringify({ ...window.location, trigger }), 107 | }) 108 | .then((response) => response.json()) 109 | .then((json) => { 110 | this.data = json; 111 | this.handleLoad(); 112 | if (callback) callback(); 113 | }) 114 | .catch((error) => { 115 | console.error(error); 116 | }); 117 | } 118 | 119 | handleLoad() { 120 | this.data.tags.forEach((tag: Tag) => { 121 | const element = document.createElement(tag.name); 122 | for (let property in tag.attributes) { 123 | if (tag.attributes.hasOwnProperty(property)) { 124 | element.setAttribute(property, tag.attributes[property]); 125 | } 126 | } 127 | element.appendChild(document.createTextNode(tag.string)); 128 | document.head.appendChild(element); 129 | }); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/settings.py: -------------------------------------------------------------------------------- 1 | import django 2 | from django.db import models 3 | from django.apps import apps 4 | from django.conf import settings 5 | from wagtail.fields import RichTextField 6 | from django.utils.text import slugify 7 | from wagtail.admin.panels import FieldPanel, PageChooserPanel 8 | from wagtail.contrib.settings.models import BaseSiteSetting, register_setting 9 | 10 | __version__ = django.get_version() 11 | if __version__.startswith("2"): 12 | from django.utils.translation import ugettext_lazy as _ 13 | else: 14 | from django.utils.translation import gettext_lazy as _ 15 | 16 | SETTING_DEFAULT = "" 17 | SETTING_REQUIRED = "required" 18 | SETTING_INITIAL = "initial" 19 | SETTING_DELAYED = "delayed" 20 | 21 | DEFAULT_SETTINGS = { 22 | "necessary": (_("Necessary"), SETTING_REQUIRED), 23 | "preferences": (_("Preferences"), SETTING_INITIAL), 24 | "statistics": (_("Statistics"), SETTING_INITIAL), 25 | "marketing": (_("Marketing"), SETTING_DEFAULT), 26 | } 27 | 28 | 29 | class TagTypeSettings: 30 | def __init__(self): 31 | self.SETTINGS = {} 32 | 33 | @staticmethod 34 | def all(): 35 | tag_type_settings = getattr(settings, "WTM_TAG_TYPES", DEFAULT_SETTINGS) 36 | return { 37 | slugify(tag_type): {"verbose_name": config[0], "value": config[1]} 38 | for tag_type, config in tag_type_settings.items() 39 | } 40 | 41 | def get(self, tag_type): 42 | if not tag_type or tag_type not in self.all(): 43 | raise ValueError(_("Provide a valid `tag_type`.")) 44 | return self.all().get(tag_type, "") 45 | 46 | def include(self, value, *args, **kwargs): 47 | self.SETTINGS.update( 48 | { 49 | tag_type: config 50 | for tag_type, config in self.all().items() 51 | if config.get("value") == value 52 | } 53 | ) 54 | 55 | return self 56 | 57 | def exclude(self, value, *args, **kwargs): 58 | if not self.SETTINGS: 59 | self.SETTINGS = self.all() 60 | 61 | remove = [] 62 | for tag_type, config in self.SETTINGS.items(): 63 | if config.get("value") == value: 64 | remove.append(tag_type) 65 | 66 | for item in remove: 67 | self.SETTINGS.pop(item, None) 68 | 69 | return self 70 | 71 | def result(self): 72 | return self.SETTINGS 73 | 74 | 75 | class CookieBarSettings(BaseSiteSetting): 76 | title = models.CharField( 77 | max_length=50, 78 | null=True, 79 | blank=True, 80 | help_text=_( 81 | "The title that should appear on the cookie bar. " 82 | "Leave empty for the default value." 83 | ), 84 | ) 85 | text = RichTextField( 86 | null=True, 87 | blank=True, 88 | help_text=_( 89 | "The text that should appear on the cookie bar. " 90 | "Leave empty for the default value." 91 | ), 92 | ) 93 | 94 | panels = [FieldPanel("title", classname="full title"), FieldPanel("text")] 95 | 96 | 97 | class CookieConsentSettings(BaseSiteSetting): 98 | select_related = ["conditions_page"] 99 | 100 | conditions_page = models.ForeignKey( 101 | "wagtailcore.Page", 102 | null=True, 103 | blank=True, 104 | on_delete=models.SET_NULL, 105 | related_name="+", 106 | help_text=_( 107 | "Set the page describing your privacy policy. " 108 | "Every time it changes, the consent given before will be invalidated." 109 | ), 110 | ) 111 | 112 | panels = [ 113 | PageChooserPanel("conditions_page"), 114 | ] 115 | 116 | def get_timestamp(self): 117 | if self.conditions_page: 118 | return self.conditions_page.last_published_at 119 | 120 | return None 121 | 122 | 123 | if apps.is_installed("wagtail.contrib.settings"): 124 | register_setting(model=CookieBarSettings) 125 | register_setting(model=CookieConsentSettings) 126 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/middleware.py: -------------------------------------------------------------------------------- 1 | from bs4 import BeautifulSoup 2 | from django.conf import settings 3 | from django.urls import reverse 4 | from django.templatetags.static import static 5 | 6 | from wagtail_tag_manager.models import Tag 7 | from wagtail_tag_manager.consent import Consent 8 | from wagtail_tag_manager.strategy import TagStrategy 9 | 10 | 11 | class BaseMiddleware: 12 | def __init__(self, get_response): 13 | self.get_response = get_response 14 | 15 | 16 | class CookieConsentMiddleware(BaseMiddleware): 17 | def __call__(self, request): 18 | return self.get_response(request) 19 | 20 | def process_template_response(self, request, response): 21 | if ( 22 | getattr(request, "method", None) == "GET" 23 | and getattr(response, "status_code", None) == 200 24 | and not getattr(response, "streaming", False) 25 | ): 26 | strategy = TagStrategy(request) 27 | consent = Consent(request) 28 | consent.apply_state( 29 | {key: value for key, value in strategy.consent_state.items()} 30 | ) 31 | consent.refresh_consent(response) 32 | 33 | return response 34 | 35 | 36 | class TagManagerMiddleware(BaseMiddleware): 37 | def __call__(self, request): 38 | response = self.get_response(request) 39 | if "Content-Length" in response and not getattr(response, "streaming", False): 40 | response["Content-Length"] = len(response.content) 41 | return response 42 | 43 | def process_template_response(self, request, response): 44 | response.render() 45 | response = self._add_instant_tags(request, response) 46 | response = self._add_lazy_manager(response) 47 | return response 48 | 49 | def _add_instant_tags(self, request, response): 50 | if hasattr(response, "content") and getattr(settings, "WTM_INJECT_TAGS", True): 51 | strategy = TagStrategy(request) 52 | content = response.content.decode(response.charset) 53 | doc = BeautifulSoup(content, "html.parser") 54 | head = getattr(doc, "head", []) 55 | body = getattr(doc, "body", []) 56 | 57 | for tag in strategy.result: 58 | obj = tag.get("object") 59 | element = tag.get("element") 60 | 61 | if head and obj.tag_location == Tag.TOP_HEAD: 62 | head.insert(1, element) 63 | elif head and obj.tag_location == Tag.BOTTOM_HEAD: 64 | head.append(element) 65 | elif body and obj.tag_location == Tag.TOP_BODY: 66 | body.insert(1, element) 67 | elif body and obj.tag_location == Tag.BOTTOM_BODY: 68 | body.append(element) 69 | 70 | doc.head = head 71 | doc.body = body 72 | response.content = doc.encode(formatter=None) 73 | 74 | return response 75 | 76 | def _add_lazy_manager(self, response): 77 | if hasattr(response, "content"): 78 | content = response.content.decode(response.charset) 79 | doc = BeautifulSoup(content, "html.parser") 80 | 81 | if doc.body: 82 | doc.body["data-wtm-config"] = reverse("wtm:config") 83 | doc.body["data-wtm-lazy"] = reverse("wtm:lazy") 84 | 85 | if getattr(settings, "WTM_INJECT_STYLE", True): 86 | link = doc.new_tag("link") 87 | link["rel"] = "stylesheet" 88 | link["type"] = "text/css" 89 | link["href"] = static("wagtail_tag_manager/wtm.bundle.css") 90 | doc.body.append(link) 91 | 92 | if getattr(settings, "WTM_INJECT_SCRIPT", True): 93 | script = doc.new_tag("script") 94 | script["type"] = "text/javascript" 95 | script["src"] = static("wagtail_tag_manager/wtm.bundle.js") 96 | doc.body.append(script) 97 | 98 | response.content = doc.encode(formatter=None) 99 | 100 | return response 101 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/migrations/0013_auto_20190506_0705.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.8 on 2019-05-06 07:05 2 | 3 | import modelcluster.fields 4 | import django.db.models.deletion 5 | from django.db import models, migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('wagtail_tag_manager', '0012_auto_20190501_1118'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='TriggerCondition', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('sort_order', models.IntegerField(blank=True, editable=False, null=True)), 20 | ('variable', models.CharField(max_length=255, null=True)), 21 | ('condition_type', models.CharField(choices=[('Text', (('exact_match', 'exact match'), ('not_exact_match', 'not exact match'), ('contains', 'contains'), ('not_contains', 'does not contain'), ('starts_with', 'starts with'), ('not_starts_with', 'does not start with'), ('ends_with', 'ends with'), ('not_ends_with', 'does not end with'))), ('Regex', (('regex_match', 'matches regex'), ('not_regex_match', 'does not match regex'), ('regex_imatch', 'matches regex (case insensitive)'), ('not_regex_imatch', 'does not match regex (case insensitive)'))), ('Numbers', (('lower_than', 'is lower than'), ('lower_than_equal', 'is lower than or equal to'), ('greater_than', 'is greater than'), ('greater_than_equal', 'is greater than or equal to')))], default='contains', max_length=255)), 22 | ('value', models.CharField(max_length=255)), 23 | ], 24 | options={ 25 | 'ordering': ['sort_order'], 26 | 'abstract': False, 27 | }, 28 | ), 29 | migrations.AddField( 30 | model_name='trigger', 31 | name='active', 32 | field=models.BooleanField(default=True, help_text='Uncheck to disable this trigger from firing.'), 33 | ), 34 | migrations.AddField( 35 | model_name='trigger', 36 | name='description', 37 | field=models.TextField(blank=True, null=True), 38 | ), 39 | migrations.AddField( 40 | model_name='trigger', 41 | name='name', 42 | field=models.CharField(default='', max_length=100, unique=True), 43 | preserve_default=False, 44 | ), 45 | migrations.AddField( 46 | model_name='trigger', 47 | name='slug', 48 | field=models.SlugField(default='', editable=False, max_length=100, unique=True), 49 | preserve_default=False, 50 | ), 51 | migrations.AddField( 52 | model_name='trigger', 53 | name='tags', 54 | field=models.ManyToManyField(help_text='The tags to include when this trigger is fired.', to='wagtail_tag_manager.Tag'), 55 | ), 56 | migrations.AddField( 57 | model_name='trigger', 58 | name='trigger_type', 59 | field=models.CharField(choices=[('form_submit', 'Form submit'), ('history_change', 'History change'), ('javascript_error', 'JavaScript error'), ('Click', (('click_all_elements', 'Click on all elements'), ('click_some_elements+', 'Click on some elements'))), ('Visibility', (('visibility_once_per_page+', 'Monitor once per page'), ('visibility_once_per_element+', 'Monitor once per element'), ('visibility_recurring+', 'Monitor recurringingly'))), ('Scroll', (('scroll_vertical+', 'Scroll vertical'), ('scroll_horizontal+', 'Scroll horizontal'))), ('Timer', (('timer_timeout+', 'Timer with timeout'), ('timer_interval+', 'Timer with interval')))], default='form_submit', max_length=255), 60 | ), 61 | migrations.AddField( 62 | model_name='trigger', 63 | name='value', 64 | field=models.CharField(blank=True, help_text='Click: the query selector of the element(s).
Visibility: the query selector of the element(s).
Scroll: the distance after which to trigger as percentage.
Timer: the time in milliseconds after which to trigger.', max_length=255, null=True), 65 | ), 66 | migrations.AddField( 67 | model_name='triggercondition', 68 | name='trigger', 69 | field=modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='conditions', to='wagtail_tag_manager.Trigger'), 70 | ), 71 | ] 72 | -------------------------------------------------------------------------------- /cypress/e2e/manage_page.js: -------------------------------------------------------------------------------- 1 | beforeEach("clear wtm cookies", () => { 2 | cy.clearCookie("wtm", { timeout: 1000 }); 3 | }); 4 | 5 | describe("The manage page", () => { 6 | it("has a form", () => { 7 | cy.visit("/wtm/manage/"); 8 | 9 | cy.get("form:not(.form)") 10 | .scrollIntoView() 11 | .should("have.attr", "method", "POST") 12 | .should("have.attr", "action", "/wtm/manage/"); 13 | 14 | cy.get("form:not(.form)") 15 | .scrollIntoView() 16 | .find("input[type='checkbox']") 17 | .then((results) => { 18 | expect(results).to.have.length(4); 19 | expect(results[0]).to.be.disabled; 20 | expect(results[1]).to.be.checked; 21 | expect(results[2]).to.be.checked; 22 | expect(results[3]).to.not.be.checked; 23 | }); 24 | }); 25 | 26 | it("should not have a visible cookie bar", () => { 27 | cy.visit("/wtm/manage/"); 28 | 29 | cy.get("#wtm_cookie_bar").should("have.class", "hidden").should("not.be.visible"); 30 | }); 31 | 32 | it("can save default cookies", () => { 33 | cy.visit("/wtm/manage/"); 34 | 35 | cy.get("form:not(.form)").scrollIntoView(); 36 | cy.get("form:not(.form) input[type='submit']").click(); 37 | 38 | cy.getConsent().should((consent) => { 39 | expect(consent).to.deep.contain({ 40 | state: { 41 | necessary: "true", 42 | preferences: "true", 43 | statistics: "true", 44 | marketing: "false", 45 | }, 46 | }); 47 | }); 48 | }); 49 | 50 | it("can set only necesarry cookies", () => { 51 | cy.visit("/wtm/manage/"); 52 | 53 | cy.get("form:not(.form)").scrollIntoView(); 54 | cy.get("form:not(.form) input#id_preferences").click(); 55 | cy.get("form:not(.form) input#id_statistics").click(); 56 | cy.get("form:not(.form) input[type='submit']").click(); 57 | 58 | cy.getConsent().should((consent) => { 59 | expect(consent).to.deep.contain({ 60 | state: { 61 | necessary: "true", 62 | preferences: "false", 63 | statistics: "false", 64 | marketing: "false", 65 | }, 66 | }); 67 | }); 68 | }); 69 | 70 | it("can set only preference cookies", () => { 71 | cy.visit("/wtm/manage/"); 72 | 73 | cy.get("form:not(.form)").scrollIntoView(); 74 | cy.get("form:not(.form) input#id_statistics").click(); 75 | cy.get("form:not(.form) input[type='submit']").click(); 76 | 77 | cy.getConsent().should((consent) => { 78 | expect(consent).to.deep.contain({ 79 | state: { 80 | necessary: "true", 81 | preferences: "true", 82 | statistics: "false", 83 | marketing: "false", 84 | }, 85 | }); 86 | }); 87 | }); 88 | 89 | it("can set only statistical cookies", () => { 90 | cy.visit("/wtm/manage/"); 91 | 92 | cy.get("form:not(.form)").scrollIntoView(); 93 | cy.get("form:not(.form) input#id_preferences").click(); 94 | cy.get("form:not(.form) input[type='submit']").click(); 95 | 96 | cy.getConsent().should((consent) => { 97 | expect(consent).to.deep.contain({ 98 | state: { 99 | necessary: "true", 100 | preferences: "false", 101 | statistics: "true", 102 | marketing: "false", 103 | }, 104 | }); 105 | }); 106 | }); 107 | 108 | it("can set only marketing cookies", () => { 109 | cy.visit("/wtm/manage/"); 110 | 111 | cy.get("form:not(.form)").scrollIntoView(); 112 | cy.get("form:not(.form) input#id_preferences").click(); 113 | cy.get("form:not(.form) input#id_statistics").click(); 114 | cy.get("form:not(.form) input#id_marketing").click(); 115 | cy.get("form:not(.form) input[type='submit']").click(); 116 | 117 | cy.getConsent().should((consent) => { 118 | expect(consent).to.deep.contain({ 119 | state: { 120 | necessary: "true", 121 | preferences: "false", 122 | statistics: "false", 123 | marketing: "true", 124 | }, 125 | }); 126 | }); 127 | }); 128 | 129 | it("can enable all cookies", () => { 130 | cy.visit("/wtm/manage/"); 131 | 132 | cy.get("form:not(.form)").scrollIntoView(); 133 | cy.get("form:not(.form) input#id_marketing").click(); 134 | cy.get("form:not(.form) input[type='submit']").click(); 135 | 136 | cy.getConsent().should((consent) => { 137 | expect(consent).to.deep.contain({ 138 | state: { 139 | necessary: "true", 140 | preferences: "true", 141 | statistics: "true", 142 | marketing: "true", 143 | }, 144 | }); 145 | }); 146 | }); 147 | }); 148 | -------------------------------------------------------------------------------- /cypress/e2e/home_page.js: -------------------------------------------------------------------------------- 1 | beforeEach("clear wtm cookies", () => { 2 | cy.clearCookie("wtm", { timeout: 1000 }); 3 | }); 4 | 5 | describe("The home page", () => { 6 | it("can enable page tags", () => { 7 | cy.on("uncaught:exception", (err, runnable) => { 8 | return false; 9 | }); 10 | 11 | cy.visit("/cms/pages/2/edit/"); 12 | cy.get("a[href='#tab-settings']").click(); 13 | 14 | cy.contains("Lazy passive required").click(); 15 | cy.contains("Lazy passive initial").click(); 16 | cy.contains("Lazy passive delayed").click(); 17 | cy.contains("Lazy passive regular").click(); 18 | 19 | cy.get("[data-w-dropdown-target='toggle']").click({ force: true, multiple: true }); 20 | cy.get("[name='action-publish']").click(); 21 | 22 | cy.setConsent({ 23 | state: { 24 | necessary: "true", 25 | preferences: "true", 26 | statistics: "true", 27 | marketing: "true", 28 | }, 29 | }); 30 | cy.visit("/", { 31 | onBeforeLoad(win) { 32 | cy.stub(win.console, "info").as("consoleInfo"); 33 | }, 34 | }); 35 | 36 | cy.get("@consoleInfo").should("be.calledWith", "lazy passive required"); 37 | cy.get("@consoleInfo").should("be.calledWith", "lazy passive initial"); 38 | cy.get("@consoleInfo").should("be.calledWith", "lazy passive delayed"); 39 | cy.get("@consoleInfo").should("be.calledWith", "lazy passive regular"); 40 | }); 41 | 42 | it("will honor consent", () => { 43 | cy.setConsent({ 44 | state: { 45 | necessary: "true", 46 | preferences: "unset", 47 | statistics: "unset", 48 | marketing: "false", 49 | }, 50 | }); 51 | cy.visit("/", { 52 | onBeforeLoad(win) { 53 | cy.stub(win.console, "info").as("consoleInfo"); 54 | }, 55 | }); 56 | 57 | cy.get("@consoleInfo").should("be.calledWith", "lazy passive required"); 58 | cy.get("@consoleInfo").should("be.calledWith", "lazy passive initial"); 59 | cy.get("@consoleInfo").should("not.be.calledWith", "lazy passive delayed"); 60 | cy.get("@consoleInfo").should("not.be.calledWith", "lazy passive regular"); 61 | 62 | cy.setConsent({ 63 | state: { 64 | necessary: "false", 65 | preferences: "false", 66 | statistics: "delayed", 67 | marketing: "false", 68 | }, 69 | }); 70 | cy.visit("/", { 71 | onBeforeLoad(win) { 72 | cy.stub(win.console, "info").as("consoleInfo"); 73 | }, 74 | }); 75 | 76 | cy.get("@consoleInfo").should("be.calledWith", "lazy passive required"); 77 | cy.get("@consoleInfo").should("not.be.calledWith", "lazy passive initial"); 78 | cy.get("@consoleInfo").should("not.be.calledWith", "lazy passive delayed"); 79 | cy.get("@consoleInfo").should("not.be.calledWith", "lazy passive regular"); 80 | 81 | cy.setConsent({ 82 | state: { 83 | necessary: "true", 84 | preferences: "true", 85 | statistics: "true", 86 | marketing: "true", 87 | }, 88 | }); 89 | cy.visit("/", { 90 | onBeforeLoad(win) { 91 | cy.stub(win.console, "info").as("consoleInfo"); 92 | }, 93 | }); 94 | 95 | cy.get("@consoleInfo").should("be.calledWith", "lazy passive required"); 96 | cy.get("@consoleInfo").should("be.calledWith", "lazy passive initial"); 97 | cy.get("@consoleInfo").should("be.calledWith", "lazy passive delayed"); 98 | cy.get("@consoleInfo").should("be.calledWith", "lazy passive regular"); 99 | }); 100 | 101 | it("can disable page tags", () => { 102 | cy.on("uncaught:exception", (err, runnable) => { 103 | return false; 104 | }); 105 | 106 | cy.visit("/cms/pages/2/edit/"); 107 | cy.get("a[href='#tab-settings']").click(); 108 | 109 | cy.contains("Lazy passive required").click(); 110 | cy.contains("Lazy passive initial").click(); 111 | cy.contains("Lazy passive delayed").click(); 112 | cy.contains("Lazy passive regular").click(); 113 | 114 | cy.get("[data-w-dropdown-target='toggle']").click({ force: true, multiple: true }); 115 | cy.get("[name='action-publish']").click(); 116 | 117 | cy.setConsent({ 118 | state: { 119 | necessary: "true", 120 | preferences: "true", 121 | statistics: "true", 122 | marketing: "true", 123 | }, 124 | }); 125 | cy.visit("/", { 126 | onBeforeLoad(win) { 127 | cy.stub(win.console, "info").as("consoleInfo"); 128 | }, 129 | }); 130 | 131 | cy.get("@consoleInfo").should("not.be.calledWith", "lazy passive required"); 132 | cy.get("@consoleInfo").should("not.be.calledWith", "lazy passive initial"); 133 | cy.get("@consoleInfo").should("not.be.calledWith", "lazy passive delayed"); 134 | cy.get("@consoleInfo").should("not.be.calledWith", "lazy passive regular"); 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/models/variables.py: -------------------------------------------------------------------------------- 1 | import re 2 | import operator 3 | 4 | import django 5 | from django.db import models 6 | from django.utils.html import mark_safe 7 | from wagtail.admin.panels import FieldPanel, FieldRowPanel, MultiFieldPanel 8 | from django.core.exceptions import ValidationError 9 | 10 | from wagtail_tag_manager.decorators import get_variables 11 | 12 | __version__ = django.get_version() 13 | if __version__.startswith("2"): 14 | from django.utils.translation import ugettext_lazy as _ 15 | else: 16 | from django.utils.translation import gettext_lazy as _ 17 | 18 | 19 | class Variable(models.Model): 20 | TYPE_CHOICES = ( 21 | (_("HTTP"), (("_repath+", _("Path with regex")),)), 22 | (_("Other"), (("_cookie+", _("Cookie")),)), 23 | ) 24 | 25 | name = models.CharField(max_length=100, unique=True) 26 | description = models.TextField(null=True, blank=True) 27 | 28 | key = models.SlugField( 29 | max_length=255, 30 | unique=True, 31 | help_text=mark_safe( 32 | _( 33 | "The key that can be used in tags to include the value.
" 34 | "For example: {{ path }}." 35 | ) 36 | ), 37 | ) 38 | variable_type = models.CharField( 39 | max_length=255, 40 | choices=TYPE_CHOICES, 41 | help_text=mark_safe( 42 | _( 43 | "Path with regex: the path of the visited page after " 44 | "applying a regex search.
" 45 | "Cookie: the value of a cookie, when available." 46 | ) 47 | ), 48 | ) 49 | value = models.CharField( 50 | max_length=255, 51 | null=True, 52 | blank=True, 53 | help_text=mark_safe( 54 | _( 55 | "Path with regex: the pattern to search the path with.
" 56 | "Cookie: the name of the cookie." 57 | ) 58 | ), 59 | ) 60 | 61 | panels = [ 62 | FieldPanel("name", classname="full title"), 63 | FieldPanel("description", classname="full"), 64 | MultiFieldPanel( 65 | [ 66 | FieldRowPanel([FieldPanel("key"), FieldPanel("variable_type")]), 67 | FieldPanel("value"), 68 | ], 69 | heading=_("Data"), 70 | ), 71 | ] 72 | 73 | def as_dict(self): 74 | return { 75 | "name": self.name, 76 | "description": self.description, 77 | "key": self.key, 78 | "variable_type": self.variable_type, 79 | "value": self.value, 80 | } 81 | 82 | def get_repath(self, request): 83 | path = getattr(request, "path", None) 84 | if path and self.value: 85 | regex = re.compile(self.value) 86 | match = regex.search(request.get_full_path()) 87 | if match: 88 | return match.group() 89 | return "" 90 | return path 91 | 92 | def get_cookie(self, request): 93 | if request and hasattr(request, "COOKIES"): 94 | return request.COOKIES.get(self.value, "") 95 | return "" 96 | 97 | def get_value(self, request): 98 | variable_type = self.variable_type 99 | 100 | if variable_type.endswith("+"): 101 | variable_type = variable_type[:-1] 102 | 103 | if variable_type.startswith("_"): 104 | method = getattr(self, "get{}".format(variable_type)) 105 | return method(request) 106 | 107 | if "." in self.variable_type: 108 | return operator.attrgetter(str(self.variable_type))(request) 109 | 110 | return getattr(request, str(self.variable_type)) 111 | 112 | @classmethod 113 | def create_context(cls, request): 114 | context = {} 115 | 116 | for variable in [*get_variables(), *cls.objects.all()]: 117 | context[variable.key] = variable.get_value(request) 118 | 119 | return context 120 | 121 | def clean(self): 122 | from wagtail_tag_manager.models.constants import Constant 123 | 124 | if Constant.objects.filter(key=self.key).exists(): 125 | raise ValidationError( 126 | "A constant with the key '{}' already exists.".format(self.key) 127 | ) 128 | else: 129 | super().clean() 130 | 131 | if not self.variable_type.endswith("+"): 132 | self.value = "" 133 | 134 | return self 135 | 136 | def save( 137 | self, force_insert=False, force_update=False, using=None, update_fields=None 138 | ): 139 | self.full_clean() 140 | return super().save(force_insert, force_update, using, update_fields) 141 | 142 | def __str__(self): 143 | return self.name 144 | -------------------------------------------------------------------------------- /sandbox/exampledata/default_tags.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "wagtail_tag_manager.tag", 4 | "pk": 1, 5 | "fields": { 6 | "name": "Instant required", 7 | "description": "Loads instantly, is required", 8 | "auto_load": true, 9 | "tag_type": "necessary", 10 | "tag_location": "1_bottom_head", 11 | "tag_loading": "instant_load", 12 | "content": "" 13 | } 14 | }, 15 | { 16 | "model": "wagtail_tag_manager.tag", 17 | "pk": 2, 18 | "fields": { 19 | "name": "Instant initial", 20 | "description": "Loads instantly, is initial", 21 | "auto_load": true, 22 | "tag_type": "preferences", 23 | "tag_location": "1_bottom_head", 24 | "tag_loading": "instant_load", 25 | "content": "" 26 | } 27 | }, 28 | { 29 | "model": "wagtail_tag_manager.tag", 30 | "pk": 3, 31 | "fields": { 32 | "name": "Instant delayed", 33 | "description": "Loads instantly, is delayed", 34 | "auto_load": true, 35 | "tag_type": "statistics", 36 | "tag_location": "1_bottom_head", 37 | "tag_loading": "instant_load", 38 | "content": "" 39 | } 40 | }, 41 | { 42 | "model": "wagtail_tag_manager.tag", 43 | "pk": 4, 44 | "fields": { 45 | "name": "Instant regular", 46 | "description": "Loads instantly, is regular", 47 | "auto_load": true, 48 | "tag_type": "marketing", 49 | "tag_location": "1_bottom_head", 50 | "tag_loading": "instant_load", 51 | "content": "" 52 | } 53 | }, 54 | { 55 | "model": "wagtail_tag_manager.tag", 56 | "pk": 5, 57 | "fields": { 58 | "name": "Lazy required", 59 | "description": "Loads lazily, is required", 60 | "auto_load": true, 61 | "tag_type": "necessary", 62 | "tag_location": "0_top_head", 63 | "tag_loading": "lazy_load", 64 | "content": "" 65 | } 66 | }, 67 | { 68 | "model": "wagtail_tag_manager.tag", 69 | "pk": 6, 70 | "fields": { 71 | "name": "Lazy initial", 72 | "description": "Loads lazily, is initial", 73 | "auto_load": true, 74 | "tag_type": "preferences", 75 | "tag_location": "0_top_head", 76 | "tag_loading": "lazy_load", 77 | "content": "" 78 | } 79 | }, 80 | { 81 | "model": "wagtail_tag_manager.tag", 82 | "pk": 7, 83 | "fields": { 84 | "name": "Lazy delayed", 85 | "description": "Loads lazily, is delayed", 86 | "auto_load": true, 87 | "tag_type": "statistics", 88 | "tag_location": "0_top_head", 89 | "tag_loading": "lazy_load", 90 | "content": "" 91 | } 92 | }, 93 | { 94 | "model": "wagtail_tag_manager.tag", 95 | "pk": 8, 96 | "fields": { 97 | "name": "Lazy regular", 98 | "description": "Loads lazily, is regular", 99 | "auto_load": true, 100 | "tag_type": "marketing", 101 | "tag_location": "0_top_head", 102 | "tag_loading": "lazy_load", 103 | "content": "" 104 | } 105 | }, 106 | { 107 | "model": "wagtail_tag_manager.tag", 108 | "pk": 9, 109 | "fields": { 110 | "name": "Lazy passive required", 111 | "description": "Loads lazily and passive, is required", 112 | "auto_load": false, 113 | "tag_type": "necessary", 114 | "tag_location": "0_top_head", 115 | "tag_loading": "lazy_load", 116 | "content": "" 117 | } 118 | }, 119 | { 120 | "model": "wagtail_tag_manager.tag", 121 | "pk": 10, 122 | "fields": { 123 | "name": "Lazy passive initial", 124 | "description": "Loads lazily and passive, is initial", 125 | "auto_load": false, 126 | "tag_type": "preferences", 127 | "tag_location": "0_top_head", 128 | "tag_loading": "lazy_load", 129 | "content": "" 130 | } 131 | }, 132 | { 133 | "model": "wagtail_tag_manager.tag", 134 | "pk": 11, 135 | "fields": { 136 | "name": "Lazy passive delayed", 137 | "description": "Loads lazily and passive, is delayed", 138 | "auto_load": false, 139 | "tag_type": "statistics", 140 | "tag_location": "0_top_head", 141 | "tag_loading": "lazy_load", 142 | "content": "" 143 | } 144 | }, 145 | { 146 | "model": "wagtail_tag_manager.tag", 147 | "pk": 12, 148 | "fields": { 149 | "name": "Lazy passive regular", 150 | "description": "Loads lazily and passive, is regular", 151 | "auto_load": false, 152 | "tag_type": "marketing", 153 | "tag_location": "0_top_head", 154 | "tag_loading": "lazy_load", 155 | "content": "" 156 | } 157 | } 158 | ] 159 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/templates/wagtail_tag_manager/templatetags/cookie_bar.html: -------------------------------------------------------------------------------- 1 | {% load i18n wagtailcore_tags %} 2 | 3 | {% block content %} 4 | 100 | {% endblock content %} 101 | -------------------------------------------------------------------------------- /tests/unit/test_factories.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from tests.factories.tag import ( 4 | TagFactory, 5 | tag_lazy_marketing, 6 | tag_lazy_necessary, 7 | tag_lazy_preferences, 8 | tag_instant_marketing, 9 | tag_instant_necessary, 10 | tag_instant_preferences, 11 | ) 12 | from tests.factories.trigger import TriggerFactory, TriggerConditionFactory 13 | from tests.factories.constant import ConstantFactory 14 | from tests.factories.variable import VariableFactory 15 | from wagtail_tag_manager.models import ( 16 | Tag, 17 | Trigger, 18 | Constant, 19 | Variable, 20 | TriggerCondition, 21 | ) 22 | 23 | 24 | def get_expected_content(string): 25 | return '\n'.format(string) 26 | 27 | 28 | @pytest.mark.django_db 29 | def test_tag_create(): 30 | produced_tag = TagFactory() 31 | tag = Tag( 32 | name="necessary instant", 33 | content='', 34 | ) 35 | 36 | assert produced_tag.name == tag.name 37 | assert produced_tag.tag_type == tag.tag_type 38 | assert produced_tag.content == get_expected_content(tag.name) 39 | 40 | 41 | @pytest.mark.django_db 42 | def test_tag_instant_necessary(): 43 | produced_tag = tag_instant_necessary() 44 | tag = Tag( 45 | name="necessary instant", 46 | content='', 47 | ) 48 | 49 | assert produced_tag.name == tag.name 50 | assert produced_tag.tag_type == tag.tag_type 51 | assert produced_tag.content == get_expected_content(tag.name) 52 | 53 | 54 | @pytest.mark.django_db 55 | def test_tag_instant_preferences(): 56 | produced_tag = tag_instant_preferences() 57 | tag = Tag( 58 | name="preferences instant", 59 | tag_type="preferences", 60 | content='', 61 | ) 62 | 63 | assert produced_tag.name == tag.name 64 | assert produced_tag.tag_type == tag.tag_type 65 | assert produced_tag.content == get_expected_content(tag.name) 66 | 67 | 68 | @pytest.mark.django_db 69 | def test_tag_instant_marketing(): 70 | produced_tag = tag_instant_marketing() 71 | tag = Tag( 72 | name="marketing instant", 73 | tag_type="marketing", 74 | content='', 75 | ) 76 | 77 | assert produced_tag.name == tag.name 78 | assert produced_tag.tag_type == tag.tag_type 79 | assert produced_tag.content == get_expected_content(tag.name) 80 | 81 | 82 | @pytest.mark.django_db 83 | def test_tag_lazy_necessary(): 84 | produced_tag = tag_lazy_necessary() 85 | tag = Tag( 86 | name="necessary lazy", content='' 87 | ) 88 | 89 | assert produced_tag.name == tag.name 90 | assert produced_tag.tag_type == tag.tag_type 91 | assert produced_tag.content == get_expected_content(tag.name) 92 | 93 | 94 | @pytest.mark.django_db 95 | def test_tag_lazy_preferences(): 96 | produced_tag = tag_lazy_preferences() 97 | tag = Tag( 98 | name="preferences lazy", 99 | tag_type="preferences", 100 | content='', 101 | ) 102 | 103 | assert produced_tag.name == tag.name 104 | assert produced_tag.tag_type == tag.tag_type 105 | assert produced_tag.content == get_expected_content(tag.name) 106 | 107 | 108 | @pytest.mark.django_db 109 | def test_tag_lazy_marketing(): 110 | produced_tag = tag_lazy_marketing() 111 | tag = Tag( 112 | name="marketing lazy", 113 | tag_type="marketing", 114 | content='', 115 | ) 116 | 117 | assert produced_tag.name == tag.name 118 | assert produced_tag.tag_type == tag.tag_type 119 | assert produced_tag.content == get_expected_content(tag.name) 120 | 121 | 122 | @pytest.mark.django_db 123 | def test_constant_create(): 124 | produced_constant = ConstantFactory() 125 | constant = Constant(name="Constant", key="key", value="value") 126 | 127 | assert produced_constant.name == constant.name 128 | assert produced_constant.key == constant.key 129 | assert produced_constant.value == constant.value 130 | 131 | 132 | @pytest.mark.django_db 133 | def test_variable_create(): 134 | produced_variable = VariableFactory() 135 | variable = Variable( 136 | name="Variable", key="key", variable_type="_cookie+", value="wtm" 137 | ) 138 | 139 | assert produced_variable.name == variable.name 140 | assert produced_variable.key == variable.key 141 | assert produced_variable.variable_type == variable.variable_type 142 | assert produced_variable.value == variable.value 143 | 144 | 145 | @pytest.mark.django_db 146 | def test_trigger_create(): 147 | produced_trigger = TriggerFactory() 148 | trigger = Trigger(name="Trigger") 149 | 150 | assert produced_trigger.name == trigger.name 151 | 152 | 153 | @pytest.mark.django_db 154 | def test_trigger_condition_create(): 155 | produced_trigger = TriggerFactory() 156 | produced_trigger_condition = TriggerConditionFactory(trigger=produced_trigger) 157 | trigger = Trigger(name="Trigger") 158 | trigger_condition = TriggerCondition( 159 | variable="navigation_path", value="/", trigger=trigger 160 | ) 161 | 162 | assert produced_trigger.name == trigger.name 163 | assert produced_trigger_condition.value == trigger_condition.value 164 | -------------------------------------------------------------------------------- /src/wagtail_tag_manager/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.3 on 2018-11-06 08:14 2 | 3 | from typing import List 4 | 5 | from django.db import models, migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies: List = [] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Constant', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('name', models.CharField(max_length=100, unique=True)), 20 | ('description', models.TextField(blank=True, null=True)), 21 | ('key', models.SlugField(help_text='The key that can be used in tags to include the value.
For example: {{ ga_id }}.', max_length=255, unique=True)), 22 | ('value', models.CharField(help_text='The value to be rendered when this constant is included.', max_length=255)), 23 | ], 24 | ), 25 | migrations.CreateModel( 26 | name='Tag', 27 | fields=[ 28 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 29 | ('name', models.CharField(max_length=100, unique=True)), 30 | ('description', models.TextField(blank=True, null=True)), 31 | ('active', models.BooleanField(default=True, help_text='Uncheck to disable this tag from being included, or when using a trigger to include this tag.')), 32 | ('priority', models.SmallIntegerField(default=0, help_text='Define how early on this tag should load as compared to other tags. A higher number will load sooner. For example:
- A tag with a priority of 3 will load before a tag with priority 1.
- A tag with a priority 0 will load before a tag with priority -1.
Please note that with instanly loading tags, the priority is only compared to tags that load in the same document location.')), 33 | ('tag_type', models.CharField(choices=[('functional', 'Functional'), ('analytical', 'Analytical'), ('traceable', 'Traceable')], default='functional', help_text='The purpose of this tag. Will decide if and when this tag is loaded on a per-user basis.', max_length=10)), 34 | ('tag_location', models.CharField(choices=[('0_top_head', 'Top of head tag'), ('1_bottom_head', 'Bottom of head tag'), ('2_top_body', 'Top of body tag'), ('3_bottom_body', 'Bottom of body tag')], default='0_top_head', help_text='Where in the document this tag will be inserted. Only applicable for tags that load instantly.', max_length=14)), 35 | ('tag_loading', models.CharField(choices=[('instant_load', 'Instant'), ('lazy_load', 'Lazy')], default='instant_load', help_text='Instant: include this tag in the document when the initial request is made.
Lazy: include this tag after the page has finished loading.', max_length=12)), 36 | ('content', models.TextField(help_text='The tag to be added or script to be executed.Will assume the content is a script if no explicit tag has been added.')), 37 | ], 38 | options={ 39 | 'ordering': ['tag_loading', '-active', 'tag_location', '-priority'], 40 | }, 41 | ), 42 | migrations.CreateModel( 43 | name='Trigger', 44 | fields=[ 45 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 46 | ('name', models.CharField(max_length=100, unique=True)), 47 | ('description', models.TextField(blank=True, null=True)), 48 | ('active', models.BooleanField(default=True, help_text='Uncheck to disable this trigger from firing.')), 49 | ('pattern', models.CharField(help_text="The regex pattern to match the full url path with. Groups will be added to the included tag's context.", max_length=255)), 50 | ('tags', models.ManyToManyField(help_text='The tags to include when this trigger is fired.', to='wagtail_tag_manager.Tag')), 51 | ], 52 | ), 53 | migrations.CreateModel( 54 | name='Variable', 55 | fields=[ 56 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 57 | ('name', models.CharField(max_length=100, unique=True)), 58 | ('description', models.TextField(blank=True, null=True)), 59 | ('key', models.SlugField(help_text='The key that can be used in tags to include the value.
For example: {{ path }}.', max_length=255, unique=True)), 60 | ('variable_type', models.CharField(choices=[('HTTP', (('path', 'Path'), ('_repath+', 'Path with regex'))), ('User', (('user.pk', 'User'), ('session.session_key', 'Session'))), ('Wagtail', (('site', 'Site'),)), ('Other', (('_cookie+', 'Cookie'), ('_random', 'Random number')))], help_text='Path: the path of the visited page.
Path with regex: the path of the visited page after applying a regex search.
User: the ID of a user, when available.
Session: the session key.
Site: the name of the site.
Cookie: the value of a cookie, when available.
Random number: a random number.', max_length=255)), 61 | ('value', models.CharField(blank=True, help_text='Path with regex: the pattern to search the path with.
Cookie: the name of the cookie.', max_length=255, null=True)), 62 | ], 63 | ), 64 | ] 65 | --------------------------------------------------------------------------------