From 13aa8c36ca58a4a445bac2ebc27fea6bf538e0bb Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Thu, 9 May 2024 22:26:58 +0200 Subject: [PATCH] :sparkles: Fixes #103 -- support natural keys for the models Added support for natural keys for the cookie and cookie group models. This required adding another unique constraint, this time to the cookie model. A specific cookie within a group was already looked up by name and domain in the existing code, so this DB constraint confirms the usage. Duplicate data would not have yielded the expected results, but users may not be aware of it, so this is also a breaking change. You can now dump and load fixtures for cookie consent, using these natural keys and avoid database primary key conflicts. --- .../migrations/0004_cookie_natural_key.py | 19 ++++++++++++ cookie_consent/models.py | 29 +++++++++++++++++-- docs/changelog.rst | 5 ++-- tests/test_cookie_group_model.py | 19 ++++++++++++ tests/test_cookie_model.py | 24 +++++++++++++++ tests/test_models.py | 1 - 6 files changed, 92 insertions(+), 5 deletions(-) create mode 100644 cookie_consent/migrations/0004_cookie_natural_key.py create mode 100644 tests/test_cookie_group_model.py create mode 100644 tests/test_cookie_model.py diff --git a/cookie_consent/migrations/0004_cookie_natural_key.py b/cookie_consent/migrations/0004_cookie_natural_key.py new file mode 100644 index 0000000..6dfa54a --- /dev/null +++ b/cookie_consent/migrations/0004_cookie_natural_key.py @@ -0,0 +1,19 @@ +# 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" + ), + ), + ] diff --git a/cookie_consent/models.py b/cookie_consent/models.py index 7fe0d85..c9a66ba 100644 --- a/cookie_consent/models.py +++ b/cookie_consent/models.py @@ -48,6 +48,11 @@ def update(self, **kwargs): return super().update(**kwargs) +class CookieGroupManager(models.Manager.from_queryset(BaseQueryset)): + def get_by_natural_key(self, varname): + return self.get(varname=varname) + + class CookieGroup(models.Model): varname = models.CharField( _("Variable name"), @@ -70,7 +75,7 @@ class CookieGroup(models.Model): ordering = models.IntegerField(_("Ordering"), default=0) created = models.DateTimeField(_("Created"), auto_now_add=True, blank=True) - objects = BaseQueryset.as_manager() + objects = CookieGroupManager() class Meta: verbose_name = _("Cookie Group") @@ -88,6 +93,9 @@ def save(self, *args, **kwargs): def delete(self, *args, **kwargs): return super().delete(*args, **kwargs) + def natural_key(self): + return (self.varname,) + def get_version(self) -> str: try: return str(self.cookie_set.all()[0].get_version()) @@ -104,6 +112,12 @@ def for_json(self) -> CookieGroupDict: } +class CookieManager(models.Manager.from_queryset(BaseQueryset)): + def get_by_natural_key(self, name, domain, cookiegroup): + 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, @@ -116,11 +130,17 @@ class Cookie(models.Model): domain = models.CharField(_("Domain"), max_length=250, blank=True) created = models.DateTimeField(_("Created"), auto_now_add=True, blank=True) - objects = BaseQueryset.as_manager() + 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): @@ -134,6 +154,11 @@ def save(self, *args, **kwargs): def delete(self, *args, **kwargs): return super().delete(*args, **kwargs) + def natural_key(self): + return (self.name, self.domain) + self.cookiegroup.natural_key() + + natural_key.dependencies = ["cookie_consent.cookiegroup"] + @property def varname(self): return "%s=%s:%s" % (self.cookiegroup.varname, self.name, self.domain) diff --git a/docs/changelog.rst b/docs/changelog.rst index ad28241..a5dd5f9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,8 +6,9 @@ Changelog ------------------ 💥 This feature release has a potential breaking change. The ``CookieGroup.varname`` -field now has a unique constraint on it. If you have duplicate values, this migration -will crash. +field now has a unique constraint on it. The ``Cookie`` model now has a unique +constraint on ``cookiegroup``, ``name`` and ``domain``. If you have duplicate values, +this migration will crash. * ... diff --git a/tests/test_cookie_group_model.py b/tests/test_cookie_group_model.py new file mode 100644 index 0000000..0a979bc --- /dev/null +++ b/tests/test_cookie_group_model.py @@ -0,0 +1,19 @@ +import pytest + +from cookie_consent.models import CookieGroup + + +def test_natural_key(): + group = CookieGroup(varname="social") + + assert group.natural_key() == ("social",) + + +@pytest.mark.django_db +def test_load_by_natural_key(): + social_group = CookieGroup.objects.create(varname="social") + CookieGroup.objects.create(varname="other") + + loaded_group = CookieGroup.objects.get_by_natural_key("social") + + assert loaded_group == social_group diff --git a/tests/test_cookie_model.py b/tests/test_cookie_model.py new file mode 100644 index 0000000..58111a5 --- /dev/null +++ b/tests/test_cookie_model.py @@ -0,0 +1,24 @@ +import pytest + +from cookie_consent.models import Cookie, CookieGroup + + +def test_natural_key(): + cookie = Cookie( + cookiegroup=CookieGroup(varname="analytics"), name="trck", domain="example.com" + ) + + assert cookie.natural_key() == ("trck", "example.com", "analytics") + + +@pytest.mark.django_db +def test_load_by_natural_key(): + social_group = CookieGroup.objects.create(varname="social") + cookie = Cookie.objects.create( + cookiegroup=social_group, name="trck", domain="example.com" + ) + Cookie.objects.create(cookiegroup=social_group, name="other", domain="example.com") + + loaded_cookie = Cookie.objects.get_by_natural_key("trck", "example.com", "social") + + assert loaded_cookie == cookie diff --git a/tests/test_models.py b/tests/test_models.py index 96afc79..5e51c1d 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- from copy import deepcopy -from unittest import mock from django.conf import settings from django.core.cache import caches