A GDPR-compliant cookie consent banner plugin for Django CMS
Project description
djangocms-cookie-love
A GDPR-compliant cookie consent management plugin for Django CMS with granular control, versioning, and a modern Bootstrap 5 design.
Features
- GDPR/TTDSG-compliant – Opt-in by default, no pre-selected optional cookies
- Granular consent – Per cookie group and per individual cookie control
- Versioning – Track policy changes, force re-consent when the version changes
- Consent audit trail – Full documentation of every consent decision (timestamp, IP hash, version, method)
- Admin interface – Configure banner, cookie groups, and individual cookies through Django Admin
- CMS Plugin + Template Tags – Flexible integration: drag & drop plugin or
{% cookie_love_banner %} - Script blocking – Conditionally load
<script>tags based on consent viadata-cookie-group - Cookie-level script blocking – Block scripts per individual cookie via
data-cookie-slug - Bootstrap 5 design – Responsive, mobile-first, easily themeable with CSS custom properties
- Accessible – ARIA attributes, keyboard navigation, focus trap in settings modal
- Vanilla JS – No jQuery or other dependencies, ~700 lines
- i18n – Ships with English and German translations
- Pre-commit hooks – Ruff linting and formatting enforced on every commit
Architecture
┌─────────────────────────────────────────────────────┐
│ Browser │
│ ┌──────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Banner │ │ Settings │ │ cookie-love │ │
│ │ (HTML) │→ │ Modal (HTML) │→ │ .js │ │
│ └──────────┘ └──────────────┘ └──────┬───────┘ │
│ │ XHR │
├─────────────────────────────────────────┼───────────┤
│ Server ▼ │
│ ┌──────────────────────────────────────────────┐ │
│ │ API Views │ │
│ │ GET /cookie-love/api/config/ │ │
│ │ GET /cookie-love/api/consent/ │ │
│ │ POST /cookie-love/api/consent/ │ │
│ │ POST /cookie-love/api/consent/revoke/ │ │
│ └──────────────────┬───────────────────────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────────────┐ │
│ │ Models │ │
│ │ CookieConsentConfig (singleton) │ │
│ │ CookieGroup → Cookie (individual items) │ │
│ │ ConsentVersion (policy snapshots) │ │
│ │ UserConsent (audit trail) │ │
│ └──────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
Data Models
| Model | Purpose |
|---|---|
CookieConsentConfig |
Singleton – banner text, button labels, links, position |
CookieGroup |
Category of cookies (e.g. Essential, Analytics, Marketing) |
Cookie |
Individual cookie within a group (name, provider, duration, purpose) |
ConsentVersion |
Snapshot of the current policy; triggers re-consent on change |
UserConsent |
Audit record: accepted groups/cookies, timestamp, IP hash, method |
Requirements
- Python ≥ 3.10
- Django ≥ 4.2
- django-cms ≥ 4.0
Installation
pip install djangocms-cookie-love
Add to your INSTALLED_APPS:
INSTALLED_APPS = [
...
"djangocms_cookie_love",
...
]
Add the middleware (after SessionMiddleware):
MIDDLEWARE = [
...
"djangocms_cookie_love.middleware.CookieConsentMiddleware",
...
]
Add the context processor:
TEMPLATES = [
{
...
"OPTIONS": {
"context_processors": [
...
"djangocms_cookie_love.context_processors.cookie_consent",
],
},
},
]
Include the URLs:
urlpatterns = [
...
path("cookie-love/", include("djangocms_cookie_love.urls")),
...
]
Run migrations:
python manage.py migrate
This creates default cookie groups (Essential, Analytics, Marketing, Preferences).
Quick Start
Option 1: Template Tags (recommended)
{% load cookie_love_tags %}
<!DOCTYPE html>
<html>
<head>
{% cookie_love_css %}
</head>
<body>
... {% cookie_love_banner %} {% cookie_love_js %}
</body>
</html>
Option 2: Django CMS Plugin
Add the Cookie Consent Banner plugin to any CMS placeholder. The banner renders automatically with all configuration from the admin.
Note: Use either the template tag or the plugin, not both – otherwise the banner appears twice.
Configuration
# settings.py
# Required
COOKIE_LOVE_IP_SALT = "your-secret-salt-here" # Salt for IP address hashing
# Optional (shown with defaults)
COOKIE_LOVE_COOKIE_NAME = "cookie_love_consent" # Browser cookie name
COOKIE_LOVE_COOKIE_DURATION = 365 # Days until consent expires
COOKIE_LOVE_COOKIE_SECURE = True # Set to False for local dev (HTTP)
COOKIE_LOVE_COOKIE_SAMESITE = "Lax" # SameSite policy
COOKIE_LOVE_COOKIE_HTTPONLY = True # HttpOnly flag
COOKIE_LOVE_CONSENT_RETENTION_DAYS = 1095 # Days to keep consent records (default: 3 years)
Script Blocking
By cookie group
Scripts with data-cookie-group are only executed after the user consents to that group:
<script type="text/plain" data-cookie-group="analytics">
// Runs only after user consents to "analytics"
</script>
<script
type="text/plain"
data-cookie-group="analytics"
data-src="https://www.googletagmanager.com/gtag/js?id=G-XXX"
>
// External script – loaded only after consent
</script>
By individual cookie
For finer control, block scripts per individual cookie:
<script type="text/plain" data-cookie-group="analytics" data-cookie-slug="ga">
// Runs only if the user consented to the "ga" cookie in the "analytics" group
</script>
JavaScript API
// Open/close settings modal
CookieLove.openSettings();
CookieLove.closeSettings();
// Programmatic consent
CookieLove.acceptAll();
CookieLove.rejectAll();
CookieLove.saveSettings();
// Query consent state
CookieLove.getConsent(); // { acceptedGroups, acceptedCookies }
CookieLove.hasConsent("analytics"); // true/false
CookieLove.hasCookieConsent("analytics", "ga"); // true/false
// React to consent changes
CookieLove.onConsent(function (groups, cookies) {
console.log("Accepted groups:", groups);
console.log("Accepted cookies:", cookies);
});
// Revoke consent
CookieLove.revokeConsent();
Events
document.addEventListener("cookie-love:consent", function (e) {
console.log(e.detail.acceptedGroups);
console.log(e.detail.acceptedCookies);
});
document.addEventListener("cookie-love:revoke", function (e) {
console.log("Consent revoked");
});
Theming
The banner and settings modal are styled with CSS custom properties. There are three levels of customisation, from a quick colour swap to a fully custom layout.
Level 1 – CSS Custom Properties (recommended)
Add overrides anywhere in your CSS — no template changes needed:
:root {
--cl-primary: #e11d48; /* Brand colour (buttons, titles, links) */
--cl-primary-hover: #be123c; /* Hover state of the primary colour */
--cl-primary-light: #fff1f2; /* Light tint (modal header background) */
--cl-primary-subtle: #ffe4e6; /* Subtle tint (badge background) */
--cl-bg: #ffffff; /* Banner / modal background */
--cl-text: #1e1b2e; /* Primary text colour */
--cl-text-muted: #6b7280; /* Secondary / description text */
--cl-border: #e5e7eb; /* Divider and border colour */
--cl-shadow: 0 -4px 32px rgba(225, 29, 72, 0.08); /* Banner shadow */
--cl-border-radius: 1rem; /* Corner radius of banner and modal */
--cl-border-radius-sm: 0.625rem;/* Corner radius of buttons */
--cl-max-width: 720px; /* Maximum width of banner / modal */
--cl-font: system-ui, sans-serif; /* Font stack */
--cl-z-index: 9999; /* Banner z-index */
--cl-modal-z-index: 10000; /* Settings modal z-index */
}
Example: dark mode
:root {
--cl-bg: #1e1e2e;
--cl-text: #cdd6f4;
--cl-text-muted: #a6adc8;
--cl-border: #313244;
--cl-primary: #cba6f7;
--cl-primary-hover: #b4befe;
--cl-primary-light: #1e1e2e;
--cl-primary-subtle: #313244;
}
Example: square, full-width corporate style
:root {
--cl-border-radius: 0;
--cl-border-radius-sm: 0;
--cl-max-width: 100%;
--cl-primary: #003366;
--cl-primary-hover: #002244;
--cl-primary-light: #e6edf5;
--cl-primary-subtle: #ccdaeb;
}
Level 2 – Banner position
Set the position directly in the Django admin (bottom, top, or center) — no code changes needed.
Level 3 – Template override
Copy the templates you want to customise into your own templates/ directory and edit them freely:
your_project/
└── templates/
└── djangocms_cookie_love/
├── banner.html # Main banner
├── settings_modal.html # Settings modal wrapper
└── includes/
├── cookie_group.html # Individual group row with toggle
└── cookie_item.html # Individual cookie row with checkbox
Django's template loader will pick up your versions automatically — no settings change required.
Admin Interface
- Cookie Consent Config – Configure banner title, description, button labels, links, position. An initial consent version (
1.0) is created automatically when you save a new config. - Cookie Groups – Add/edit cookie categories with inline cookie management
- Consent Versions – Publish new versions to trigger re-consent. Create a new version with
requires_reconsent=Truewhenever you make a significant policy change — all users will be shown the banner again. - User Consents – Read-only audit log with CSV export
Middleware
The CookieConsentMiddleware sets these attributes on every request:
| Attribute | Type | Description |
|---|---|---|
request.cookie_consent |
UserConsent or None |
The user's consent record |
request.cookie_consent_required |
bool |
Whether the banner should be shown |
request.cookie_consent_groups |
list[str] |
Accepted group slugs |
request.cookie_consent_cookies |
list[str] |
Accepted cookie refs (group:cookie) |
Use in templates:
{% if request.cookie_consent_required %}
<!-- Banner will show automatically via JS -->
{% endif %} {% if "analytics" in request.cookie_consent_groups %}
<!-- Server-side conditional rendering -->
{% endif %}
Development
git clone https://github.com/noelpmax/djangocms-cookie-love.git
cd djangocms-cookie-love
python -m venv .venv
source .venv/bin/activate # Linux/macOS
# .venv\Scripts\activate # Windows
pip install -e ".[dev]"
pre-commit install
pytest
Example Project
cd example
./setup.sh
python manage.py runserver
Visit http://localhost:8000 to see the banner in action.
Code Quality
Pre-commit hooks enforce on every commit:
- ruff check – Linting (pycodestyle, pyflakes, isort, bugbear, pyupgrade, flake8-django)
- ruff format – Code formatting
- trailing-whitespace / end-of-file-fixer – File hygiene
GDPR Compliance
This package implements the following GDPR/TTDSG requirements:
| Requirement | Implementation |
|---|---|
| Opt-in by default | No optional cookies pre-selected |
| Granular control | Per group and per individual cookie |
| Informed consent | Cookie name, provider, duration, purpose displayed |
| Revocable consent | CookieLove.openSettings() / CookieLove.revokeConsent() |
| Documented consent | UserConsent model with timestamp, IP hash, version, method |
| IP anonymization | SHA-256 hash with configurable salt |
| Version tracking | ConsentVersion with automatic re-consent |
| Essential cookies without consent | is_required groups are always active |
| Immutable audit trail | Admin blocks add/change/delete on UserConsent |
| Storage limitation (Art. 5(1)(e)) | purge_old_consents management command (default: 3 years) |
| User-Agent storage | Stored pseudonymously for audit purposes; mention in your privacy policy |
Data Retention
Run the purge_old_consents command periodically (e.g. via cron or Celery beat) to delete
consent records older than the configured retention period:
# Delete records older than 3 years (default)
python manage.py purge_old_consents
# Preview without deleting
python manage.py purge_old_consents --dry-run
# Custom retention period
python manage.py purge_old_consents --days=730
Privacy Policy Note: The
UserConsentmodel stores the browser's User-Agent string alongside the hashed IP address for audit trail purposes. No user account or raw IP is ever stored. Mention this in your privacy policy.
Further Reading
- TESTING.md – Test coverage report (116 tests, 93% coverage)
- idea/ – Planned features and ideas
- Plain Django support – Making Django CMS optional
- CHANGELOG.md – Version history
License
MIT License – see LICENSE for details.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters