diff --git a/CHANGELOG.md b/CHANGELOG.md index 5565e82b..b2bb0b3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## 2.0.0 (2024-05-22) + +- Fixes for old style storages removed in Django 5.1 + +## 2.0.0-alpha.2 (2024-04-19) +## 2.0.0-alpha.1 (2024-04-17) + +### WARNING + +**Breaking change:** Starting with version the 2.0.0 the category slugs are unique. +While this brings big advantage for simplified category addressing, it can break projects that are containing categories with duplicated slugs. +If your database contains colliding slugs, they will be automatically renamed by the migration. +Three categories with slugs ``foo`` will be renamed to ``foo``, ``foo-1``, ``foo-2``. +If this causes problems in your project, you can rename the categories yourself before running the migration. + +- Django 2.1 is no longer supported + + ## 1.9.4 (2024-04-18) - Remove dependency on unicode-slugify, use Django builtin function diff --git a/categories/__init__.py b/categories/__init__.py index 35dee15d..72924630 100644 --- a/categories/__init__.py +++ b/categories/__init__.py @@ -1,6 +1,6 @@ """Django categories.""" -__version__ = "1.9.4" +__version__ = "2.0.0-alpha.2" default_app_config = "categories.apps.CategoriesConfig" diff --git a/categories/base.py b/categories/base.py index f1ed01d6..74c3fdec 100644 --- a/categories/base.py +++ b/categories/base.py @@ -46,7 +46,7 @@ class CategoryBase(MPTTModel): verbose_name=_("parent"), ) name = models.CharField(max_length=100, verbose_name=_("name")) - slug = models.SlugField(verbose_name=_("slug")) + slug = models.SlugField(verbose_name=_("slug"), unique=True) active = models.BooleanField(default=True, verbose_name=_("active")) objects = CategoryManager() diff --git a/categories/fields.py b/categories/fields.py index 1f44d1ff..25f77d23 100644 --- a/categories/fields.py +++ b/categories/fields.py @@ -7,19 +7,15 @@ class CategoryM2MField(ManyToManyField): """A many to many field to a Category model.""" def __init__(self, **kwargs): - from .models import Category - if "to" in kwargs: kwargs.pop("to") - super(CategoryM2MField, self).__init__(to=Category, **kwargs) + super(CategoryM2MField, self).__init__(to="categories.Category", **kwargs) class CategoryFKField(ForeignKey): """A foreign key to the Category model.""" def __init__(self, **kwargs): - from .models import Category - if "to" in kwargs: kwargs.pop("to") - super(CategoryFKField, self).__init__(to=Category, **kwargs) + super(CategoryFKField, self).__init__(to="categories.Category", **kwargs) diff --git a/categories/fixtures/musicgenres.json b/categories/fixtures/musicgenres.json index 38175100..1cf4e4ff 100644 --- a/categories/fixtures/musicgenres.json +++ b/categories/fixtures/musicgenres.json @@ -2292,7 +2292,7 @@ "name": "Country pop", "parent": 142, "level": 1, - "slug": "country-pop", + "slug": "country-pop_1", "lft": 100, "tree_id": 10, "order": 1 diff --git a/categories/migrations/0005_unique_category_slug.py b/categories/migrations/0005_unique_category_slug.py new file mode 100644 index 00000000..1be7e62e --- /dev/null +++ b/categories/migrations/0005_unique_category_slug.py @@ -0,0 +1,36 @@ +# Generated by Django 2.0.9 on 2018-10-05 13:59 + +from django.db import migrations, models + +from categories.models import Category + + +def make_slugs_unique(apps, schema_editor): + duplicates = Category.tree.values("slug").annotate(slug_count=models.Count("slug")).filter(slug_count__gt=1) + category_objs = [] + for duplicate in duplicates: + slug = duplicate["slug"] + categories = Category.tree.filter(slug=slug) + count = categories.count() + i = 0 + for category in categories.all(): + if i != 0: + category.slug = "{}-{}".format(slug, str(i).zfill(len(str(count)))) + category_objs.append(category) + i += 1 + Category.objects.bulk_update(category_objs, ["slug"]) + + +class Migration(migrations.Migration): + dependencies = [ + ("categories", "0004_auto_20200517_1832"), + ] + + operations = [ + migrations.RunPython(make_slugs_unique, reverse_code=migrations.RunPython.noop), + migrations.AlterField( + model_name="category", + name="slug", + field=models.SlugField(unique=True, verbose_name="slug"), + ), + ] diff --git a/categories/tests/test_category_import.py b/categories/tests/test_category_import.py index acd366f5..3c2a5b5f 100644 --- a/categories/tests/test_category_import.py +++ b/categories/tests/test_category_import.py @@ -58,7 +58,7 @@ def testMixingTabsSpaces(self): Should raise an exception. """ string1 = ["cat1", " cat1-1", "\tcat1-2-FAIL!", ""] - string2 = ["cat1", "\tcat1-1", " cat1-2-FAIL!", ""] + string2 = ["cat1a", "\tcat1-1a", " cat1-2-FAIL!", ""] cmd = Command() # raise Exception diff --git a/categories/tests/test_migrations.py b/categories/tests/test_migrations.py new file mode 100644 index 00000000..211955da --- /dev/null +++ b/categories/tests/test_migrations.py @@ -0,0 +1,42 @@ +import sys + +if sys.version_info >= (3, 0): + from django_test_migrations.contrib.unittest_case import MigratorTestCase + + class TestMigrations(MigratorTestCase): + migrate_from = ("categories", "0004_auto_20200517_1832") + migrate_to = ("categories", "0005_unique_category_slug") + + def prepare(self): + Category = self.old_state.apps.get_model("categories", "Category") + Category.tree.create(slug="foo", lft=0, rght=0, tree_id=0, level=0) + Category.tree.create(slug="foo", lft=0, rght=0, tree_id=0, level=0) + Category.tree.create(slug="foo", lft=0, rght=0, tree_id=0, level=0) + for i in range(1, 12): + Category.tree.create(slug="bar", lft=0, rght=0, tree_id=0, level=0) + Category.tree.create(slug="baz", lft=0, rght=0, tree_id=0, level=0) + assert Category.tree.count() == 15 + + def test_unique_slug_migration(self): + Category = self.new_state.apps.get_model("categories", "Category") + + self.assertListEqual( + list(Category.tree.values_list("slug", flat=True)), + [ + "foo", + "foo-1", + "foo-2", + "bar", + "bar-01", + "bar-02", + "bar-03", + "bar-04", + "bar-05", + "bar-06", + "bar-07", + "bar-08", + "bar-09", + "bar-10", + "baz", + ], + ) diff --git a/example/settings-testing.py b/example/settings-testing.py index 2af07026..ecfb5a05 100644 --- a/example/settings-testing.py +++ b/example/settings-testing.py @@ -123,7 +123,7 @@ ), }, "M2M_REGISTRY": { - # 'simpletext.simpletext': {'name': 'categories', 'related_name': 'm2mcats'}, + "simpletext.simpletext": {"name": "categories", "related_name": "m2mcats"}, "flatpages.flatpage": ( {"name": "other_categories", "related_name": "other_cats"}, {"name": "more_categories", "related_name": "more_cats"}, diff --git a/tox.ini b/tox.ini index f3440727..85815df1 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,6 @@ envlist = begin py37-lint - py{37}-django{21} py{37,38,39}-django{22,3,31} py{37,38,39,310}-django{32} py{38,39,310}-django{40} @@ -22,8 +21,6 @@ python = passenv = GITHUB_* deps= - django2: Django>=2.0,<2.1 - django21: Django>=2.1,<2.2 django22: Django>=2.2,<2.3 django3: Django>=3.0,<3.1 django31: Django>=3.1,<3.2 @@ -37,6 +34,11 @@ deps= pillow ipdb codecov + django-test-migrations + django21: django-test-migrations<=1.2.0 + django22: django-test-migrations<=1.2.0 + django3: django-test-migrations<=1.2.0 + django31: django-test-migrations<=1.2.0 -r{toxinidir}/requirements.txt commands=