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.
Files changed (64) hide show
  1. {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/MANIFEST.in +1 -0
  2. django_cookie_consent-1.0.0/PKG-INFO +96 -0
  3. {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/README.md +12 -13
  4. django_cookie_consent-1.0.0/cookie_consent/__init__.py +1 -0
  5. django_cookie_consent-1.0.0/cookie_consent/admin.py +65 -0
  6. {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/cookie_consent/cache.py +13 -8
  7. django_cookie_consent-1.0.0/cookie_consent/conf.py +35 -0
  8. django_cookie_consent-1.0.0/cookie_consent/forms.py +50 -0
  9. {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/cookie_consent/middleware.py +7 -6
  10. {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/cookie_consent/migrations/0001_initial.py +2 -1
  11. {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/cookie_consent/migrations/0003_alter_cookiegroup_varname.py +2 -2
  12. {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/cookie_consent/migrations/0004_cookie_natural_key.py +0 -1
  13. {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/cookie_consent/models.py +30 -20
  14. django_cookie_consent-1.0.0/cookie_consent/processor.py +77 -0
  15. django_cookie_consent-1.0.0/cookie_consent/py.typed +0 -0
  16. {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/cookie_consent/static/cookie_consent/cookiebar.module.js +8 -3
  17. django_cookie_consent-1.0.0/cookie_consent/static/cookie_consent/cookiebar.module.js.map +7 -0
  18. {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/cookie_consent/templates/cookie_consent/_cookie_group.html +4 -2
  19. django_cookie_consent-1.0.0/cookie_consent/templatetags/__init__.py +1 -0
  20. django_cookie_consent-1.0.0/cookie_consent/templatetags/cookie_consent_tags.py +88 -0
  21. django_cookie_consent-1.0.0/cookie_consent/urls.py +15 -0
  22. django_cookie_consent-1.0.0/cookie_consent/util.py +170 -0
  23. {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/cookie_consent/views.py +46 -40
  24. django_cookie_consent-1.0.0/django_cookie_consent.egg-info/PKG-INFO +96 -0
  25. {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/django_cookie_consent.egg-info/SOURCES.txt +4 -3
  26. {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/django_cookie_consent.egg-info/requires.txt +4 -7
  27. django_cookie_consent-1.0.0/pyproject.toml +170 -0
  28. django_cookie_consent-1.0.0/setup.cfg +4 -0
  29. django_cookie_consent-1.0.0/tests/test_admin.py +24 -0
  30. {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/tests/test_cache.py +0 -1
  31. {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/tests/test_middleware.py +7 -4
  32. {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/tests/test_models.py +30 -15
  33. {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/tests/test_settings.py +6 -3
  34. {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/tests/test_templatetags.py +2 -2
  35. {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/tests/test_util.py +23 -7
  36. django_cookie_consent-1.0.0/tests/test_views.py +283 -0
  37. django_cookie_consent-0.8.0/PKG-INFO +0 -125
  38. django_cookie_consent-0.8.0/cookie_consent/__init__.py +0 -1
  39. django_cookie_consent-0.8.0/cookie_consent/admin.py +0 -37
  40. django_cookie_consent-0.8.0/cookie_consent/conf.py +0 -27
  41. django_cookie_consent-0.8.0/cookie_consent/static/cookie_consent/cookiebar.js +0 -67
  42. django_cookie_consent-0.8.0/cookie_consent/static/cookie_consent/cookiebar.module.js.map +0 -7
  43. django_cookie_consent-0.8.0/cookie_consent/templatetags/__init__.py +0 -2
  44. django_cookie_consent-0.8.0/cookie_consent/templatetags/cookie_consent_tags.py +0 -179
  45. django_cookie_consent-0.8.0/cookie_consent/urls.py +0 -37
  46. django_cookie_consent-0.8.0/cookie_consent/util.py +0 -202
  47. django_cookie_consent-0.8.0/django_cookie_consent.egg-info/PKG-INFO +0 -125
  48. django_cookie_consent-0.8.0/pyproject.toml +0 -108
  49. django_cookie_consent-0.8.0/setup.cfg +0 -10
  50. django_cookie_consent-0.8.0/tests/test_legacy_javascript_cookiebar.py +0 -78
  51. django_cookie_consent-0.8.0/tests/test_views.py +0 -161
  52. {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/AUTHORS +0 -0
  53. {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/LICENSE +0 -0
  54. {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/cookie_consent/apps.py +0 -0
  55. {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/cookie_consent/fixtures/common_cookies.json +0 -0
  56. {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/cookie_consent/migrations/0002_auto__add_logitem.py +0 -0
  57. {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/cookie_consent/migrations/__init__.py +0 -0
  58. {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/cookie_consent/templates/cookie_consent/base.html +0 -0
  59. {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/cookie_consent/templates/cookie_consent/cookiegroup_list.html +0 -0
  60. {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/django_cookie_consent.egg-info/dependency_links.txt +0 -0
  61. {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/django_cookie_consent.egg-info/top_level.txt +0 -0
  62. {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/tests/test_cookie_group_model.py +0 -0
  63. {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/tests/test_cookie_model.py +0 -0
  64. {django_cookie_consent-0.8.0 → django_cookie_consent-1.0.0}/tests/test_javascript_cookiebar.py +0 -0
@@ -1,6 +1,7 @@
1
1
  include LICENSE
2
2
  include AUTHORS
3
3
  include README.md
4
+ include cookie_consent/py.typed
4
5
  recursive-include cookie_consent/templates *
5
6
  recursive-include cookie_consent/static *
6
7
  recursive-include cookie_consent/fixtures *
@@ -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
+ ![License](https://img.shields.io/pypi/l/django-cookie-consent)
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
+ ![Supported python versions](https://img.shields.io/pypi/pyversions/django-cookie-consent)
61
+ ![Supported Django versions](https://img.shields.io/pypi/djversions/django-cookie-consent)
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
  ![License](https://img.shields.io/pypi/l/django-cookie-consent)
9
7
  [![Build status][badge:GithubActions:CI]][GithubActions:CI]
10
8
  [![Code Quality][badge:GithubActions:CQ]][GithubActions:CQ]
11
- [![Code style: black][badge:black]][black]
9
+ [![Code style: ruff][badge:ruff]][ruff]
12
10
  [![Test coverage][badge:codecov]][codecov]
13
11
  [![Documentation][badge:docs]][docs]
14
12
 
15
13
  ![Supported python versions](https://img.shields.io/pypi/pyversions/django-cookie-consent)
16
14
  ![Supported Django versions](https://img.shields.io/pypi/djversions/django-cookie-consent)
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
- [jazzband]: https://jazzband.co/
38
- [badge:jazzband]: https://jazzband.co/static/img/badge.svg
39
- [GithubActions:CI]: https://github.com/jazzband/django-cookie-consent/actions?query=workflow%3A%22Run+CI%22
40
- [badge:GithubActions:CI]: https://github.com/jazzband/django-cookie-consent/workflows/Run%20CI/badge.svg
41
- [GithubActions:CQ]: https://github.com/jazzband/django-cookie-consent/actions?query=workflow%3A%22Code+quality+checks%22
42
- [badge:GithubActions:CQ]: https://github.com/jazzband/django-cookie-consent/workflows/Code%20quality%20checks/badge.svg
43
- [black]: https://github.com/psf/black
44
- [badge:black]: https://img.shields.io/badge/code%20style-black-000000.svg
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
- # -*- coding: utf-8 -*-
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
- return cache.get_or_set(
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
- # -*- coding: utf-8 -*-
2
- from typing import Optional
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: Optional[str]) -> bool:
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, numbers, underscores or hyphens.",
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, underscores or hyphens.",
24
+ "Enter a valid 'varname' consisting of letters, numbers, "
25
+ "underscores or hyphens.",
26
26
  "invalid",
27
27
  )
28
28
  ],
@@ -4,7 +4,6 @@ from django.db import migrations, models
4
4
 
5
5
 
6
6
  class Migration(migrations.Migration):
7
-
8
7
  dependencies = [
9
8
  ("cookie_consent", "0003_alter_cookiegroup_varname"),
10
9
  ]
@@ -1,6 +1,8 @@
1
- # -*- coding: utf-8 -*-
1
+ from __future__ import annotations
2
+
2
3
  import re
3
- from typing import TypedDict
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
- # TODO: should we output this? page cache busting would be
37
- # required if we do this. Alternatively, set up a JSONView to output these?
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 "%s %s%s" % (self.name, self.domain, self.path)
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
- return "%s=%s:%s" % (self.cookiegroup.varname, self.name, self.domain)
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