Repository: jazzband/django-cookie-consent Branch: main Commit: e5f357434899 Files: 103 Total size: 193.5 KB Directory structure: gitextract_18qginr2/ ├── .editorconfig ├── .git-blame-ignore-revs ├── .github/ │ ├── actions/ │ │ └── build-js/ │ │ └── action.yml │ └── workflows/ │ ├── ci.yml │ └── code_quality.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── AUTHORS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.rst ├── LICENSE ├── MANIFEST.in ├── README.md ├── __init__.py ├── cookie_consent/ │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── cache.py │ ├── conf.py │ ├── fixtures/ │ │ └── common_cookies.json │ ├── forms.py │ ├── locale/ │ │ ├── en/ │ │ │ └── LC_MESSAGES/ │ │ │ ├── django.mo │ │ │ └── django.po │ │ ├── nl/ │ │ │ └── LC_MESSAGES/ │ │ │ ├── django.mo │ │ │ └── django.po │ │ ├── oc/ │ │ │ └── LC_MESSAGES/ │ │ │ ├── django.mo │ │ │ └── django.po │ │ ├── pt_BR/ │ │ │ └── LC_MESSAGES/ │ │ │ ├── django.mo │ │ │ └── django.po │ │ └── sl/ │ │ └── LC_MESSAGES/ │ │ ├── django.mo │ │ └── django.po │ ├── management/ │ │ ├── __init__.py │ │ └── commands/ │ │ ├── __init__.py │ │ └── prune_cookie_consent_logs.py │ ├── middleware.py │ ├── migrations/ │ │ ├── 0001_initial.py │ │ ├── 0002_auto__add_logitem.py │ │ ├── 0003_alter_cookiegroup_varname.py │ │ ├── 0004_cookie_natural_key.py │ │ └── __init__.py │ ├── models.py │ ├── processor.py │ ├── py.typed │ ├── templates/ │ │ └── cookie_consent/ │ │ ├── _cookie_group.html │ │ ├── base.html │ │ └── cookiegroup_list.html │ ├── templatetags/ │ │ ├── __init__.py │ │ └── cookie_consent_tags.py │ ├── urls.py │ ├── util.py │ └── views.py ├── docs/ │ ├── Makefile │ ├── changelog.rst │ ├── check_sphinx.py │ ├── concept.rst │ ├── conf.py │ ├── contributing.rst │ ├── example_app.rst │ ├── index.rst │ ├── javascript.rst │ ├── make.bat │ ├── migrating-1.0.rst │ ├── quickstart.rst │ ├── reference/ │ │ ├── api_middleware.rst │ │ ├── api_models.rst │ │ ├── api_templatetags.rst │ │ ├── api_util.rst │ │ ├── api_views.rst │ │ ├── index.rst │ │ └── management_commands.rst │ ├── settings.rst │ └── usage.rst ├── js/ │ ├── .nvmrc │ ├── README.md │ ├── package.json │ ├── src/ │ │ ├── cookiebar.ts │ │ └── index.ts │ └── tsconfig.json ├── pyproject.toml ├── testapp/ │ ├── __init__.py │ ├── fixture.json │ ├── settings.py │ ├── static/ │ │ └── styles.css │ ├── templates/ │ │ ├── show-cookie-bar-script.html │ │ └── test_page.html │ ├── urls.py │ └── views.py ├── tests/ │ ├── __init__.py │ ├── conftest.py │ ├── test_admin.py │ ├── test_cache.py │ ├── test_cookie_group_model.py │ ├── test_cookie_model.py │ ├── test_javascript_cookiebar.py │ ├── test_middleware.py │ ├── test_models.py │ ├── test_prune_cookie_consent_logs.py │ ├── test_settings.py │ ├── test_templatetags.py │ ├── test_util.py │ └── test_views.py └── tox.ini ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] indent_style = space indent_size = 4 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true [*.{scss,sass}] indent_size = 2 [*.{yml,yaml}] indent_size = 2 [*.{js,ts}] indent_size = 2 [*.json] indent_size = 2 [Makefile] indent_style = tab ================================================ FILE: .git-blame-ignore-revs ================================================ # black and isort on the codebase 938a25cb45b672b65673053b3521d04f737e121a ================================================ FILE: .github/actions/build-js/action.yml ================================================ --- name: 'Build JS' description: 'Compile the TS source code' inputs: npm-package: description: Build NPM package required: false default: 'false' django-staticfiles: description: Bundle Django staticfiles required: false default: 'false' runs: using: 'composite' steps: - uses: actions/setup-node@v4 with: node-version-file: 'js/.nvmrc' cache: npm cache-dependency-path: js/package-lock.json - name: Install dependencies run: npm ci shell: bash working-directory: js - name: Build NPM package if: ${{ inputs.npm-package == 'true' }} run: npm run build shell: bash working-directory: js - name: Build Django assets package if: ${{ inputs.django-staticfiles == 'true' }} run: npm run build:django-static shell: bash working-directory: js ================================================ FILE: .github/workflows/ci.yml ================================================ name: Run CI # Run this workflow every time a new commit pushed to your repository on: push: branches: - main tags: - '*' pull_request: workflow_dispatch: jobs: build-js: runs-on: ubuntu-latest name: Compile the frontend code steps: - uses: actions/checkout@v4 - uses: ./.github/actions/build-js with: npm-package: 'true' django-staticfiles: 'false' tests: runs-on: ubuntu-latest strategy: matrix: python: ['3.10', '3.11', '3.12', '3.13', '3.14'] django: ['4.2', '5.2', '6.0'] exclude: - python: '3.13' django: '4.2' - python: '3.14' django: '4.2' name: Run the test suite (Python ${{ matrix.python }}, Django ${{ matrix.django }}) steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} - name: Install dependencies run: pip install tox tox-gh-actions - name: Run tests run: tox env: DJANGO: ${{ matrix.django }} - name: Publish coverage report uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} directory: reports/ e2e_tests: runs-on: ubuntu-latest name: Run the end-to-end tests steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.12' - uses: ./.github/actions/build-js with: npm-package: 'false' django-staticfiles: 'true' - name: Install dependencies run: | pip install tox tox-gh-actions pytest-playwright playwright install --with-deps chromium - name: Run tests run: tox -e e2e - name: Publish coverage report uses: codecov/codecov-action@v3 with: directory: reports/ publish: name: Publish packages to PyPI and NPM runs-on: ubuntu-latest needs: - tests - e2e_tests if: github.repository == 'django-commons/django-cookie-consent' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags') environment: name: release url: https://pypi.org/project/django-cookie-consent/ permissions: id-token: write contents: read steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.12' - uses: actions/setup-node@v4 with: node-version-file: 'js/.nvmrc' cache: npm cache-dependency-path: js/package-lock.json registry-url: 'https://registry.npmjs.org' # Required to ensure the build artifacts are in the packages. - uses: ./.github/actions/build-js with: npm-package: 'true' django-staticfiles: 'true' - name: Build sdist and wheel run: | pip install build --upgrade python -m build # TODO: enable verified publishing! - name: Publish a Python distribution to PyPI uses: pypa/gh-action-pypi-publish@release/v1 - name: Publish NPM package run: | version=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') npm publish --new-version="$version" working-directory: js env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} ================================================ FILE: .github/workflows/code_quality.yml ================================================ name: Code quality checks # Run this workflow every time a new commit pushed to your repository on: push: branches: - main tags: - '*' pull_request: workflow_dispatch: jobs: linting: name: Code-quality checks runs-on: ubuntu-latest strategy: matrix: toxenv: - ruff - docs steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.12' - name: Install dependencies run: pip install tox - run: tox env: TOXENV: ${{ matrix.toxenv }} FORCE_COLOR: '1' ================================================ FILE: .gitignore ================================================ *.log *.pot *.pyc local_settings.py env/ # build artifacts docs/_build build/ dist/ *.egg-info/ # editors .vscode .history .sonarlint # tests and coverage .tox/ .pytest_cache .coverage htmlcov/ reports/ testapp/*.db .hypothesis # frontend tooling / builds js/node_modules/ js/lib/ cookie_consent/static/cookie_consent/cookiebar.module.* ================================================ FILE: .pre-commit-config.yaml ================================================ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: - id: check-merge-conflict - repo: https://github.com/adamchainz/django-upgrade rev: 1.29.1 hooks: - id: django-upgrade ================================================ FILE: .readthedocs.yaml ================================================ # .readthedocs.yaml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details version: 2 sphinx: configuration: docs/conf.py build: os: 'ubuntu-22.04' tools: python: '3.10' python: install: - method: pip path: . extra_requirements: - docs ================================================ FILE: AUTHORS ================================================ The django-cookie-consent was created by Bojan Mihelac (bmihelac). The following is a list of much appreciated contributors: * Jonathan L Herr (JonHerr) * Jasper Koops (Jasper-Koops) * Fernando Cordeiro (MrCordeiro) * Mejans * Sergei Maertens (sergei-maertens) * Abdullah Alahdal (alahdal) * some1ataplace * binoyudayan * adilhussain540 * Johanan Oppong Amoateng (JohananOppongAmoateng) ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Code of Conduct The django-cookie-consent project utilizes the [Django Commons Code of Conduct](https://github.com/django-commons/membership/blob/main/CODE_OF_CONDUCT.md). ================================================ FILE: CONTRIBUTING.rst ================================================ By contributing you agree to abide by the `Contributor Code of Conduct `_. How can you contribute? ======================= Contributions in all forms are welcome, not only code patches. You can contribute by: * reporting bugs/issues * triaging reported issues * improving the documentation * suggesting enhancements Of course, if you report a bug or have a feature request, a pull request implementing the fix or feature is much appreciated. Contributing code ----------------- If you decide to submit a patch or implement a feature, please adhere to the quality checks: * Pull requests must be accompanied by tests. We use ``pytest`` and prefer using this testing style over Django's ``django.test.TestCase``. * Ideally, documentation updates are included in a pull request. * Code formatting and linting is done with ``ruff``. There are tox environments and CI checks in place to check/enforce this. * Follow Django's code style where possible. * Keep commits atomic - one commit should only concern one topic. Bugfixes typically have one commit for the regression test and one commit with the fix. Setting up the project for local development -------------------------------------------- After checking out the project (through ``git clone``), it's advised to set up a virtualenv with the lowest supported python and Django version. You can then install the project with all the dev-tools: .. code-block:: bash pip install -e .[tests,docs,release] Some frontend tooling is needed too: * NodeJS (for the version, see ``.nvmrc``, you can use ``nvm``) .. code-block:: bash cd js nvm use npm install npm run build:django-static npm run build # optional, but a nice check **Running the testapp as dev environment** In Django project's, you are typically expecting a ``manage.py`` file. This is less common in libraries, but it's fairly straightforward to emulate this: .. code-block:: bash export DJANGO_SETTINGS_MODULE=testapp.settings PYTHONPATH=. django-admin migrate django-admin createsuperuser django-admin runserver You can now start working on your contribution! **Running the tests** Run the tests locally using ``tox`` to verify everything is okay: .. code-block:: bash tox Cheat sheet ----------- This cheat sheet provides some quick tooling/commands lookup while working on the project. **Running tests** In your current environment .. code-block:: bash pytest or to build the full test matrix .. code-block:: bash tox **Formatting the code for check-in** .. code-block:: bash ruff format . ruff check --fix . Should be sufficient. Consider using a pre-commit hook to automate this. **Building the docs** .. code-block:: bash cd docs make html You can now open the file ``_build/html/index.html`` in your browser. **Generating message catalogs** .. code-block:: bash export DJANGO_SETTINGS_MODULE=testapp.settings cd cookie_consent django-admin makemessages --all cd .. After translating the message, you need to compile the message catalogs: .. code-block:: bash django-admin compilemessages **Bumping the version/releasing** After updating changelogs etc. .. code-block:: bash bump-my-version bump major|minor|patch bump-my-version bump pre_l git commit -am ":bookmark: Bump to version " git tag -s X.Y.Z git push origin main --tags ================================================ FILE: LICENSE ================================================ Copyright (c) Bojan Mihelac and individual contributors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: MANIFEST.in ================================================ include LICENSE include AUTHORS include README.md include cookie_consent/py.typed recursive-include cookie_consent/templates * recursive-include cookie_consent/static * recursive-include cookie_consent/fixtures * ================================================ FILE: README.md ================================================ Django cookie consent ===================== Manage cookie information and let visitors give or reject consent for them. ![License](https://img.shields.io/pypi/l/django-cookie-consent) [![Build status][badge:GithubActions:CI]][GithubActions:CI] [![Code Quality][badge:GithubActions:CQ]][GithubActions:CQ] [![Code style: ruff][badge:ruff]][ruff] [![Test coverage][badge:codecov]][codecov] [![Documentation][badge:docs]][docs] ![Supported python versions](https://img.shields.io/pypi/pyversions/django-cookie-consent) ![Supported Django versions](https://img.shields.io/pypi/djversions/django-cookie-consent) [![PyPI version][badge:pypi]][pypi] [![NPM version][badge:npm]][npm] **Features** * cookies and cookie groups are stored in models for easy management through Django admin interface * support for both opt-in and opt-out cookie consent schemes * removing declined cookies (or non accepted when opt-in scheme is used) * logging user actions when they accept and decline various cookies * easy adding new cookies and seamlessly re-asking for consent for new cookies Documentation ------------- The documentation is hosted on [readthedocs][docs] and contains all instructions to get started. Alternatively, if the documentation is not available, you can consult or build the docs from the `docs` directory in this repository. [GithubActions:CI]: https://github.com/django-commons/django-cookie-consent/actions?query=workflow%3A%22Run+CI%22 [badge:GithubActions:CI]: https://github.com/django-commons/django-cookie-consent/workflows/Run%20CI/badge.svg [GithubActions:CQ]: https://github.com/django-commons/django-cookie-consent/actions?query=workflow%3A%22Code+quality+checks%22 [badge:GithubActions:CQ]: https://github.com/django-commons/django-cookie-consent/workflows/Code%20quality%20checks/badge.svg [ruff]: https://github.com/astral-sh/ruff [badge:ruff]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json [codecov]: https://codecov.io/gh/django-commons/django-cookie-consent [badge:codecov]: https://codecov.io/gh/django-commons/django-cookie-consent/branch/main/graph/badge.svg [docs]: https://django-cookie-consent.readthedocs.io/en/latest/?badge=latest [badge:docs]: https://readthedocs.org/projects/django-cookie-consent/badge/?version=latest [pypi]: https://pypi.org/project/django-cookie-consent/ [badge:pypi]: https://img.shields.io/pypi/v/django-cookie-consent.svg [npm]: https://www.npmjs.com/package/django-cookie-consent [badge:npm]: https://img.shields.io/npm/v/django-cookie-consent ================================================ FILE: __init__.py ================================================ ================================================ FILE: cookie_consent/__init__.py ================================================ __version__ = "1.0.0" ================================================ FILE: cookie_consent/admin.py ================================================ from django.contrib import admin from django.db.models import Count from django.http.request import HttpRequest from django.templatetags.l10n import localize from django.templatetags.static import static from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ from .conf import settings from .models import Cookie, CookieGroup, LogItem @admin.register(Cookie) class CookieAdmin(admin.ModelAdmin): list_display = ("varname", "name", "cookiegroup", "path", "domain", "get_version") search_fields = ("name", "domain", "cookiegroup__varname", "cookiegroup__name") readonly_fields = ("varname",) list_filter = ("cookiegroup",) @admin.register(CookieGroup) class CookieGroupAdmin(admin.ModelAdmin): list_display = ( "varname", "name", "is_required", "is_deletable", "num_cookies", "get_version", ) search_fields = ( "varname", "name", ) list_filter = ( "is_required", "is_deletable", ) def get_queryset(self, request: HttpRequest): qs = super().get_queryset(request) return qs.annotate(num_cookies=Count("cookie")) @admin.display(ordering="num_cookies", description=_("# cookies")) def num_cookies(self, obj: CookieGroup): if (count := obj.num_cookies) > 0: return localize(count) return format_html( '{count} {alt}', count=localize(count), src=static("admin/img/icon-alert.svg"), alt=_("Warning icon for missing cookies in cookie group."), ) class LogItemAdmin(admin.ModelAdmin): list_display = ("action", "cookiegroup", "version", "created") list_filter = ("action", "cookiegroup") readonly_fields = ("action", "cookiegroup", "version", "created") date_hierarchy = "created" if settings.COOKIE_CONSENT_LOG_ENABLED: admin.site.register(LogItem, LogItemAdmin) ================================================ FILE: cookie_consent/apps.py ================================================ from django.apps import AppConfig from django.utils.translation import gettext_lazy as _ class CookieConsentConf(AppConfig): name = "cookie_consent" verbose_name = _("cookie consent") default_auto_field = "django.db.models.AutoField" ================================================ FILE: cookie_consent/cache.py ================================================ from collections.abc import Mapping from django.core.cache import caches from .conf import settings from .models import Cookie, CookieGroup CACHE_KEY = "cookie_consent_cache" CACHE_TIMEOUT = 60 * 60 # 60 minutes def _get_cache(): """ Lazily wrap around django.core.cache. This prevents the cache object to be resolved at import-time, which breaks the `django.test.override_settings` functionality for projects adding tests for this package, see https://github.com/bmihelac/django-cookie-consent/issues/41. """ return caches[settings.COOKIE_CONSENT_CACHE_BACKEND] def delete_cache() -> None: cache = _get_cache() cache.delete(CACHE_KEY) def _get_cookie_groups_from_db() -> Mapping[str, CookieGroup]: qs = CookieGroup.objects.filter(is_required=False).prefetch_related("cookie_set") return qs.in_bulk(field_name="varname") def all_cookie_groups() -> Mapping[str, CookieGroup]: """ Get all cookie groups that are optional. Reads from the cache where possible, sets the value in the cache if there's a cache miss. """ cache = _get_cache() result = cache.get_or_set( CACHE_KEY, _get_cookie_groups_from_db, timeout=CACHE_TIMEOUT ) assert result is not None return result def get_cookie_group(varname: str) -> CookieGroup | None: return all_cookie_groups().get(varname) def get_cookie(cookie_group: CookieGroup, name: str, domain: str) -> Cookie | None: # loop over cookie set relation instead of doing a lookup query, as this should # come from the cache and avoid hitting the database for cookie in cookie_group.cookie_set.all(): if cookie.name == name and cookie.domain == domain: return cookie return None ================================================ FILE: cookie_consent/conf.py ================================================ from typing import Literal from django.conf import settings from django.urls import reverse_lazy from django.utils.functional import Promise from appconf import AppConf __all__ = ["settings"] class CookieConsentConf(AppConf): # django-cookie-consent cookie settings that store the configuration NAME: str = "cookie_consent" # TODO: rename to AGE for parity with django settings MAX_AGE: int = 60 * 60 * 24 * 365 * 1 # 1 year, DOMAIN: str | None = None SECURE: bool = False HTTPONLY: bool = True SAMESITE: Literal["Strict", "Lax", "None", False] = "Lax" DECLINE: str = "-1" ENABLED: bool = True OPT_OUT: bool = False CACHE_BACKEND: str = "default" LOG_ENABLED: bool = True """ DeprecationWarning: in future versions the default may switch to log disabled. """ SUCCESS_URL: str | Promise = reverse_lazy("cookie_consent_cookie_group_list") ================================================ FILE: cookie_consent/fixtures/common_cookies.json ================================================ [{"pk": 1, "model": "cookie_consent.cookiegroup", "fields": {"is_deletable": false, "name": "Required cookies", "created": "2013-06-03T12:54:15.809", "ordering": 0, "varname": "required", "is_required": true, "description": "This cookies are required for website."}}, {"pk": 2, "model": "cookie_consent.cookiegroup", "fields": {"is_deletable": true, "name": "Google Analytics", "created": "2013-06-03T12:58:11.616", "ordering": 0, "varname": "analytics", "is_required": false, "description": "These cookies are used to collect information about how visitors use our site. We use the information to compile reports and to help us improve the site. The cookies collect information in an anonymous form, including the number of visitors to the site, where vistors have come to the site from and the pages they visited."}}, {"pk": 3, "model": "cookie_consent.cookiegroup", "fields": {"is_deletable": true, "name": "Social media", "created": "2013-06-04T03:16:45.089", "ordering": 0, "varname": "social", "is_required": false, "description": "Facebook, Twitter and other social websites need to know who you are to work properly."}}, {"pk": 1, "model": "cookie_consent.cookie", "fields": {"domain": "", "name": "_utma", "created": "2013-06-03T12:59:03.467", "cookiegroup": 2, "path": "/", "description": null}}, {"pk": 2, "model": "cookie_consent.cookie", "fields": {"domain": "", "name": "_utmb", "created": "2013-06-03T12:59:36.710", "cookiegroup": 2, "path": "/", "description": null}}, {"pk": 3, "model": "cookie_consent.cookie", "fields": {"domain": "", "name": "_utmc", "created": "2013-06-03T12:59:45.074", "cookiegroup": 2, "path": "/", "description": null}}, {"pk": 4, "model": "cookie_consent.cookie", "fields": {"domain": "", "name": "_utmz", "created": "2013-06-03T12:59:55.465", "cookiegroup": 2, "path": "/", "description": null}}, {"pk": 5, "model": "cookie_consent.cookie", "fields": {"domain": "", "name": "sessionid", "created": "2013-06-03T13:00:15.592", "cookiegroup": 1, "path": "/", "description": null}}, {"pk": 6, "model": "cookie_consent.cookie", "fields": {"domain": "", "name": "csrftoken", "created": "2013-06-03T13:00:49.878", "cookiegroup": 1, "path": "/", "description": null}}, {"pk": 7, "model": "cookie_consent.cookie", "fields": {"domain": ".facebook.com", "name": "*", "created": "2013-06-04T03:17:01.421", "cookiegroup": 3, "path": "/", "description": "This cookies are used by Facebook to allow sharing content. "}}] ================================================ FILE: cookie_consent/forms.py ================================================ from collections.abc import Collection, Iterator from django import forms from django.utils.translation import gettext_lazy as _ from .cache import all_cookie_groups from .models import CookieGroup def iter_cookie_group_choices() -> Iterator[tuple[str, str]]: """ Use the cached cookie group instances to get a list of choices. """ for varname, cookie_group in all_cookie_groups().items(): yield varname, cookie_group.name class CookieGroupsChoiceField(forms.TypedMultipleChoiceField): def __init__(self, **kwargs): kwargs["coerce"] = self._coerce_choice kwargs["choices"] = iter_cookie_group_choices super().__init__(**kwargs) def _coerce_choice(self, varname: str) -> CookieGroup: all_groups = all_cookie_groups() return all_groups[varname] class ProcessCookiesForm(forms.Form): all_groups = forms.BooleanField( label=_("Apply to all cookie groups"), required=False, ) cookie_groups = CookieGroupsChoiceField( label=_("Cookie group varnames"), choices=iter_cookie_group_choices, required=False, ) def get_cookie_groups(self) -> Collection[CookieGroup]: """ Build the collection of specified cookies. """ match self.cleaned_data: case {"all_groups": True}: return all_cookie_groups().values() case {"cookie_groups": [*groups]}: return groups case _: return [] ================================================ FILE: cookie_consent/locale/en/LC_MESSAGES/django.po ================================================ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2013-06-09 10:12+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: models.py:16 msgid "" "Enter a valid 'varname' consisting of letters, numbers, underscores or " "hyphens." msgstr "" #: models.py:23 msgid "Variable name" msgstr "" #: models.py:26 models.py:66 msgid "Name" msgstr "" #: models.py:27 models.py:67 msgid "Description" msgstr "" #: models.py:29 msgid "Is required" msgstr "" #: models.py:30 msgid "Are cookies in this group required." msgstr "" #: models.py:33 msgid "Is deletable?" msgstr "" #: models.py:34 msgid "Can cookies in this group be deleted." msgstr "" #: models.py:36 msgid "Ordering" msgstr "" #: models.py:37 models.py:70 models.py:106 msgid "Created" msgstr "" #: models.py:40 msgid "Cookie Group" msgstr "" #: models.py:41 msgid "Cookie Groups" msgstr "" #: models.py:68 msgid "Path" msgstr "" #: models.py:69 msgid "Domain" msgstr "" #: models.py:73 msgid "Cookie" msgstr "" #: models.py:74 msgid "Cookies" msgstr "" #: models.py:95 templates/cookie_consent/_cookie_group.html:22 msgid "Declined" msgstr "" #: models.py:96 templates/cookie_consent/_cookie_group.html:13 msgid "Accepted" msgstr "" #: models.py:101 msgid "Action" msgstr "" #: models.py:105 msgid "Version" msgstr "" #: models.py:109 msgid "Log item" msgstr "" #: models.py:110 msgid "Log items" msgstr "" #: templates/cookie_consent/_cookie_group.html:17 msgid "Accept" msgstr "" #: templates/cookie_consent/_cookie_group.html:26 msgid "Decline" msgstr "" ================================================ FILE: cookie_consent/locale/nl/LC_MESSAGES/django.po ================================================ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: 0.4.0\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-09-04 15:38-0500\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Sergei Maertens \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: cookie_consent/models.py:14 msgid "" "Enter a valid 'varname' consisting of letters, numbers, underscores or " "hyphens." msgstr "" "Geef een geldige 'variabelenaam' op die bestaat uit letters, getallen, liggende " "streepjes of koppeltekens." #: cookie_consent/models.py:23 msgid "Variable name" msgstr "Variabelenaam" #: cookie_consent/models.py:25 cookie_consent/models.py:69 msgid "Name" msgstr "Naam" #: cookie_consent/models.py:26 cookie_consent/models.py:70 msgid "Description" msgstr "Omschrijving" #: cookie_consent/models.py:28 msgid "Is required" msgstr "Is verplicht" #: cookie_consent/models.py:29 msgid "Are cookies in this group required." msgstr "Of cookies in deze groep verplicht zijn." #: cookie_consent/models.py:33 msgid "Is deletable?" msgstr "Kan worden verwijderd?" #: cookie_consent/models.py:34 msgid "Can cookies in this group be deleted." msgstr "Of cookies in deze groep verwijderd kunnen worden." #: cookie_consent/models.py:37 msgid "Ordering" msgstr "Volgorde" #: cookie_consent/models.py:38 cookie_consent/models.py:73 #: cookie_consent/models.py:115 msgid "Created" msgstr "Aangemaakt" #: cookie_consent/models.py:41 msgid "Cookie Group" msgstr "Cookiegroep" #: cookie_consent/models.py:42 msgid "Cookie Groups" msgstr "Cookiegroepen" #: cookie_consent/models.py:71 msgid "Path" msgstr "Pad" #: cookie_consent/models.py:72 msgid "Domain" msgstr "Domein" #: cookie_consent/models.py:76 msgid "Cookie" msgstr "Cookie" #: cookie_consent/models.py:77 msgid "Cookies" msgstr "Cookies" #: cookie_consent/models.py:102 #: cookie_consent/templates/cookie_consent/_cookie_group.html:21 msgid "Declined" msgstr "Afgewezen" #: cookie_consent/models.py:103 #: cookie_consent/templates/cookie_consent/_cookie_group.html:12 msgid "Accepted" msgstr "Aanvaard" #: cookie_consent/models.py:108 msgid "Action" msgstr "Actie" #: cookie_consent/models.py:114 msgid "Version" msgstr "Versie" #: cookie_consent/models.py:121 msgid "Log item" msgstr "Logitem" #: cookie_consent/models.py:122 msgid "Log items" msgstr "Logitems" #: cookie_consent/templates/cookie_consent/_cookie_group.html:16 msgid "Accept" msgstr "Aanvaarden" #: cookie_consent/templates/cookie_consent/_cookie_group.html:25 msgid "Decline" msgstr "Weigeren" ================================================ FILE: cookie_consent/locale/oc/LC_MESSAGES/django.po ================================================ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2013-06-09 10:12+0200\n" "PO-Revision-Date: 2021-07-22 21:55+0200\n" "Language: oc\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Last-Translator: Quentin PAGÈS\n" "Language-Team: \n" "X-Generator: Poedit 3.0\n" "Plural-Forms: nplurals=2; plural=n > 1;\n" #: models.py:16 msgid "" "Enter a valid 'varname' consisting of letters, numbers, underscores or " "hyphens." msgstr "" "Picatz un « varname » valid compausat de letras, nombres, jonhents " "basses e jonhents." #: models.py:23 msgid "Variable name" msgstr "Nom de la variabla" #: models.py:26 models.py:66 msgid "Name" msgstr "Nom" #: models.py:27 models.py:67 msgid "Description" msgstr "Descripcion" #: models.py:29 msgid "Is required" msgstr "Es requesit" #: models.py:30 msgid "Are cookies in this group required." msgstr "Se los cookies d’aqueste grop son requesits." #: models.py:33 msgid "Is deletable?" msgstr "Se pòt suprimir ?" #: models.py:34 msgid "Can cookies in this group be deleted." msgstr "Se los cookies d’aqueste grop se pòdon suprimir." #: models.py:36 msgid "Ordering" msgstr "Òrdre" #: models.py:37 models.py:70 models.py:106 msgid "Created" msgstr "Creat lo" #: models.py:40 msgid "Cookie Group" msgstr "Grop de cookies" #: models.py:41 msgid "Cookie Groups" msgstr "Grops de cookies" #: models.py:68 msgid "Path" msgstr "Camin d'accès" #: models.py:69 msgid "Domain" msgstr "Domeni" #: models.py:73 msgid "Cookie" msgstr "Cookie" #: models.py:74 msgid "Cookies" msgstr "Cookies" #: models.py:95 templates/cookie_consent/_cookie_group.html:22 msgid "Declined" msgstr "Refusat" #: models.py:96 templates/cookie_consent/_cookie_group.html:13 msgid "Accepted" msgstr "Acceptat" #: models.py:101 msgid "Action" msgstr "Accion" #: models.py:105 msgid "Version" msgstr "Version" #: models.py:109 msgid "Log item" msgstr "Auditar element" #: models.py:110 msgid "Log items" msgstr "Auditar element" #: templates/cookie_consent/_cookie_group.html:17 msgid "Accept" msgstr "Acceptar" #: templates/cookie_consent/_cookie_group.html:26 msgid "Decline" msgstr "Refusar" ================================================ FILE: cookie_consent/locale/pt_BR/LC_MESSAGES/django.po ================================================ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-06-16 08:40+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #: .\django-cookie-consent\cookie_consent\models.py:16 msgid "" "Enter a valid 'varname' consisting of letters, numbers, underscores or " "hyphens." msgstr "" "Insira um 'varname' válido, composto por letras, números, underscores ou " "hífens." #: .\django-cookie-consent\cookie_consent\models.py:23 msgid "Variable name" msgstr "Nome da variável" #: .\django-cookie-consent\cookie_consent\models.py:26 #: .\django-cookie-consent\cookie_consent\models.py:67 msgid "Name" msgstr "Nome" #: .\django-cookie-consent\cookie_consent\models.py:27 #: .\django-cookie-consent\cookie_consent\models.py:68 msgid "Description" msgstr "Descrição" #: .\django-cookie-consent\cookie_consent\models.py:29 msgid "Is required" msgstr "" #: .\django-cookie-consent\cookie_consent\models.py:30 msgid "Are cookies in this group required." msgstr "Se os cookies deste grupo são necessários." #: .\django-cookie-consent\cookie_consent\models.py:33 msgid "Is deletable?" msgstr "" #: .\django-cookie-consent\cookie_consent\models.py:34 msgid "Can cookies in this group be deleted." msgstr "Se os cookies deste grupo podem serem apagados." #: .\django-cookie-consent\cookie_consent\models.py:36 msgid "Ordering" msgstr "Ordem" #: .\django-cookie-consent\cookie_consent\models.py:37 #: .\django-cookie-consent\cookie_consent\models.py:71 #: .\django-cookie-consent\cookie_consent\models.py:112 msgid "Created" msgstr "Criado em" #: .\django-cookie-consent\cookie_consent\models.py:40 msgid "Cookie Group" msgstr "Grupo de Cookies" #: .\django-cookie-consent\cookie_consent\models.py:41 msgid "Cookie Groups" msgstr "Grupos de Cookies" #: .\django-cookie-consent\cookie_consent\models.py:69 msgid "Path" msgstr "" #: .\django-cookie-consent\cookie_consent\models.py:70 msgid "Domain" msgstr "Domínio" #: .\django-cookie-consent\cookie_consent\models.py:74 msgid "Cookie" msgstr "" #: .\django-cookie-consent\cookie_consent\models.py:75 msgid "Cookies" msgstr "" #: .\django-cookie-consent\cookie_consent\models.py:100 #: .\django-cookie-consent\cookie_consent\templates\cookie_consent\_cookie_group.html:21 msgid "Declined" msgstr "Rejeitado" #: .\django-cookie-consent\cookie_consent\models.py:101 #: .\django-cookie-consent\cookie_consent\templates\cookie_consent\_cookie_group.html:12 msgid "Accepted" msgstr "Aceito" #: .\django-cookie-consent\cookie_consent\models.py:106 msgid "Action" msgstr "Ação" #: .\django-cookie-consent\cookie_consent\models.py:111 msgid "Version" msgstr "Versão" #: .\django-cookie-consent\cookie_consent\models.py:117 msgid "Log item" msgstr "" #: .\django-cookie-consent\cookie_consent\models.py:118 msgid "Log items" msgstr "" #: .\django-cookie-consent\cookie_consent\templates\cookie_consent\_cookie_group.html:16 msgid "Accept" msgstr "Aceitar" #: .\django-cookie-consent\cookie_consent\templates\cookie_consent\_cookie_group.html:25 msgid "Decline" msgstr "Recusar" ================================================ FILE: cookie_consent/locale/sl/LC_MESSAGES/django.po ================================================ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2013-06-09 10:26+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n" "%100==4 ? 2 : 3);\n" #: models.py:16 msgid "" "Enter a valid 'varname' consisting of letters, numbers, underscores or " "hyphens." msgstr "" #: models.py:23 msgid "Variable name" msgstr "" #: models.py:26 models.py:66 msgid "Name" msgstr "" #: models.py:27 models.py:67 msgid "Description" msgstr "" #: models.py:29 msgid "Is required" msgstr "" #: models.py:30 msgid "Are cookies in this group required." msgstr "" #: models.py:33 msgid "Is deletable?" msgstr "" #: models.py:34 msgid "Can cookies in this group be deleted." msgstr "" #: models.py:36 msgid "Ordering" msgstr "" #: models.py:37 models.py:70 models.py:106 msgid "Created" msgstr "" #: models.py:40 msgid "Cookie Group" msgstr "" #: models.py:41 msgid "Cookie Groups" msgstr "" #: models.py:68 msgid "Path" msgstr "" #: models.py:69 msgid "Domain" msgstr "" #: models.py:73 msgid "Cookie" msgstr "Piškot" #: models.py:74 msgid "Cookies" msgstr "Piškotki" #: models.py:95 templates/cookie_consent/_cookie_group.html:22 msgid "Declined" msgstr "Zavrnjeno" #: models.py:96 templates/cookie_consent/_cookie_group.html:13 msgid "Accepted" msgstr "Sprejeto" #: models.py:101 msgid "Action" msgstr "Ukrep" #: models.py:105 msgid "Version" msgstr "Različica" #: models.py:109 msgid "Log item" msgstr "" #: models.py:110 msgid "Log items" msgstr "" #: templates/cookie_consent/_cookie_group.html:17 msgid "Accept" msgstr "Strinjam se" #: templates/cookie_consent/_cookie_group.html:26 msgid "Decline" msgstr "Ne strinjam se" ================================================ FILE: cookie_consent/management/__init__.py ================================================ ================================================ FILE: cookie_consent/management/commands/__init__.py ================================================ ================================================ FILE: cookie_consent/management/commands/prune_cookie_consent_logs.py ================================================ from datetime import timedelta from django.core.management.base import BaseCommand from django.utils import timezone from ...models import LogItem class Command(BaseCommand): help = "Prune old LogItem records older than a given number of days." def add_arguments(self, parser): parser.add_argument( "--days", type=int, default=90, help="Delete log items older than this many days (default: 90).", ) def handle(self, *args, **options): days = options["days"] cutoff = timezone.now() - timedelta(days=days) deleted, _ = LogItem.objects.filter(created__lt=cutoff).delete() self.stdout.write( self.style.SUCCESS(f"Deleted {deleted} log item(s) older than {days} days.") ) ================================================ FILE: cookie_consent/middleware.py ================================================ from collections.abc import Callable from django.http import HttpRequest, HttpResponseBase from .cache import all_cookie_groups from .conf import settings from .util import get_cookie_dict_from_request, is_cookie_consent_enabled def _should_delete_cookie(group_version: str | None) -> bool: # declined after it was accepted (and set) before if group_version == settings.COOKIE_CONSENT_DECLINE: return True # if you need to opt-out instead of opt-in, then we only delete the cookie in the # above scenario -> when the group is explicitly declined if settings.COOKIE_CONSENT_OPT_OUT: return False # when we are opt-in and have no information whether the cookie group was accepted # or declined, delete the cookie(s). if group_version is None: return True return False class CleanCookiesMiddleware: """ Clean declined or non-accepted cookies. Note that this only applies if COOKIE_CONSENT_OPT_OUT is not set. """ def __init__(self, get_response: Callable[[HttpRequest], HttpResponseBase]): self.get_response = get_response def __call__(self, request: HttpRequest): response = self.get_response(request) if is_cookie_consent_enabled(request): self.process_response(request, response) return response def process_response(self, request: HttpRequest, response: HttpResponseBase): cookie_dic = get_cookie_dict_from_request(request) cookies_to_delete = [] for cookie_group in all_cookie_groups().values(): if not cookie_group.is_deletable: continue group_version = cookie_dic.get(cookie_group.varname, None) for cookie in cookie_group.cookie_set.all(): if cookie.name not in request.COOKIES: continue if _should_delete_cookie(group_version): cookies_to_delete.append(cookie) for cookie in cookies_to_delete: response.delete_cookie(cookie.name, cookie.path, cookie.domain) return response ================================================ FILE: cookie_consent/migrations/0001_initial.py ================================================ # Generated by Django 2.1 on 2019-02-08 14:14 import re import django.core.validators import django.db.models.deletion from django.db import migrations, models class Migration(migrations.Migration): initial = True dependencies = [] operations = [ migrations.CreateModel( name="Cookie", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("name", models.CharField(max_length=250, verbose_name="Name")), ( "description", models.TextField(blank=True, verbose_name="Description"), ), ( "path", models.TextField(blank=True, default="/", verbose_name="Path"), ), ( "domain", models.CharField(blank=True, max_length=250, verbose_name="Domain"), ), ( "created", models.DateTimeField(auto_now_add=True, verbose_name="Created"), ), ], options={ "verbose_name": "Cookie", "verbose_name_plural": "Cookies", "ordering": ["-created"], }, ), migrations.CreateModel( name="CookieGroup", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ( "varname", models.CharField( max_length=32, validators=[ django.core.validators.RegexValidator( re.compile("^[-_a-zA-Z0-9]+$"), "Enter a valid 'varname' consisting of letters, " "numbers, underscores or hyphens.", "invalid", ) ], verbose_name="Variable name", ), ), ( "name", models.CharField(blank=True, max_length=100, verbose_name="Name"), ), ( "description", models.TextField(blank=True, verbose_name="Description"), ), ( "is_required", models.BooleanField( default=False, help_text="Are cookies in this group required.", verbose_name="Is required", ), ), ( "is_deletable", models.BooleanField( default=True, help_text="Can cookies in this group be deleted.", verbose_name="Is deletable?", ), ), ("ordering", models.IntegerField(default=0, verbose_name="Ordering")), ( "created", models.DateTimeField(auto_now_add=True, verbose_name="Created"), ), ], options={ "verbose_name": "Cookie Group", "verbose_name_plural": "Cookie Groups", "ordering": ["ordering"], }, ), migrations.AddField( model_name="cookie", name="cookiegroup", field=models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, to="cookie_consent.CookieGroup", verbose_name="Cookie Group", ), ), ] ================================================ FILE: cookie_consent/migrations/0002_auto__add_logitem.py ================================================ # Generated by Django 2.1 on 2019-02-08 14:16 import django.db.models.deletion from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("cookie_consent", "0001_initial"), ] operations = [ migrations.CreateModel( name="LogItem", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ( "action", models.IntegerField( choices=[(-1, "Declined"), (1, "Accepted")], verbose_name="Action", ), ), ("version", models.CharField(max_length=32, verbose_name="Version")), ( "created", models.DateTimeField(auto_now_add=True, verbose_name="Created"), ), ( "cookiegroup", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, to="cookie_consent.CookieGroup", verbose_name="Cookie Group", ), ), ], options={ "verbose_name": "Log item", "verbose_name_plural": "Log items", "ordering": ["-created"], }, ), ] ================================================ FILE: cookie_consent/migrations/0003_alter_cookiegroup_varname.py ================================================ # Generated by Django 4.2.13 on 2024-05-09 19:01 import re import django.core.validators from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("cookie_consent", "0002_auto__add_logitem"), ] operations = [ migrations.AlterField( model_name="cookiegroup", name="varname", field=models.CharField( max_length=32, unique=True, validators=[ django.core.validators.RegexValidator( re.compile("^[-_a-zA-Z0-9]+$"), "Enter a valid 'varname' consisting of letters, numbers, " "underscores or hyphens.", "invalid", ) ], verbose_name="Variable name", ), ), ] ================================================ FILE: cookie_consent/migrations/0004_cookie_natural_key.py ================================================ # Generated by Django 4.2.13 on 2024-05-09 20:22 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("cookie_consent", "0003_alter_cookiegroup_varname"), ] operations = [ migrations.AddConstraint( model_name="cookie", constraint=models.UniqueConstraint( fields=("cookiegroup", "name", "domain"), name="natural_key" ), ), ] ================================================ FILE: cookie_consent/migrations/__init__.py ================================================ ================================================ FILE: cookie_consent/models.py ================================================ from __future__ import annotations import re from collections.abc import Callable from typing import ClassVar, ParamSpec, TypedDict, TypeVar from django.core.validators import RegexValidator from django.db import models from django.utils.translation import gettext_lazy as _ COOKIE_NAME_RE = re.compile(r"^[-_a-zA-Z0-9]+$") validate_cookie_name = RegexValidator( COOKIE_NAME_RE, _( "Enter a valid 'varname' consisting of letters, numbers" ", underscores or hyphens." ), "invalid", ) P = ParamSpec("P") T = TypeVar("T") def clear_cache_after(func: Callable[P, T]) -> Callable[P, T]: def wrapper(*args: P.args, **kwargs: P.kwargs): from .cache import delete_cache return_value = func(*args, **kwargs) delete_cache() return return_value return wrapper class CookieGroupDict(TypedDict): varname: str name: str description: str is_required: bool # The version is deliberately not included because it requires page/view cache # busting if a new cookie gets added to the group, which we don't control. class BaseQueryset(models.query.QuerySet): @clear_cache_after def delete(self): return super().delete() @clear_cache_after def update(self, **kwargs): return super().update(**kwargs) class CookieGroupManager(models.Manager.from_queryset(BaseQueryset)): def get_by_natural_key(self, varname: str) -> CookieGroup: return self.get(varname=varname) class CookieGroup(models.Model): varname = models.CharField( _("Variable name"), max_length=32, unique=True, validators=[validate_cookie_name], ) name = models.CharField(_("Name"), max_length=100, blank=True) description = models.TextField(_("Description"), blank=True) is_required = models.BooleanField( _("Is required"), help_text=_("Are cookies in this group required."), default=False, ) is_deletable = models.BooleanField( _("Is deletable?"), help_text=_("Can cookies in this group be deleted."), default=True, ) ordering = models.IntegerField(_("Ordering"), default=0) created = models.DateTimeField(_("Created"), auto_now_add=True, blank=True) objects: ClassVar[CookieGroupManager] = CookieGroupManager() # pyright: ignore[reportIncompatibleVariableOverride] cookie_set: ClassVar[CookieManager] class Meta: verbose_name = _("Cookie Group") verbose_name_plural = _("Cookie Groups") ordering = ["ordering"] def __str__(self): return self.name @clear_cache_after def save(self, *args, **kwargs): super().save(*args, **kwargs) @clear_cache_after def delete(self, *args, **kwargs): return super().delete(*args, **kwargs) def natural_key(self) -> tuple[str]: return (self.varname,) def get_version(self) -> str: try: # this relies on the cookie set being ordered by most-recently created # first. # Note that we don't use `.first()` as that's a new query and bypasses # the cache. return str(self.cookie_set.all()[0].get_version()) except IndexError: return "" def for_json(self) -> CookieGroupDict: return { "varname": self.varname, "name": self.name, "description": self.description, "is_required": self.is_required, # "version": self.get_version(), } class CookieManager(models.Manager.from_queryset(BaseQueryset)): def get_by_natural_key(self, name: str, domain: str, cookiegroup: str) -> Cookie: group = CookieGroup.objects.get_by_natural_key(cookiegroup) return self.get(cookiegroup=group, name=name, domain=domain) class Cookie(models.Model): cookiegroup = models.ForeignKey( CookieGroup, verbose_name=CookieGroup._meta.verbose_name, on_delete=models.CASCADE, ) name = models.CharField(_("Name"), max_length=250) description = models.TextField(_("Description"), blank=True) path = models.TextField(_("Path"), blank=True, default="/") domain = models.CharField(_("Domain"), max_length=250, blank=True) created = models.DateTimeField(_("Created"), auto_now_add=True, blank=True) objects = CookieManager() class Meta: verbose_name = _("Cookie") verbose_name_plural = _("Cookies") constraints = [ models.UniqueConstraint( fields=("cookiegroup", "name", "domain"), name="natural_key", ), ] ordering = ["-created"] def __str__(self): return f"{self.name} {self.domain}{self.path}" @clear_cache_after def save(self, *args, **kwargs): super().save(*args, **kwargs) @clear_cache_after def delete(self, *args, **kwargs): return super().delete(*args, **kwargs) def natural_key(self) -> tuple[str, str, str]: return (self.name, self.domain) + self.cookiegroup.natural_key() natural_key.dependencies = ["cookie_consent.cookiegroup"] # pyright: ignore[reportFunctionMemberAccess] @property def varname(self) -> str: group_varname = self.cookiegroup.varname return f"{group_varname}={self.name}:{self.domain}" def get_version(self) -> str: return self.created.isoformat() ACTION_ACCEPTED = 1 ACTION_DECLINED = -1 ACTION_CHOICES = ( (ACTION_DECLINED, _("Declined")), (ACTION_ACCEPTED, _("Accepted")), ) class LogItem(models.Model): action = models.IntegerField(_("Action"), choices=ACTION_CHOICES) cookiegroup = models.ForeignKey( CookieGroup, verbose_name=CookieGroup._meta.verbose_name, on_delete=models.CASCADE, ) version = models.CharField(_("Version"), max_length=32) created = models.DateTimeField(_("Created"), auto_now_add=True, blank=True) class Meta: verbose_name = _("Log item") verbose_name_plural = _("Log items") ordering = ["-created"] def __str__(self): return f"{self.cookiegroup.name} {self.version}" ================================================ FILE: cookie_consent/processor.py ================================================ from collections.abc import Collection from typing import Literal from django.http import HttpRequest, HttpResponseBase from .conf import settings from .models import ACTION_ACCEPTED, ACTION_DECLINED, CookieGroup, LogItem from .util import get_cookie_dict_from_request, set_cookie_dict_to_response class CookiesProcessor: """ Process the accept/decline logic for cookie groups. """ def __init__(self, request: HttpRequest, response: HttpResponseBase): self.request = request self.response = response def process( self, cookie_groups: Collection[CookieGroup], action: Literal["accept", "decline"], ) -> None: """ Apply ``action`` to the specified ``cookie_groups``. Mutates the response by updating the cookie tracking the cookie group status. If there are no cookie groups provided, nothing happens. """ if not cookie_groups: return cookie_dic = get_cookie_dict_from_request(self.request) match action: case "accept": for cookie_group in cookie_groups: cookie_dic[cookie_group.varname] = cookie_group.get_version() case "decline": self._delete_cookies(cookie_groups) for cookie_group in cookie_groups: cookie_dic[cookie_group.varname] = settings.COOKIE_CONSENT_DECLINE self._log_action(cookie_groups, action) set_cookie_dict_to_response(self.response, cookie_dic) def _log_action( self, cookie_groups: Collection[CookieGroup], action: Literal["accept", "decline"], ) -> None: if not settings.COOKIE_CONSENT_LOG_ENABLED: return # TODO: replace with stdlib logging call/helper instead of creating DB records # directly. action_map: dict[Literal["accept", "decline"], int] = { "accept": ACTION_ACCEPTED, "decline": ACTION_DECLINED, } log_items: list[LogItem] = [ LogItem( action=action_map[action], cookiegroup=cookie_group, version=cookie_group.get_version(), ) for cookie_group in cookie_groups ] LogItem.objects.bulk_create(log_items) def _delete_cookies(self, cookie_groups: Collection[CookieGroup]) -> None: for cookie_group in cookie_groups: if not cookie_group.is_deletable: continue for cookie in cookie_group.cookie_set.all(): self.response.delete_cookie(cookie.name, cookie.path, cookie.domain) ================================================ FILE: cookie_consent/py.typed ================================================ ================================================ FILE: cookie_consent/templates/cookie_consent/_cookie_group.html ================================================ {% load i18n %} {% load cookie_consent_tags %} ================================================ FILE: cookie_consent/templates/cookie_consent/base.html ================================================ {% block body %} {% endblock %} ================================================ FILE: cookie_consent/templates/cookie_consent/cookiegroup_list.html ================================================ {% extends "cookie_consent/base.html" %} {% block body %}

