Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

make Category.slug unique #149

Merged
merged 10 commits into from
May 22, 2024
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion categories/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Django categories."""

__version__ = "1.9.4"
__version__ = "2.0.0-alpha.2"


default_app_config = "categories.apps.CategoriesConfig"
2 changes: 1 addition & 1 deletion categories/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
8 changes: 2 additions & 6 deletions categories/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
2 changes: 1 addition & 1 deletion categories/fixtures/musicgenres.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 36 additions & 0 deletions categories/migrations/0005_unique_category_slug.py
Original file line number Diff line number Diff line change
@@ -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"),
),
]
2 changes: 1 addition & 1 deletion categories/tests/test_category_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
42 changes: 42 additions & 0 deletions categories/tests/test_migrations.py
Original file line number Diff line number Diff line change
@@ -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",
],
)
2 changes: 1 addition & 1 deletion example/settings-testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down
8 changes: 5 additions & 3 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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
Expand All @@ -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=
Expand Down
Loading