django-cookie-consent 0.8.0__tar.gz → 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/MANIFEST.in +1 -0
- django_cookie_consent-1.0.0/PKG-INFO +96 -0
- {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/README.md +12 -13
- django_cookie_consent-1.0.0/cookie_consent/__init__.py +1 -0
- django_cookie_consent-1.0.0/cookie_consent/admin.py +65 -0
- {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/cookie_consent/cache.py +13 -8
- django_cookie_consent-1.0.0/cookie_consent/conf.py +35 -0
- django_cookie_consent-1.0.0/cookie_consent/forms.py +50 -0
- {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/cookie_consent/middleware.py +7 -6
- {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/cookie_consent/migrations/0001_initial.py +2 -1
- {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/cookie_consent/migrations/0003_alter_cookiegroup_varname.py +2 -2
- {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/cookie_consent/migrations/0004_cookie_natural_key.py +0 -1
- {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/cookie_consent/models.py +30 -20
- django_cookie_consent-1.0.0/cookie_consent/processor.py +77 -0
- django_cookie_consent-1.0.0/cookie_consent/py.typed +0 -0
- {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/cookie_consent/static/cookie_consent/cookiebar.module.js +8 -3
- django_cookie_consent-1.0.0/cookie_consent/static/cookie_consent/cookiebar.module.js.map +7 -0
- {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/cookie_consent/templates/cookie_consent/_cookie_group.html +4 -2
- django_cookie_consent-1.0.0/cookie_consent/templatetags/__init__.py +1 -0
- django_cookie_consent-1.0.0/cookie_consent/templatetags/cookie_consent_tags.py +88 -0
- django_cookie_consent-1.0.0/cookie_consent/urls.py +15 -0
- django_cookie_consent-1.0.0/cookie_consent/util.py +170 -0
- {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/cookie_consent/views.py +46 -40
- django_cookie_consent-1.0.0/django_cookie_consent.egg-info/PKG-INFO +96 -0
- {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/django_cookie_consent.egg-info/SOURCES.txt +4 -3
- {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/django_cookie_consent.egg-info/requires.txt +4 -7
- django_cookie_consent-1.0.0/pyproject.toml +170 -0
- django_cookie_consent-1.0.0/setup.cfg +4 -0
- django_cookie_consent-1.0.0/tests/test_admin.py +24 -0
- {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/tests/test_cache.py +0 -1
- {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/tests/test_middleware.py +7 -4
- {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/tests/test_models.py +30 -15
- {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/tests/test_settings.py +6 -3
- {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/tests/test_templatetags.py +2 -2
- {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/tests/test_util.py +23 -7
- django_cookie_consent-1.0.0/tests/test_views.py +283 -0
- django_cookie_consent-0.8.0/PKG-INFO +0 -125
- django_cookie_consent-0.8.0/cookie_consent/__init__.py +0 -1
- django_cookie_consent-0.8.0/cookie_consent/admin.py +0 -37
- django_cookie_consent-0.8.0/cookie_consent/conf.py +0 -27
- django_cookie_consent-0.8.0/cookie_consent/static/cookie_consent/cookiebar.js +0 -67
- django_cookie_consent-0.8.0/cookie_consent/static/cookie_consent/cookiebar.module.js.map +0 -7
- django_cookie_consent-0.8.0/cookie_consent/templatetags/__init__.py +0 -2
- django_cookie_consent-0.8.0/cookie_consent/templatetags/cookie_consent_tags.py +0 -179
- django_cookie_consent-0.8.0/cookie_consent/urls.py +0 -37
- django_cookie_consent-0.8.0/cookie_consent/util.py +0 -202
- django_cookie_consent-0.8.0/django_cookie_consent.egg-info/PKG-INFO +0 -125
- django_cookie_consent-0.8.0/pyproject.toml +0 -108
- django_cookie_consent-0.8.0/setup.cfg +0 -10
- django_cookie_consent-0.8.0/tests/test_legacy_javascript_cookiebar.py +0 -78
- django_cookie_consent-0.8.0/tests/test_views.py +0 -161
- {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/AUTHORS +0 -0
- {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/LICENSE +0 -0
- {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/cookie_consent/apps.py +0 -0
- {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/cookie_consent/fixtures/common_cookies.json +0 -0
- {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/cookie_consent/migrations/0002_auto__add_logitem.py +0 -0
- {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/cookie_consent/migrations/__init__.py +0 -0
- {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/cookie_consent/templates/cookie_consent/base.html +0 -0
- {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/cookie_consent/templates/cookie_consent/cookiegroup_list.html +0 -0
- {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/django_cookie_consent.egg-info/dependency_links.txt +0 -0
- {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/django_cookie_consent.egg-info/top_level.txt +0 -0
- {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/tests/test_cookie_group_model.py +0 -0
- {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/tests/test_cookie_model.py +0 -0
- {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/tests/test_javascript_cookiebar.py +0 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-cookie-consent
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Django cookie consent application
|
|
5
|
+
Author-email: Informatika Mihelac <bmihelac@mihelac.org>
|
|
6
|
+
License-Expression: BSD-2-Clause-first-lines
|
|
7
|
+
Project-URL: Documentation, https://django-cookie-consent.readthedocs.io/en/latest/
|
|
8
|
+
Project-URL: Changelog, https://github.com/django-commons/django-cookie-consent/blob/main/docs/changelog.rst
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/django-commons/django-cookie-consent/issues
|
|
10
|
+
Project-URL: Source Code, https://github.com/django-commons/django-cookie-consent
|
|
11
|
+
Keywords: cookies,cookie-consent,cookie bar
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Framework :: Django
|
|
14
|
+
Classifier: Framework :: Django :: 4.2
|
|
15
|
+
Classifier: Framework :: Django :: 5.2
|
|
16
|
+
Classifier: Framework :: Django :: 6.0
|
|
17
|
+
Classifier: Intended Audience :: Developers
|
|
18
|
+
Classifier: Operating System :: Unix
|
|
19
|
+
Classifier: Operating System :: MacOS
|
|
20
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
21
|
+
Classifier: Operating System :: OS Independent
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
27
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
28
|
+
Requires-Python: >=3.10
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Requires-Dist: django>=4.2
|
|
32
|
+
Requires-Dist: django-appconf
|
|
33
|
+
Provides-Extra: tests
|
|
34
|
+
Requires-Dist: pytest; extra == "tests"
|
|
35
|
+
Requires-Dist: pytest-cov; extra == "tests"
|
|
36
|
+
Requires-Dist: pytest-django; extra == "tests"
|
|
37
|
+
Requires-Dist: pytest-playwright; extra == "tests"
|
|
38
|
+
Requires-Dist: hypothesis; extra == "tests"
|
|
39
|
+
Requires-Dist: tox; extra == "tests"
|
|
40
|
+
Requires-Dist: ruff; extra == "tests"
|
|
41
|
+
Provides-Extra: docs
|
|
42
|
+
Requires-Dist: sphinx; extra == "docs"
|
|
43
|
+
Requires-Dist: sphinx-rtd-theme; extra == "docs"
|
|
44
|
+
Provides-Extra: release
|
|
45
|
+
Requires-Dist: bump-my-version; extra == "release"
|
|
46
|
+
Dynamic: license-file
|
|
47
|
+
|
|
48
|
+
Django cookie consent
|
|
49
|
+
=====================
|
|
50
|
+
|
|
51
|
+
Manage cookie information and let visitors give or reject consent for them.
|
|
52
|
+
|
|
53
|
+

|
|
54
|
+
[![Build status][badge:GithubActions:CI]][GithubActions:CI]
|
|
55
|
+
[![Code Quality][badge:GithubActions:CQ]][GithubActions:CQ]
|
|
56
|
+
[![Code style: ruff][badge:ruff]][ruff]
|
|
57
|
+
[![Test coverage][badge:codecov]][codecov]
|
|
58
|
+
[![Documentation][badge:docs]][docs]
|
|
59
|
+
|
|
60
|
+

|
|
61
|
+

|
|
62
|
+
[![PyPI version][badge:pypi]][pypi]
|
|
63
|
+
[![NPM version][badge:npm]][npm]
|
|
64
|
+
|
|
65
|
+
**Features**
|
|
66
|
+
|
|
67
|
+
* cookies and cookie groups are stored in models for easy management
|
|
68
|
+
through Django admin interface
|
|
69
|
+
* support for both opt-in and opt-out cookie consent schemes
|
|
70
|
+
* removing declined cookies (or non accepted when opt-in scheme is used)
|
|
71
|
+
* logging user actions when they accept and decline various cookies
|
|
72
|
+
* easy adding new cookies and seamlessly re-asking for consent for new cookies
|
|
73
|
+
|
|
74
|
+
Documentation
|
|
75
|
+
-------------
|
|
76
|
+
|
|
77
|
+
The documentation is hosted on [readthedocs][docs] and contains all instructions
|
|
78
|
+
to get started.
|
|
79
|
+
|
|
80
|
+
Alternatively, if the documentation is not available, you can consult or build the docs
|
|
81
|
+
from the `docs` directory in this repository.
|
|
82
|
+
|
|
83
|
+
[GithubActions:CI]: https://github.com/django-commons/django-cookie-consent/actions?query=workflow%3A%22Run+CI%22
|
|
84
|
+
[badge:GithubActions:CI]: https://github.com/django-commons/django-cookie-consent/workflows/Run%20CI/badge.svg
|
|
85
|
+
[GithubActions:CQ]: https://github.com/django-commons/django-cookie-consent/actions?query=workflow%3A%22Code+quality+checks%22
|
|
86
|
+
[badge:GithubActions:CQ]: https://github.com/django-commons/django-cookie-consent/workflows/Code%20quality%20checks/badge.svg
|
|
87
|
+
[ruff]: https://github.com/astral-sh/ruff
|
|
88
|
+
[badge:ruff]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json
|
|
89
|
+
[codecov]: https://codecov.io/gh/django-commons/django-cookie-consent
|
|
90
|
+
[badge:codecov]: https://codecov.io/gh/django-commons/django-cookie-consent/branch/main/graph/badge.svg
|
|
91
|
+
[docs]: https://django-cookie-consent.readthedocs.io/en/latest/?badge=latest
|
|
92
|
+
[badge:docs]: https://readthedocs.org/projects/django-cookie-consent/badge/?version=latest
|
|
93
|
+
[pypi]: https://pypi.org/project/django-cookie-consent/
|
|
94
|
+
[badge:pypi]: https://img.shields.io/pypi/v/django-cookie-consent.svg
|
|
95
|
+
[npm]: https://www.npmjs.com/package/django-cookie-consent
|
|
96
|
+
[badge:npm]: https://img.shields.io/npm/v/django-cookie-consent
|
|
@@ -3,18 +3,17 @@ Django cookie consent
|
|
|
3
3
|
|
|
4
4
|
Manage cookie information and let visitors give or reject consent for them.
|
|
5
5
|
|
|
6
|
-
[![Jazzband][badge:jazzband]][jazzband]
|
|
7
|
-
|
|
8
6
|

|
|
9
7
|
[![Build status][badge:GithubActions:CI]][GithubActions:CI]
|
|
10
8
|
[![Code Quality][badge:GithubActions:CQ]][GithubActions:CQ]
|
|
11
|
-
[![Code style:
|
|
9
|
+
[![Code style: ruff][badge:ruff]][ruff]
|
|
12
10
|
[![Test coverage][badge:codecov]][codecov]
|
|
13
11
|
[![Documentation][badge:docs]][docs]
|
|
14
12
|
|
|
15
13
|

|
|
16
14
|

|
|
17
15
|
[![PyPI version][badge:pypi]][pypi]
|
|
16
|
+
[![NPM version][badge:npm]][npm]
|
|
18
17
|
|
|
19
18
|
**Features**
|
|
20
19
|
|
|
@@ -34,17 +33,17 @@ to get started.
|
|
|
34
33
|
Alternatively, if the documentation is not available, you can consult or build the docs
|
|
35
34
|
from the `docs` directory in this repository.
|
|
36
35
|
|
|
37
|
-
[
|
|
38
|
-
[badge:
|
|
39
|
-
[GithubActions:
|
|
40
|
-
[badge:GithubActions:
|
|
41
|
-
[
|
|
42
|
-
[badge:
|
|
43
|
-
[
|
|
44
|
-
[badge:
|
|
45
|
-
[codecov]: https://codecov.io/gh/jazzband/django-cookie-consent
|
|
46
|
-
[badge:codecov]: https://codecov.io/gh/jazzband/django-cookie-consent/branch/master/graph/badge.svg
|
|
36
|
+
[GithubActions:CI]: https://github.com/django-commons/django-cookie-consent/actions?query=workflow%3A%22Run+CI%22
|
|
37
|
+
[badge:GithubActions:CI]: https://github.com/django-commons/django-cookie-consent/workflows/Run%20CI/badge.svg
|
|
38
|
+
[GithubActions:CQ]: https://github.com/django-commons/django-cookie-consent/actions?query=workflow%3A%22Code+quality+checks%22
|
|
39
|
+
[badge:GithubActions:CQ]: https://github.com/django-commons/django-cookie-consent/workflows/Code%20quality%20checks/badge.svg
|
|
40
|
+
[ruff]: https://github.com/astral-sh/ruff
|
|
41
|
+
[badge:ruff]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json
|
|
42
|
+
[codecov]: https://codecov.io/gh/django-commons/django-cookie-consent
|
|
43
|
+
[badge:codecov]: https://codecov.io/gh/django-commons/django-cookie-consent/branch/main/graph/badge.svg
|
|
47
44
|
[docs]: https://django-cookie-consent.readthedocs.io/en/latest/?badge=latest
|
|
48
45
|
[badge:docs]: https://readthedocs.org/projects/django-cookie-consent/badge/?version=latest
|
|
49
46
|
[pypi]: https://pypi.org/project/django-cookie-consent/
|
|
50
47
|
[badge:pypi]: https://img.shields.io/pypi/v/django-cookie-consent.svg
|
|
48
|
+
[npm]: https://www.npmjs.com/package/django-cookie-consent
|
|
49
|
+
[badge:npm]: https://img.shields.io/npm/v/django-cookie-consent
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from django.contrib import admin
|
|
2
|
+
from django.db.models import Count
|
|
3
|
+
from django.http.request import HttpRequest
|
|
4
|
+
from django.templatetags.l10n import localize
|
|
5
|
+
from django.templatetags.static import static
|
|
6
|
+
from django.utils.html import format_html
|
|
7
|
+
from django.utils.translation import gettext_lazy as _
|
|
8
|
+
|
|
9
|
+
from .conf import settings
|
|
10
|
+
from .models import Cookie, CookieGroup, LogItem
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@admin.register(Cookie)
|
|
14
|
+
class CookieAdmin(admin.ModelAdmin):
|
|
15
|
+
list_display = ("varname", "name", "cookiegroup", "path", "domain", "get_version")
|
|
16
|
+
search_fields = ("name", "domain", "cookiegroup__varname", "cookiegroup__name")
|
|
17
|
+
readonly_fields = ("varname",)
|
|
18
|
+
list_filter = ("cookiegroup",)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@admin.register(CookieGroup)
|
|
22
|
+
class CookieGroupAdmin(admin.ModelAdmin):
|
|
23
|
+
list_display = (
|
|
24
|
+
"varname",
|
|
25
|
+
"name",
|
|
26
|
+
"is_required",
|
|
27
|
+
"is_deletable",
|
|
28
|
+
"num_cookies",
|
|
29
|
+
"get_version",
|
|
30
|
+
)
|
|
31
|
+
search_fields = (
|
|
32
|
+
"varname",
|
|
33
|
+
"name",
|
|
34
|
+
)
|
|
35
|
+
list_filter = (
|
|
36
|
+
"is_required",
|
|
37
|
+
"is_deletable",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def get_queryset(self, request: HttpRequest):
|
|
41
|
+
qs = super().get_queryset(request)
|
|
42
|
+
return qs.annotate(num_cookies=Count("cookie"))
|
|
43
|
+
|
|
44
|
+
@admin.display(ordering="num_cookies", description=_("# cookies"))
|
|
45
|
+
def num_cookies(self, obj: CookieGroup):
|
|
46
|
+
if (count := obj.num_cookies) > 0:
|
|
47
|
+
return localize(count)
|
|
48
|
+
|
|
49
|
+
return format_html(
|
|
50
|
+
'{count} <img src="{src}" alt="{alt}">',
|
|
51
|
+
count=localize(count),
|
|
52
|
+
src=static("admin/img/icon-alert.svg"),
|
|
53
|
+
alt=_("Warning icon for missing cookies in cookie group."),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class LogItemAdmin(admin.ModelAdmin):
|
|
58
|
+
list_display = ("action", "cookiegroup", "version", "created")
|
|
59
|
+
list_filter = ("action", "cookiegroup")
|
|
60
|
+
readonly_fields = ("action", "cookiegroup", "version", "created")
|
|
61
|
+
date_hierarchy = "created"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if settings.COOKIE_CONSENT_LOG_ENABLED:
|
|
65
|
+
admin.site.register(LogItem, LogItemAdmin)
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
|
|
2
3
|
from django.core.cache import caches
|
|
3
4
|
|
|
4
5
|
from .conf import settings
|
|
5
|
-
from .models import CookieGroup
|
|
6
|
+
from .models import Cookie, CookieGroup
|
|
6
7
|
|
|
7
8
|
CACHE_KEY = "cookie_consent_cache"
|
|
8
9
|
CACHE_TIMEOUT = 60 * 60 # 60 minutes
|
|
@@ -19,17 +20,17 @@ def _get_cache():
|
|
|
19
20
|
return caches[settings.COOKIE_CONSENT_CACHE_BACKEND]
|
|
20
21
|
|
|
21
22
|
|
|
22
|
-
def delete_cache():
|
|
23
|
+
def delete_cache() -> None:
|
|
23
24
|
cache = _get_cache()
|
|
24
25
|
cache.delete(CACHE_KEY)
|
|
25
26
|
|
|
26
27
|
|
|
27
|
-
def _get_cookie_groups_from_db():
|
|
28
|
+
def _get_cookie_groups_from_db() -> Mapping[str, CookieGroup]:
|
|
28
29
|
qs = CookieGroup.objects.filter(is_required=False).prefetch_related("cookie_set")
|
|
29
30
|
return qs.in_bulk(field_name="varname")
|
|
30
31
|
|
|
31
32
|
|
|
32
|
-
def all_cookie_groups():
|
|
33
|
+
def all_cookie_groups() -> Mapping[str, CookieGroup]:
|
|
33
34
|
"""
|
|
34
35
|
Get all cookie groups that are optional.
|
|
35
36
|
|
|
@@ -37,16 +38,20 @@ def all_cookie_groups():
|
|
|
37
38
|
cache miss.
|
|
38
39
|
"""
|
|
39
40
|
cache = _get_cache()
|
|
40
|
-
|
|
41
|
+
result = cache.get_or_set(
|
|
41
42
|
CACHE_KEY, _get_cookie_groups_from_db, timeout=CACHE_TIMEOUT
|
|
42
43
|
)
|
|
44
|
+
assert result is not None
|
|
45
|
+
return result
|
|
43
46
|
|
|
44
47
|
|
|
45
|
-
def get_cookie_group(varname):
|
|
48
|
+
def get_cookie_group(varname: str) -> CookieGroup | None:
|
|
46
49
|
return all_cookie_groups().get(varname)
|
|
47
50
|
|
|
48
51
|
|
|
49
|
-
def get_cookie(cookie_group, name, domain):
|
|
52
|
+
def get_cookie(cookie_group: CookieGroup, name: str, domain: str) -> Cookie | None:
|
|
53
|
+
# loop over cookie set relation instead of doing a lookup query, as this should
|
|
54
|
+
# come from the cache and avoid hitting the database
|
|
50
55
|
for cookie in cookie_group.cookie_set.all():
|
|
51
56
|
if cookie.name == name and cookie.domain == domain:
|
|
52
57
|
return cookie
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
3
|
+
from django.conf import settings
|
|
4
|
+
from django.urls import reverse_lazy
|
|
5
|
+
from django.utils.functional import Promise
|
|
6
|
+
|
|
7
|
+
from appconf import AppConf
|
|
8
|
+
|
|
9
|
+
__all__ = ["settings"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CookieConsentConf(AppConf):
|
|
13
|
+
# django-cookie-consent cookie settings that store the configuration
|
|
14
|
+
NAME: str = "cookie_consent"
|
|
15
|
+
# TODO: rename to AGE for parity with django settings
|
|
16
|
+
MAX_AGE: int = 60 * 60 * 24 * 365 * 1 # 1 year,
|
|
17
|
+
DOMAIN: str | None = None
|
|
18
|
+
SECURE: bool = False
|
|
19
|
+
HTTPONLY: bool = True
|
|
20
|
+
SAMESITE: Literal["Strict", "Lax", "None", False] = "Lax"
|
|
21
|
+
|
|
22
|
+
DECLINE: str = "-1"
|
|
23
|
+
|
|
24
|
+
ENABLED: bool = True
|
|
25
|
+
|
|
26
|
+
OPT_OUT: bool = False
|
|
27
|
+
|
|
28
|
+
CACHE_BACKEND: str = "default"
|
|
29
|
+
|
|
30
|
+
LOG_ENABLED: bool = True
|
|
31
|
+
"""
|
|
32
|
+
DeprecationWarning: in future versions the default may switch to log disabled.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
SUCCESS_URL: str | Promise = reverse_lazy("cookie_consent_cookie_group_list")
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from collections.abc import Collection, Iterator
|
|
2
|
+
|
|
3
|
+
from django import forms
|
|
4
|
+
from django.utils.translation import gettext_lazy as _
|
|
5
|
+
|
|
6
|
+
from .cache import all_cookie_groups
|
|
7
|
+
from .models import CookieGroup
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def iter_cookie_group_choices() -> Iterator[tuple[str, str]]:
|
|
11
|
+
"""
|
|
12
|
+
Use the cached cookie group instances to get a list of choices.
|
|
13
|
+
"""
|
|
14
|
+
for varname, cookie_group in all_cookie_groups().items():
|
|
15
|
+
yield varname, cookie_group.name
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CookieGroupsChoiceField(forms.TypedMultipleChoiceField):
|
|
19
|
+
def __init__(self, **kwargs):
|
|
20
|
+
kwargs["coerce"] = self._coerce_choice
|
|
21
|
+
kwargs["choices"] = iter_cookie_group_choices
|
|
22
|
+
super().__init__(**kwargs)
|
|
23
|
+
|
|
24
|
+
def _coerce_choice(self, varname: str) -> CookieGroup:
|
|
25
|
+
all_groups = all_cookie_groups()
|
|
26
|
+
return all_groups[varname]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ProcessCookiesForm(forms.Form):
|
|
30
|
+
all_groups = forms.BooleanField(
|
|
31
|
+
label=_("Apply to all cookie groups"),
|
|
32
|
+
required=False,
|
|
33
|
+
)
|
|
34
|
+
cookie_groups = CookieGroupsChoiceField(
|
|
35
|
+
label=_("Cookie group varnames"),
|
|
36
|
+
choices=iter_cookie_group_choices,
|
|
37
|
+
required=False,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def get_cookie_groups(self) -> Collection[CookieGroup]:
|
|
41
|
+
"""
|
|
42
|
+
Build the collection of specified cookies.
|
|
43
|
+
"""
|
|
44
|
+
match self.cleaned_data:
|
|
45
|
+
case {"all_groups": True}:
|
|
46
|
+
return all_cookie_groups().values()
|
|
47
|
+
case {"cookie_groups": [*groups]}:
|
|
48
|
+
return groups
|
|
49
|
+
case _:
|
|
50
|
+
return []
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
|
|
3
|
+
from django.http import HttpRequest, HttpResponseBase
|
|
3
4
|
|
|
4
5
|
from .cache import all_cookie_groups
|
|
5
6
|
from .conf import settings
|
|
6
7
|
from .util import get_cookie_dict_from_request, is_cookie_consent_enabled
|
|
7
8
|
|
|
8
9
|
|
|
9
|
-
def _should_delete_cookie(group_version:
|
|
10
|
+
def _should_delete_cookie(group_version: str | None) -> bool:
|
|
10
11
|
# declined after it was accepted (and set) before
|
|
11
12
|
if group_version == settings.COOKIE_CONSENT_DECLINE:
|
|
12
13
|
return True
|
|
@@ -31,16 +32,16 @@ class CleanCookiesMiddleware:
|
|
|
31
32
|
Note that this only applies if COOKIE_CONSENT_OPT_OUT is not set.
|
|
32
33
|
"""
|
|
33
34
|
|
|
34
|
-
def __init__(self, get_response):
|
|
35
|
+
def __init__(self, get_response: Callable[[HttpRequest], HttpResponseBase]):
|
|
35
36
|
self.get_response = get_response
|
|
36
37
|
|
|
37
|
-
def __call__(self, request):
|
|
38
|
+
def __call__(self, request: HttpRequest):
|
|
38
39
|
response = self.get_response(request)
|
|
39
40
|
if is_cookie_consent_enabled(request):
|
|
40
41
|
self.process_response(request, response)
|
|
41
42
|
return response
|
|
42
43
|
|
|
43
|
-
def process_response(self, request, response):
|
|
44
|
+
def process_response(self, request: HttpRequest, response: HttpResponseBase):
|
|
44
45
|
cookie_dic = get_cookie_dict_from_request(request)
|
|
45
46
|
|
|
46
47
|
cookies_to_delete = []
|
|
@@ -68,7 +68,8 @@ class Migration(migrations.Migration):
|
|
|
68
68
|
validators=[
|
|
69
69
|
django.core.validators.RegexValidator(
|
|
70
70
|
re.compile("^[-_a-zA-Z0-9]+$"),
|
|
71
|
-
"Enter a valid 'varname' consisting of letters,
|
|
71
|
+
"Enter a valid 'varname' consisting of letters, "
|
|
72
|
+
"numbers, underscores or hyphens.",
|
|
72
73
|
"invalid",
|
|
73
74
|
)
|
|
74
75
|
],
|
|
@@ -7,7 +7,6 @@ from django.db import migrations, models
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class Migration(migrations.Migration):
|
|
10
|
-
|
|
11
10
|
dependencies = [
|
|
12
11
|
("cookie_consent", "0002_auto__add_logitem"),
|
|
13
12
|
]
|
|
@@ -22,7 +21,8 @@ class Migration(migrations.Migration):
|
|
|
22
21
|
validators=[
|
|
23
22
|
django.core.validators.RegexValidator(
|
|
24
23
|
re.compile("^[-_a-zA-Z0-9]+$"),
|
|
25
|
-
"Enter a valid 'varname' consisting of letters, numbers,
|
|
24
|
+
"Enter a valid 'varname' consisting of letters, numbers, "
|
|
25
|
+
"underscores or hyphens.",
|
|
26
26
|
"invalid",
|
|
27
27
|
)
|
|
28
28
|
],
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import re
|
|
3
|
-
from
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import ClassVar, ParamSpec, TypedDict, TypeVar
|
|
4
6
|
|
|
5
7
|
from django.core.validators import RegexValidator
|
|
6
8
|
from django.db import models
|
|
@@ -16,9 +18,12 @@ validate_cookie_name = RegexValidator(
|
|
|
16
18
|
"invalid",
|
|
17
19
|
)
|
|
18
20
|
|
|
21
|
+
P = ParamSpec("P")
|
|
22
|
+
T = TypeVar("T")
|
|
23
|
+
|
|
19
24
|
|
|
20
|
-
def clear_cache_after(func):
|
|
21
|
-
def wrapper(*args, **kwargs):
|
|
25
|
+
def clear_cache_after(func: Callable[P, T]) -> Callable[P, T]:
|
|
26
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs):
|
|
22
27
|
from .cache import delete_cache
|
|
23
28
|
|
|
24
29
|
return_value = func(*args, **kwargs)
|
|
@@ -33,9 +38,8 @@ class CookieGroupDict(TypedDict):
|
|
|
33
38
|
name: str
|
|
34
39
|
description: str
|
|
35
40
|
is_required: bool
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
# version: str
|
|
41
|
+
# The version is deliberately not included because it requires page/view cache
|
|
42
|
+
# busting if a new cookie gets added to the group, which we don't control.
|
|
39
43
|
|
|
40
44
|
|
|
41
45
|
class BaseQueryset(models.query.QuerySet):
|
|
@@ -49,7 +53,7 @@ class BaseQueryset(models.query.QuerySet):
|
|
|
49
53
|
|
|
50
54
|
|
|
51
55
|
class CookieGroupManager(models.Manager.from_queryset(BaseQueryset)):
|
|
52
|
-
def get_by_natural_key(self, varname):
|
|
56
|
+
def get_by_natural_key(self, varname: str) -> CookieGroup:
|
|
53
57
|
return self.get(varname=varname)
|
|
54
58
|
|
|
55
59
|
|
|
@@ -75,7 +79,8 @@ class CookieGroup(models.Model):
|
|
|
75
79
|
ordering = models.IntegerField(_("Ordering"), default=0)
|
|
76
80
|
created = models.DateTimeField(_("Created"), auto_now_add=True, blank=True)
|
|
77
81
|
|
|
78
|
-
objects = CookieGroupManager()
|
|
82
|
+
objects: ClassVar[CookieGroupManager] = CookieGroupManager() # pyright: ignore[reportIncompatibleVariableOverride]
|
|
83
|
+
cookie_set: ClassVar[CookieManager]
|
|
79
84
|
|
|
80
85
|
class Meta:
|
|
81
86
|
verbose_name = _("Cookie Group")
|
|
@@ -93,11 +98,15 @@ class CookieGroup(models.Model):
|
|
|
93
98
|
def delete(self, *args, **kwargs):
|
|
94
99
|
return super().delete(*args, **kwargs)
|
|
95
100
|
|
|
96
|
-
def natural_key(self):
|
|
101
|
+
def natural_key(self) -> tuple[str]:
|
|
97
102
|
return (self.varname,)
|
|
98
103
|
|
|
99
104
|
def get_version(self) -> str:
|
|
100
105
|
try:
|
|
106
|
+
# this relies on the cookie set being ordered by most-recently created
|
|
107
|
+
# first.
|
|
108
|
+
# Note that we don't use `.first()` as that's a new query and bypasses
|
|
109
|
+
# the cache.
|
|
101
110
|
return str(self.cookie_set.all()[0].get_version())
|
|
102
111
|
except IndexError:
|
|
103
112
|
return ""
|
|
@@ -113,7 +122,7 @@ class CookieGroup(models.Model):
|
|
|
113
122
|
|
|
114
123
|
|
|
115
124
|
class CookieManager(models.Manager.from_queryset(BaseQueryset)):
|
|
116
|
-
def get_by_natural_key(self, name, domain, cookiegroup):
|
|
125
|
+
def get_by_natural_key(self, name: str, domain: str, cookiegroup: str) -> Cookie:
|
|
117
126
|
group = CookieGroup.objects.get_by_natural_key(cookiegroup)
|
|
118
127
|
return self.get(cookiegroup=group, name=name, domain=domain)
|
|
119
128
|
|
|
@@ -144,7 +153,7 @@ class Cookie(models.Model):
|
|
|
144
153
|
ordering = ["-created"]
|
|
145
154
|
|
|
146
155
|
def __str__(self):
|
|
147
|
-
return "
|
|
156
|
+
return f"{self.name} {self.domain}{self.path}"
|
|
148
157
|
|
|
149
158
|
@clear_cache_after
|
|
150
159
|
def save(self, *args, **kwargs):
|
|
@@ -154,16 +163,17 @@ class Cookie(models.Model):
|
|
|
154
163
|
def delete(self, *args, **kwargs):
|
|
155
164
|
return super().delete(*args, **kwargs)
|
|
156
165
|
|
|
157
|
-
def natural_key(self):
|
|
166
|
+
def natural_key(self) -> tuple[str, str, str]:
|
|
158
167
|
return (self.name, self.domain) + self.cookiegroup.natural_key()
|
|
159
168
|
|
|
160
|
-
natural_key.dependencies = ["cookie_consent.cookiegroup"]
|
|
169
|
+
natural_key.dependencies = ["cookie_consent.cookiegroup"] # pyright: ignore[reportFunctionMemberAccess]
|
|
161
170
|
|
|
162
171
|
@property
|
|
163
|
-
def varname(self):
|
|
164
|
-
|
|
172
|
+
def varname(self) -> str:
|
|
173
|
+
group_varname = self.cookiegroup.varname
|
|
174
|
+
return f"{group_varname}={self.name}:{self.domain}"
|
|
165
175
|
|
|
166
|
-
def get_version(self):
|
|
176
|
+
def get_version(self) -> str:
|
|
167
177
|
return self.created.isoformat()
|
|
168
178
|
|
|
169
179
|
|
|
@@ -185,10 +195,10 @@ class LogItem(models.Model):
|
|
|
185
195
|
version = models.CharField(_("Version"), max_length=32)
|
|
186
196
|
created = models.DateTimeField(_("Created"), auto_now_add=True, blank=True)
|
|
187
197
|
|
|
188
|
-
def __str__(self):
|
|
189
|
-
return "%s %s" % (self.cookiegroup.name, self.version)
|
|
190
|
-
|
|
191
198
|
class Meta:
|
|
192
199
|
verbose_name = _("Log item")
|
|
193
200
|
verbose_name_plural = _("Log items")
|
|
194
201
|
ordering = ["-created"]
|
|
202
|
+
|
|
203
|
+
def __str__(self):
|
|
204
|
+
return f"{self.cookiegroup.name} {self.version}"
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from collections.abc import Collection
|
|
2
|
+
from typing import Literal
|
|
3
|
+
|
|
4
|
+
from django.http import HttpRequest, HttpResponseBase
|
|
5
|
+
|
|
6
|
+
from .conf import settings
|
|
7
|
+
from .models import ACTION_ACCEPTED, ACTION_DECLINED, CookieGroup, LogItem
|
|
8
|
+
from .util import get_cookie_dict_from_request, set_cookie_dict_to_response
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CookiesProcessor:
|
|
12
|
+
"""
|
|
13
|
+
Process the accept/decline logic for cookie groups.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, request: HttpRequest, response: HttpResponseBase):
|
|
17
|
+
self.request = request
|
|
18
|
+
self.response = response
|
|
19
|
+
|
|
20
|
+
def process(
|
|
21
|
+
self,
|
|
22
|
+
cookie_groups: Collection[CookieGroup],
|
|
23
|
+
action: Literal["accept", "decline"],
|
|
24
|
+
) -> None:
|
|
25
|
+
"""
|
|
26
|
+
Apply ``action`` to the specified ``cookie_groups``.
|
|
27
|
+
|
|
28
|
+
Mutates the response by updating the cookie tracking the cookie group status. If
|
|
29
|
+
there are no cookie groups provided, nothing happens.
|
|
30
|
+
"""
|
|
31
|
+
if not cookie_groups:
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
cookie_dic = get_cookie_dict_from_request(self.request)
|
|
35
|
+
|
|
36
|
+
match action:
|
|
37
|
+
case "accept":
|
|
38
|
+
for cookie_group in cookie_groups:
|
|
39
|
+
cookie_dic[cookie_group.varname] = cookie_group.get_version()
|
|
40
|
+
case "decline":
|
|
41
|
+
self._delete_cookies(cookie_groups)
|
|
42
|
+
for cookie_group in cookie_groups:
|
|
43
|
+
cookie_dic[cookie_group.varname] = settings.COOKIE_CONSENT_DECLINE
|
|
44
|
+
|
|
45
|
+
self._log_action(cookie_groups, action)
|
|
46
|
+
set_cookie_dict_to_response(self.response, cookie_dic)
|
|
47
|
+
|
|
48
|
+
def _log_action(
|
|
49
|
+
self,
|
|
50
|
+
cookie_groups: Collection[CookieGroup],
|
|
51
|
+
action: Literal["accept", "decline"],
|
|
52
|
+
) -> None:
|
|
53
|
+
if not settings.COOKIE_CONSENT_LOG_ENABLED:
|
|
54
|
+
return
|
|
55
|
+
# TODO: replace with stdlib logging call/helper instead of creating DB records
|
|
56
|
+
# directly.
|
|
57
|
+
|
|
58
|
+
action_map: dict[Literal["accept", "decline"], int] = {
|
|
59
|
+
"accept": ACTION_ACCEPTED,
|
|
60
|
+
"decline": ACTION_DECLINED,
|
|
61
|
+
}
|
|
62
|
+
log_items: list[LogItem] = [
|
|
63
|
+
LogItem(
|
|
64
|
+
action=action_map[action],
|
|
65
|
+
cookiegroup=cookie_group,
|
|
66
|
+
version=cookie_group.get_version(),
|
|
67
|
+
)
|
|
68
|
+
for cookie_group in cookie_groups
|
|
69
|
+
]
|
|
70
|
+
LogItem.objects.bulk_create(log_items)
|
|
71
|
+
|
|
72
|
+
def _delete_cookies(self, cookie_groups: Collection[CookieGroup]) -> None:
|
|
73
|
+
for cookie_group in cookie_groups:
|
|
74
|
+
if not cookie_group.is_deletable:
|
|
75
|
+
continue
|
|
76
|
+
for cookie in cookie_group.cookie_set.all():
|
|
77
|
+
self.response.delete_cookie(cookie.name, cookie.path, cookie.domain)
|
|
File without changes
|