Cookies

This is a list of the categories of cookies used in our website and why we use them.

{% for cookie_group in object_list %} {% include "cookie_consent/_cookie_group.html" %} {% endfor %} {% endblock %} ================================================ FILE: cookie_consent/templatetags/__init__.py ================================================ #!/usr/bin/env python ================================================ FILE: cookie_consent/templatetags/cookie_consent_tags.py ================================================ from collections.abc import Collection from django import template from django.http import HttpRequest from django.utils.html import json_script from ..cache import all_cookie_groups as get_all_cookie_groups from ..models import CookieGroup from ..util import ( are_all_cookies_accepted, get_cookie_value_from_request, get_not_accepted_or_declined_cookie_groups, is_cookie_consent_enabled, ) register = template.Library() @register.filter def cookie_group_accepted(request: HttpRequest, group_or_cookie: str) -> bool: """ Return ``True`` if the cookie group/cookie is accepted. Examples: .. code-block:: django {{ request|cookie_group_accepted:"analytics" }} {{ request|cookie_group_accepted:"analytics=*:.google.com" }} """ value = get_cookie_value_from_request(request, *group_or_cookie.split("=")) return value is True @register.filter def cookie_group_declined(request: HttpRequest, group_or_cookie: str) -> bool: """ Return ``True`` if the cookie group/cookie is declined. Examples: .. code-block:: django {{ request|cookie_group_declined:"analytics" }} {{ request|cookie_group_declined:"analytics=*:.google.com" }} """ value = get_cookie_value_from_request(request, *group_or_cookie.split("=")) return value is False @register.filter def all_cookies_accepted(request: HttpRequest) -> bool: """ Filter returns if all cookies are accepted. """ return are_all_cookies_accepted(request) @register.simple_tag def not_accepted_or_declined_cookie_groups( request: HttpRequest, ) -> Collection[CookieGroup]: """ Return the cookie groups for which no explicit accept or decline has been given. """ return get_not_accepted_or_declined_cookie_groups(request) @register.filter def cookie_consent_enabled(request: HttpRequest) -> bool: """ Indicate whether the cookie-consent app is enabled or not. """ return is_cookie_consent_enabled(request) @register.simple_tag def all_cookie_groups(element_id: str): """ Serialize all cookie groups to JSON and output them in a script tag. :param element_id: The ID for the script tag so you can look it up in JS later. This uses Django's core json_script filter under the hood. """ groups = get_all_cookie_groups() value = [group.for_json() for group in groups.values()] return json_script(value, element_id) ================================================ FILE: cookie_consent/urls.py ================================================ from django.urls import path from .views import ( CookieGroupAcceptView, CookieGroupDeclineView, CookieGroupListView, CookieStatusView, ) urlpatterns = [ path("accept/", CookieGroupAcceptView.as_view(), name="cookie_consent_accept"), path("decline/", CookieGroupDeclineView.as_view(), name="cookie_consent_decline"), path("status/", CookieStatusView.as_view(), name="cookie_consent_status"), path("", CookieGroupListView.as_view(), name="cookie_consent_cookie_group_list"), ] ================================================ FILE: cookie_consent/util.py ================================================ import logging from collections.abc import Callable, Collection, Iterator from django.http import HttpRequest, HttpResponseBase from .cache import all_cookie_groups, get_cookie, get_cookie_group from .conf import settings from .models import Cookie, CookieGroup logger = logging.getLogger(__name__) COOKIE_GROUP_SEP = "|" KEY_VALUE_SEP = "=" def parse_cookie_str(cookie: str) -> dict[str, str]: if not cookie: return {} bits = cookie.split(COOKIE_GROUP_SEP) def _gen_pairs() -> Iterator[tuple[str, str]]: for possible_pair in bits: parts = possible_pair.split(KEY_VALUE_SEP) if len(parts) == 2: varname, cookie = parts yield varname, cookie else: logger.debug("cookie_value_discarded", extra={"value": possible_pair}) return dict(_gen_pairs()) def _contains_invalid_characters(*inputs: str) -> bool: # = and | are special separators. They are unexpected characters in both # keys and values. for separator in (COOKIE_GROUP_SEP, KEY_VALUE_SEP): for value in inputs: if separator in value: logger.debug("skip_separator", extra={"value": value, "sep": separator}) return True return False def dict_to_cookie_str(dic: dict[str, str]) -> str: """ Serialize a dictionary of cookie-group metadata to a string. The result is stored in a cookie itself. Note that the dictionary keys are expected to be cookie group ``varname`` fields, which are validated against a slug regex. The values are supposed to be ISO-8601 timestamps. Invalid key/value pairs are dropped. """ def _gen_pairs() -> Iterator[str]: for key, value in dic.items(): if _contains_invalid_characters(key, value): continue yield f"{key}={value}" return "|".join(_gen_pairs()) def get_cookie_dict_from_request(request: HttpRequest) -> dict[str, str]: cookie_str = request.COOKIES.get(settings.COOKIE_CONSENT_NAME, "") return parse_cookie_str(cookie_str) def set_cookie_dict_to_response( response: HttpResponseBase, dic: dict[str, str] ) -> None: response.set_cookie( settings.COOKIE_CONSENT_NAME, dict_to_cookie_str(dic), max_age=settings.COOKIE_CONSENT_MAX_AGE, domain=settings.COOKIE_CONSENT_DOMAIN, secure=settings.COOKIE_CONSENT_SECURE, httponly=settings.COOKIE_CONSENT_HTTPONLY, samesite=settings.COOKIE_CONSENT_SAMESITE, ) def get_cookie_value_from_request( request: HttpRequest, varname: str, cookie: str = "" ) -> bool | None: """ Returns if cookie group or its specific cookie has been accepted. Returns True or False when cookie is accepted or declined or None if cookie is not set. """ if not (cookie_dic := get_cookie_dict_from_request(request)): return None if not (cookie_group := get_cookie_group(varname=varname)): return None _cookie: Cookie | None = None if cookie: name, domain = cookie.split(":") _cookie = get_cookie(cookie_group, name, domain) match version := cookie_dic.get(varname, None): case None: return None case str() if version == settings.COOKIE_CONSENT_DECLINE: return False reference_version = _cookie.get_version() if _cookie else cookie_group.get_version() if version >= reference_version: return True return None def get_cookie_groups(varname: str = "") -> Collection[CookieGroup]: if not varname: return all_cookie_groups().values() keys = varname.split(",") return [g for k, g in all_cookie_groups().items() if k in keys] def are_all_cookies_accepted(request: HttpRequest) -> bool: """ Returns if all cookies are accepted. """ return all( [ get_cookie_value_from_request(request, cookie_group.varname) for cookie_group in get_cookie_groups() ] ) def _get_cookie_groups_by_state(request, state: bool | None) -> Collection[CookieGroup]: return [ cookie_group for cookie_group in get_cookie_groups() if get_cookie_value_from_request(request, cookie_group.varname) is state ] def get_not_accepted_or_declined_cookie_groups( request: HttpRequest, ) -> Collection[CookieGroup]: """ Returns all cookie groups that are neither accepted or declined. """ return _get_cookie_groups_by_state(request, state=None) def get_accepted_cookie_groups(request: HttpRequest) -> Collection[CookieGroup]: """ Returns all cookie groups that are accepted. """ return _get_cookie_groups_by_state(request, state=True) def get_declined_cookie_groups(request: HttpRequest) -> Collection[CookieGroup]: """ Returns all cookie groups that are declined. """ return _get_cookie_groups_by_state(request, state=False) def is_cookie_consent_enabled(request: HttpRequest) -> bool: """ Returns if django-cookie-consent is enabled for given request. """ enabled: bool | Callable[[HttpRequest], bool] = settings.COOKIE_CONSENT_ENABLED return enabled(request) if callable(enabled) else enabled ================================================ FILE: cookie_consent/views.py ================================================ from typing import Literal from django.contrib.auth.views import RedirectURLMixin from django.http import ( HttpRequest, HttpResponse, HttpResponseBase, HttpResponseRedirect, JsonResponse, ) from django.middleware.csrf import get_token as get_csrf_token from django.urls import reverse from django.views.generic import ListView, View from .conf import settings from .forms import ProcessCookiesForm from .models import CookieGroup from .processor import CookiesProcessor from .util import ( get_accepted_cookie_groups, get_declined_cookie_groups, get_not_accepted_or_declined_cookie_groups, ) def is_ajax_like(request: HttpRequest) -> bool: # legacy ajax, removed in Django 4.0 (used to be request.is_ajax()) ajax_header = request.headers.get("X-Requested-With") if ajax_header == "XMLHttpRequest": return True # module-js uses fetch and a custom header return bool(request.headers.get("X-Cookie-Consent-Fetch")) class CookieGroupListView(ListView): """ Display all cookies. """ model = CookieGroup class CookieGroupBaseProcessView(RedirectURLMixin, View): """ Process the cookie groups submitted in the POST request (or URL parameters). :class:`RedirectURLMixin` takes care of the hardening against open redirects. """ cookie_process_action: Literal["accept", "decline"] """ Processing action to apply, must be set on the subclasses. """ def get_default_redirect_url(self) -> str: return settings.COOKIE_CONSENT_SUCCESS_URL def post(self, request: HttpRequest, *args, **kwargs): form = ProcessCookiesForm(data=request.POST) if not form.is_valid(): if is_ajax_like(request): return JsonResponse(form.errors.get_json_data()) else: return HttpResponse(form.errors.render()) cookie_groups = form.get_cookie_groups() response: HttpResponseBase if is_ajax_like(request): response = HttpResponse() else: response = HttpResponseRedirect(self.get_success_url()) processor = CookiesProcessor(request, response) processor.process(cookie_groups, action=self.cookie_process_action) return response class CookieGroupAcceptView(CookieGroupBaseProcessView): """ View to accept CookieGroup. """ cookie_process_action = "accept" class CookieGroupDeclineView(CookieGroupBaseProcessView): """ View to decline CookieGroup. """ cookie_process_action = "decline" class CookieStatusView(View): """ Check the current accept/decline status for cookies. The returned accept and decline URLs are specific to this user and include the cookie groups that weren't accepted or declined yet. Note that this endpoint also returns a CSRF Token to be used by the frontend, as baking a CSRFToken into a cached page will not reliably work. """ def get(self, request: HttpRequest) -> JsonResponse: accepted = get_accepted_cookie_groups(request) declined = get_declined_cookie_groups(request) not_accepted_or_declined = get_not_accepted_or_declined_cookie_groups(request) data = { "csrftoken": get_csrf_token(request), "acceptUrl": reverse("cookie_consent_accept"), "declineUrl": reverse("cookie_consent_decline"), "acceptedCookieGroups": [group.varname for group in accepted], "declinedCookieGroups": [group.varname for group in declined], "notAcceptedOrDeclinedCookieGroups": [ group.varname for group in not_accepted_or_declined ], } return JsonResponse(data) ================================================ FILE: docs/Makefile ================================================ # Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-shop-discounts.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-shop-discounts.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/django-shop-discounts" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-shop-discounts" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." make -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." ================================================ FILE: docs/changelog.rst ================================================ ========= Changelog ========= 1.0.0 (2026-02-04) ------------------ The long-awaited 1.0 version of django-cookie-consent is finally here! The project has seen numerous cleanups, clarifications and (hopefully) simplifications over the past few releases, but now the public API is considered stable and any future breaking changes will result in a major version bump. The documentation :ref:`Migrating to 1.0 ` is there to help you updating your projects, but don't hesitate to open a Github issue if you run into problems. **💥 Breaking changes** *Now* was the time to perform necessary cleanups that break the existing API/features. The upgrade documentation covers these in more detail. * [#106] Views and URLs rework: * Removed the URL patterns ``accept//`` and ``decline//``. * Renamed the ``cookie_consent_accept_all`` view name to ``cookie_consent_accept``. * Renamed the ``cookie_consent_decline_all`` view name to ``cookie_consent_decline``. * Replaced the ``CookieGroupBaseProcessView.process`` method with the new ``CookiesProcessor`` utility class. * The views no longer take the cookie groups/cookie group varnames from the ``varname`` URL parameter - it has been removed. * The cookie accept/decline views are no longer ``csrf_exempt`` and require a CRSF token now. * The ``cookie_consent_accept_url`` and ``cookie_consent_decline_url`` template tags are removed due to the URL structure changes. * Removed the ``DELETE`` method support for the cookie group decline view. * [#108] Legacy cookiebar Javascript removal: * The legacy implementation is removed. * Removed the ``get_accept_cookie_groups_cookie_string``, ``get_decline_cookie_groups_cookie_string``, ``js_type_for_cookie_consent`` and ``accepted_cookies`` template tags due to being incompatible with template/view caching. * Dropped support for the end-of-life Django 5.1. **New features** * Confirmed support for Django 6.0. * Confirmed support for Python 3.14. * [#106] You can now use plain HTTP POST form semantics to submit the cookie groups to accept/decline. * Added static type annotations to the project and added the ``py.typed`` package marker. **Project maintenance** * Added django-upgrade to pre-commit hooks. * Converted more tests to pytest style and refactored accept/decline views. * Cleaned up/updated the documentation. * Renamed the master branch to main. * Switch the version management tool from tbump to bump-my-version. 0.9.0 (2025-09-28) ------------------ Maintenance and bugfix release. **💥 Breaking changes** * Dropped support for Python 3.8 (end-of-life). * Dropped support for Python 3.9 (soon end-of-life). **New features** * [#114] You can now customize the redirect behaviour after accepting/declining cookies, using the new ``COOKIE_CONSENT_SUCCESS_URL`` setting. * The admin now displays a warning for cookie groups that have no cookies in them. **Bugfixes** * [#135] Make cookie parsing and serialization more robust. **Project maintenance** * Transferred the package from the jazzband Github organization to django-commons, and updated all community-related documents like the code of conduct. * Updated the package metadata to latest format. * Bumped minimum required setuptools version to build the package. * Replaced black and isort with Ruff for linting and code-formatting duties. * Improved the test suite. * The PyPI and NPM packages are now automatically published with Trusted Publishing, enhancing supply-chain security. 0.8.0 (2025-05-30) ------------------ Small feature release **New features** * The Javascript (package) now exports some more utilities (#126). **Project maintenance** * Fixed some test flakiness. 0.7.0 (2025-04-26) ------------------ Bugfix and Django supported versions release. **New features** * Confirmed Python 3.13 support. * Confirmed Django 5.1 and 5.2 support. **Project maintenance** * Fixed incorrect JS example in the documentation. * Removed Django 5.0 from CI pipeline (it still works, but Django 5.0 is end of life). * Upgraded esbuild for the Javascript module building. 0.6.0 (2024-05-10) ------------------ Feature release with improved JS support. **💥 Breaking changes** Some (database) unique constraints have been added to the model fields. If you have duplicate values in those fields, the migrations will crash. You should check for duplicates before upgrading, and fix those: .. code-block:: py from django.db.models import Count from cookie_consent.models import CookieGroup, Cookie # duplicated cookie groups (by varname) CookieGroup.objects.values("varname").annotate(n=Count("varname")).filter(n__gt=1) # # duplicated cookies Cookie.objects.values("cookiegroup", "name", "domain").annotate(n=Count("id")).filter(n__gt=1) # Additionally, support for unmaintained Django versions (3.2, 4.1) is dropped. **New features** * The JS for the cookiebar module is rewritten in TypeScript and published as an `npm package`_ for people wishing to integrate this functionality in their own frontend stack. ``cookie_consent/cookiebar.module.js`` is still in the Python package, and it's generated from the same source code. * Added support for natural keys in dumpdata/loaddata management commands. **Deprecations** None. **Bugfixes** * Fixed cache not being cleared after queryset (bulk) update/deletes * Swapped the order of ``onShow`` and ``doInsert`` in the cookiebar JS. ``onShow`` is now called after the cookiebar is inserted into the document. * Added missing unique constraint to ``CookieGroup.varname`` field * Added missing unique constraint on ``Cookie`` fields ``cookiegroup``, ``name`` and ``domain``. **Project maintenance** * Add missing templatetag instruction to docs * Removed Django < 4.2 compatibility shims * Formatted code with latest black version * Dropped Django 3.2 & 4.1 from the supported versions * Removed unused dependencies * Bumped github actions to latest versions * Updated to modern packaging tooling with ``pyproject.toml`` .. _npm package: https://www.npmjs.com/package/django-cookie-consent 0.5.0b0 (2023-09-24) -------------------- A django-cookie-consent version to test the new Javascript integration. You can install this using: .. code-block:: bash pip install django-cookie-consent --pre The new cookiebar JS uses a modern approach and should resolve issues with page caches and Content Security Policies. Please try it out and report any issues or suggestion on Github! **Breaking changes** None **New features** * Implemented ``cookie_consent/cookiebar.module.js`` as a new Javascript integration. Please review the updated documentation for usage instructions. (#15, #49, #99) **Deprecations** Deprecated functionality is scheduled for removal in django-cookie-consent 1.0. * Deprecated ``cookie_consent/cookiebar.js`` and added an alias ``legacyShowCookieBar``. Existing users are advised to upgrade to the new module approach, or at the very least substitute ``showCookieBar`` with ``window.legacyShowCookieBar`` to better keep track of this deprecation. * Deprecated template tags that build up cookie strings suitable for Javascript. **Bugfixes** None **Project maintenance** * Extensively documented the new cookiebar JS usage. * Added Playwright for end-to-end testing (covers both the new and legacy cookie bar) * Removed unnecessary ``smart_str`` usage - thanks @some1ataplace * Test app and tests themselves are now excluded from coverage measuring for more a more accurate reflection of the coverage status. 0.4.0 (2023-06-11) ------------------ .. note:: The 0.4.0 release mainly has had a project management overhaul. The project has transferred to the Jazzband organization. This release mostly focuses on Python/Django version compatibility and organization of tests, CI etc. Many thanks for people who reported bugs, and especially, your patience for getting this release on PyPI. **Breaking changes** * Dropped support for Django 2.2, 3.0, 3.1 and 4.0 * Dropped support for Python 3.6 and 3.7 These versions are (nearly) end-of-life and no longer supported by their upstream teams. **New features** * Implemented settings for cookie flags: SameSite, HttpOnly, Secure, domain (#27, #60, #36, #88) * Added Dutch translations **Bugfixes** * Cache instance resolution is now lazy (#41) * Fixed support for Django 4.1 (#73) - thanks @alahdal * Fixed default settings being bytestrings (#24, #55, #69) * Fixed the middleware to clean cookies (#13) - thanks @some1ataplace * Fixed bug in JS ``beforeDeclined`` attribute **Project maintenance** * Transferred project to Jazzband (#38, #64, #75) * Replaced Travis CI with Github Actions (#64, #75) * Set up correct test matrix for python/django versions (#75) * Code is now ``isort`` and ``black`` formatted (#75) * Set up ``tox`` and ``pytest`` for testing (#64, #75) * 'Removed' the example app - the ``testapp`` in the repository is still a good example * Configured tbump for the release flow * Confirmed support for Python 3.11 and Django 4.2 * Added explicit template tag tests (#39) **Documentation** Did some initial restructuring to make the docs easier to digest, more to come. * Added documentation on how to contribute * Corrected settings documentation (#53, #14) * Documented ``cookiebar.js`` usage (#90) - thanks @MrCordeiro * Added better contributor documentation and example app documentation based on the ``testapp`` in the repository. 0.3.1 (2022-02-17) ------------------ - Protect against open redirect after accepting cookies (#48) 0.3.0 (2021-12-08) ------------------ * support ranges from django 2.2 to 4.0 and python 3.6 to 3.9 0.2.6 (2020-06-17) ------------------ * fix: setup for python 2.7 0.2.5 (2020-06-17) ------------------ * chore: add package descriptions 0.2.4 (2020-06-17) ------------------ * Cookie Bar Choosing Decline Not Disappearing Right Away (#22) * 📦 NEW: pt_BR (#23) 0.2.3 (2020-06-15) ------------------ * Update package classifiers 0.2.2 (2020-06-15) ------------------ * 8732949 Remove jquery (#20) 0.2.1 (2020-06-02) ------------------ * fix: Set max version for django-appconf (#18) * fix: Views ignore 'next' url parameter (#12) * Update configuration.rst 0.2.0 (2020-02-11) ------------------ * support ranges from django 1.9 to 3.0 and python 2.7 to 3.7 (JonHerr) 0.1.1 ----- * tweak admin * Add accepted_cookies template filter * Add varname property to Cookie model * Add translation catalog 0.1.0 ----- * Initial release ================================================ FILE: docs/check_sphinx.py ================================================ import subprocess def test_linkcheck(tmpdir): doctrees = tmpdir.join("doctrees") htmldir = tmpdir.join("html") subprocess.check_call( ["sphinx-build", "-W", "-blinkcheck", "-d", str(doctrees), ".", str(htmldir)], ) def test_build_docs(tmpdir): doctrees = tmpdir.join("doctrees") htmldir = tmpdir.join("html") subprocess.check_call( ["sphinx-build", "-W", "-bhtml", "-d", str(doctrees), ".", str(htmldir)], ) ================================================ FILE: docs/concept.rst ================================================ ============= Main concepts ============= Cookie Group ------------ The :class:`CookieGroup ` model represents a group of related cookies. Cookie groups can be either required or not. Users can accept or decline the use of the cookies in the non-required cookie groups. Versions ^^^^^^^^ Each cookie group has a "current version" set to the timestamp of the last added Cookie in it. When a user accepts a cookie group, the current version (at the time of consent) is :ref:`saved `. Versions allow django-cookie-consent to check if new cookies have been introduced after the user has given consent for a cookie group, so that they can be prompted again to accept the new cookie(s). Important attributes: ^^^^^^^^^^^^^^^^^^^^^ ``varname`` The variable name that acts as unique identifier for the cookie group. You can use the value in template tags and filters to refer to a particular cookie group. ``is_required`` Required cookies are never deleted and users cannot accept or decline them. For example, Django's default ``sessionid`` and ``csrftoken`` cookies are required for the correct functioning of your project. Without these cookies, the website will not work properly - so users can't opt-out. ``is_deletable`` If a cookie group is marked as deletable, django-cookie-consent will try to delete the cookies in this group when the user declines the group, or through the :class:`cookie_consent.middleware.CleanCookiesMiddleware`. Cookie ------ The :class:`Cookie` model represents as single cookie. .. admonition:: Domain and path :collapsible: closed The ``domain`` and ``path`` fields are important to be able to delete the cookies programmatically. Keep in mind that only cookies of your own domain can be deleted. .. _concept_storing_consent: Saving user selection --------------------- A bit ironically, django-cookie-consent uses a cookie itself to store the user consent. By default, the name ``cookie_consent`` is used. An example value of such a cookie could be: .. code-block:: none optional=-1|social=2013-06-04T03:17:01.421395Z The meaning of this is: * the user declined the cookie group with varname ``optional`` * the user accepted the cookie group with varname ``social``, and specifically only the cookies that were created before the stated timestamp Caching ------- django-cookie-consent keeps the non-required cookie groups and cookies in cache, to avoid hitting the database for each request. By default, the ``default`` Django cache is used. You can modify this, see :ref:`settings`. .. note:: Django's default cache is a local-memory cache. Cache invalidation in one wsgi-server process will not propagate to other instances/processes, so you can temporarily see inconsistent results. It's recommended to use a shared cache like Redis/Valkey or Memcache. ================================================ FILE: docs/conf.py ================================================ import os import sys from pathlib import Path import django # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. root_dir = Path(__file__).parent.parent.resolve() sys.path.insert(0, str(root_dir)) os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testapp.settings") django.setup() from cookie_consent import __version__ # noqa: E402 # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ["sphinx.ext.autodoc"] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix of source filenames. source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = "index" # General information about the project. project = "django-cookie-consent" copyright = "2013, Bojan Mihelac" release = __version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = "django-cookie-consent" # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). # latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). # latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ( "index", "django-cookie-consent.tex", "django-cookie-consent Documentation", "Bojan Mihelac", "manual", ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Additional stuff for the LaTeX preamble. # latex_preamble = '' # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ( "index", "django-cookie-consent", "django-cookie-consent Documentation", ["Bojan Mihelac"], 1, ) ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( "index", "django-cookie-consent", "django-cookie-consent Documentation", "Bojan Mihelac", "django-cookie-consent", "", "Miscellaneous", ), ] # Documents to append as an appendix to all manuals. texinfo_appendices = [] linkcheck_ignore = [ r"https://(www\.)?npmjs\.com.*", # IP/UA blocking... ] ================================================ FILE: docs/contributing.rst ================================================ .. _contributing: ============= Contributing ============= .. include:: ../CONTRIBUTING.rst ================================================ FILE: docs/example_app.rst ================================================ =========== Example app =========== The ``testapp`` project is both an example of how you could use this library and serves as the reference for our test suite. Running the testapp ------------------- The testapp is essentially a standard django project, however there is no ``manage.py`` file. Instead, you have to use the ``django-admin`` command (``manage.py`` is only a wrapper around this anyway). #. First, clone the repository to get all the necessary files: .. code-block:: bash git clone https://github.com/django-commons/django-cookie-consent.git cd django-cookie-consent #. Create a virtual environment for the project, using any supported Python version (3.10+) and activate it .. code-block:: bash python3.12 -m venv ./env source ./env/bin/activate #. Install the application and dependencies .. code-block:: bash pip install . #. Prepare your settings and local project instance .. code-block:: bash export DJANGO_SETTINGS_MODULE=testapp.settings PYTHONPATH=. django-admin migrate django-admin loaddata testapp/fixture.json django-admin createsuperuser #. Start the development server .. code-block:: bash django-admin runserver You can now navigate to ``http://127.0.0.1:8000`` and ``http://127.0.0.1:8000/admin/``. ================================================ FILE: docs/index.rst ================================================ ===================== Django cookie consent ===================== Manage cookie information and let visitors give or reject consent for them. |build-status| |code-quality| |ruff| |coverage| |docs| |python-versions| |django-versions| |pypi-version| Features ======== * cookies and cookie groups are stored in models for easy management through Django admin interface * support for both opt-in and opt-out cookie consent schemes * removing declined cookies (or non accepted when opt-in scheme is used) * logging user actions when they accept and decline various cookies * easy adding new cookies and seamlessly re-asking for consent for new cookies You can find the source code and development progress on https://github.com/django-commons/django-cookie-consent/. User Guide ---------- .. toctree:: :maxdepth: 2 quickstart concept usage javascript settings example_app reference/index contributing migrating-1.0 changelog Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` .. |build-status| image:: https://github.com/django-commons/django-cookie-consent/workflows/Run%20CI/badge.svg :alt: Build status :target: https://github.com/django-commons/django-cookie-consent/actions?query=workflow%3A%22Run+CI%22 .. |code-quality| image:: https://github.com/django-commons/django-cookie-consent/workflows/Code%20quality%20checks/badge.svg :alt: Code quality checks :target: https://github.com/django-commons/django-cookie-consent/actions?query=workflow%3A%22Code+quality+checks%22 .. |ruff| image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json :target: https://github.com/astral-sh/ruff .. |coverage| image:: https://codecov.io/gh/django-commons/django-cookie-consent/branch/main/graph/badge.svg :target: https://codecov.io/gh/django-commons/django-cookie-consent :alt: Coverage status .. |docs| image:: https://readthedocs.org/projects/django-cookie-consent/badge/?version=latest :target: https://django-cookie-consent.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status .. |python-versions| image:: https://img.shields.io/pypi/pyversions/django-cookie-consent.svg .. |django-versions| image:: https://img.shields.io/pypi/djversions/django-cookie-consent.svg .. |pypi-version| image:: https://img.shields.io/pypi/v/django-cookie-consent.svg :target: https://pypi.org/project/django-cookie-consent/ ================================================ FILE: docs/javascript.rst ================================================ .. _javascript: ====================== Javascript integration ====================== Cookie consent supports "classic" pages where you submit the accept/decline form and then it performs a full page reload. This strategy is simple and straight-forward, as any dynamic scripts tied to the cookie groups are then automatically initialized. However, this does not lead to the best user-experience. Consider a user filling out a long form and half-way they decide to get rid of the "annoying cookie bar". Either accepting or declining will make them lose their changes, providing a frustrating experience. Using the scripts we ship, you can provide a better user experience, at the cost of more development work. .. _showcookiebar_getting_started: Getting started =============== Requirements ------------ The new script is designed for modern Javascript and modern browsers. It should also integrate with JS tooling like Webpack/Rollup/ESBuild... but we are not actively testing this. Please let us know via Github issues what your issues and/or wishes are! As such, the target browsers must support: * `` tag #} **Include a script that calls the showCookieBar function** The most straight-forward way is to include this in your Django template: .. code-block:: django {% load static cookie_consent_tags %} {% static "cookie_consent/cookiebar.module.js" as cookiebar_src %} {% url 'cookie_consent_status' as status_url %} You call the function with the necessary options, and on page-load the cookie bar will be properly initialized. The ``status_url`` is special - it points to a backend view which returns the user-specific cookie consent status, returning the appropriate accept and decline URLs and other details relevant to cookie consent. .. note:: If you prefer the include the cookiebar module in your own Javascript entrypoint, the easiest way is to install our `published package`_. This package should work with TypeScript, Webpack, ESBuild, Vite... and other popular bundlers and toolchains. Just be careful to install the same (minor) version as the backend package to avoid weird bugs. .. _published package: hhttps://www.npmjs.com/package/django-cookie-consent Options ======= The ``showCookieBar`` function takes a few required options and many optional options to tweak the behaviour to your wishes. **Required options** * ``statusUrl``: URL to the ``CookieStatusView`` - essential to determine the accept/decline URLs and CSRF token. Use ``{% url 'cookie_consent_status' as status_url %}`` for the correct value, irrespective of your urlconf. **Recommended options** These options have default values, but to prevent surprises and maximum flexibility, you should provide them. Please check the source code for their default values. * ``templateSelector`` - CSS selector to find the template element of the cookie bar. This element will be cloned and ultimately added to the page. * ``cookieGroupsSelector`` - CSS selector to the element produced by ``{% all_cookie_groups 'cookie-consent__cookie-groups' %}``. This provides all configured cookie groups in a JSON script tag and is read by ``showCookieBar`` to determine if a bar should be shown at all (e.g. if there are no cookie groups, nothing is done). * ``acceptSelector`` - CSS selector to the element to accept all cookies. A ``click`` event listener is bound to this element to register the cookies accept action. * ``declineSelector`` - CSS selector to the element to decline all cookies. A ``click`` event listener is bound to this element to register the cookies decline action. **Optional** * ``insertBefore`` - A CSS selector, DOM node or ``null``. If provided, the cookie bar is prepended before this node, otherwise it is appended to the body element. * ``onShow`` - an optional callback function, called right before the cookie bar is added to the document. * ``onAccept`` - an optional callback, called when the "cookies accept" element is clicked and when the cookie status is initially loaded. It receives the list of all cookie groups that are (now) accepted and the click event (if there was one). * ``onDecline`` - an optional callback, called when the "cookies decline" element is clicked and when the cookie status is initially loaded. It receives the list of all cookie groups that are (now) declined and the click event (if there was one). * ``csrfHeaderName`` - HTTP header name for the CSRF Token. Defaults to Django's default value, so if you have a non-default ``settings.CSRF_HEADER_NAME``, you must provide this. .. _javascript_enable_scripts: Enabling other scripts after cookies were accepted ================================================== The legacy version of ``showCookieBar`` supported emitting scripts with a custom type in the Django templates, which where then changed to ``type="text/javascript"`` to make them execute without a full page reload. The new version does not support this out of the box, as it may interfere with page caches, Content Security Policies and was poorly documented. We recommend hooking into the ``onAccept`` and ``onDecline`` hooks to perform these actions. E.g. in the django template: .. code-block:: django and the Javascript function: .. code-block:: javascript function onAccept(cookieGroups) { const analyticsEnabled = cookieGroups.find(group => group.varname === 'analytics') != undefined; if (analyticsEnabled) { const template = document.getElementById('analytics-scripts').content; const analyticsScripts = templateNode.content.cloneNode(true); document.body.appendChild(analyticsScripts); } } Passing this ``onAccept`` callback then adds the scripts after the user accepted the cookies, causing them to execute. This way, there's no reliance on ``unsafe-eval``. .. _javascript_design_considerations: Considerations and design decisions made for the JS integration =============================================================== We realize there is quite a bit of work to do to use this functionality. We've aimed for a trade-off where the simple things are easy to do and the complex set-ups are achievable. The :ref:`showcookiebar_getting_started` section should be close to plug-and-play by integrating well with Django's static files. Especially on modern browsers, we intend to have a working solution without intricate Javascript knowledge. For more advanced Javascript usage/developers, we expose hooks and options to tap into the life-cycle. The code may also serve as a reference for your own implementation. HttpOnly and CSRF ----------------- The cookie-consent cookie itself can safely be set to ``HttpOnly`` so it cannot be tampered with (or even read) from Javascript. This follows security best practices. The new script no longer touches ``document.cookie``. Accepting and declining cookies must be CSRF-protected and use ``POST`` requests. This works out of the box with the async calls we make - the status endpoint provides the CSRF token to the Javascript so that it can include this via an HTTP header. This means that you can mark your CSRF cookies ``HttpOnly`` in Django. Content Security Policy (CSP) ----------------------------- Content Security Policies aim to lock down which scripts, styles... can run in the browser. They are a good tool in helping prevent Cross-Site-Scripting attacks, by specifying from which sources scripts are allowed to run and usually by blocking ``eval`` (which should be the bare minimum of what you block). The new scripts play well with this - you can include your analytics scripts inside ``