├── .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 |
| {% trans "Name" %} | 9 |{% trans "Description" %} | 10 |
|---|---|
| {{ tag_type.grouper }} | 16 ||
| {{ tag.name }} | 20 |{{ tag.description }} | 21 |
| 10 | {% trans 'Identifier' %} 11 | | 12 |13 | {% trans 'Location' %} 14 | | 15 |16 | {% trans 'Consent state' %} 17 | | 18 |19 | {% trans 'Timestamp' %} 20 | | 21 |
|---|---|---|---|
| 27 | {{cookie_consent.identifier}} 28 | | 29 |30 | {{cookie_consent.location}} 31 | | 32 |33 | {{cookie_consent.consent_state}} 34 | | 35 |36 | {{cookie_consent.timestamp}} 37 | | 38 |
{% 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 || {% trans "Name" %} | 9 |{% trans "Domain" %} | 10 |{% trans "Purpose" %} | 11 |{% trans "Expiration" %} | 12 |{% trans "Security" %} | 13 |
|---|---|---|---|---|
| 19 | {% if cookie_type.grouper %} 20 | {{ cookie_type.grouper }} 21 | {% else %} 22 | {% trans "Unclassified" %} 23 | {% endif %} 24 | | 25 |||||
| {{ cookie.name }} | 29 |{{ cookie.domain }} | 30 |{{ cookie.purpose }} | 31 |{{ cookie.expiration }} | 32 |{{ cookie.get_security_display }} | 33 |
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..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.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 |{{ 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..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).{{ 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.{{ 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:{{ 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.