(models) add slug field to Team model

Add slug field for team objects. Unique slug based on team names,
in an effort to avoid duplicates.
This commit is contained in:
Marie PUPO JEAMMET
2024-02-01 18:15:25 +01:00
committed by Marie
parent c117f67952
commit d2bf44d2fd
10 changed files with 153 additions and 29 deletions

View File

@@ -2,6 +2,8 @@
People is an application to handle users and teams.
This project is as of yet **not ready for production**. Expect breaking changes.
People is built on top of [Django Rest
Framework](https://www.django-rest-framework.org/).

View File

@@ -108,4 +108,5 @@ class TeamAdmin(admin.ModelAdmin):
"created_at",
"updated_at",
)
prepopulated_fields = {"slug": ("name",)}
search_fields = ("name",)

View File

@@ -131,11 +131,12 @@ class TeamSerializer(serializers.ModelSerializer):
abilities = serializers.SerializerMethodField(read_only=True)
accesses = TeamAccessSerializer(many=True, read_only=True)
slug = serializers.SerializerMethodField()
class Meta:
model = models.Team
fields = ["id", "name", "accesses", "abilities"]
read_only_fields = ["id", "accesses", "abilities"]
fields = ["id", "name", "accesses", "abilities", "slug"]
read_only_fields = ["id", "accesses", "abilities", "slug"]
def get_abilities(self, team) -> dict:
"""Return abilities of the logged-in user on the instance."""
@@ -143,3 +144,7 @@ class TeamSerializer(serializers.ModelSerializer):
if request:
return team.get_abilities(request.user)
return {}
def get_slug(self, instance):
"""Return slug from the team's name."""
return instance.get_slug()

View File

@@ -1,4 +1,4 @@
# Generated by Django 5.0 on 2024-01-14 13:41
# Generated by Django 5.0.1 on 2024-02-06 15:08
import core.models
import django.core.validators
@@ -27,6 +27,7 @@ class Migration(migrations.Migration):
('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created at')),
('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated at')),
('name', models.CharField(max_length=100)),
('slug', models.SlugField(max_length=100, unique=True)),
],
options={
'verbose_name': 'Team',

View File

@@ -11,6 +11,7 @@ from django.contrib.auth.base_user import AbstractBaseUser
from django.core import exceptions, mail, validators
from django.db import models
from django.utils.functional import lazy
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
import jsonschema
@@ -315,6 +316,7 @@ class Team(BaseModel):
"""
name = models.CharField(max_length=100)
slug = models.SlugField(max_length=100, unique=True, null=False)
users = models.ManyToManyField(
User,
@@ -332,6 +334,15 @@ class Team(BaseModel):
def __str__(self):
return self.name
def save(self, *args, **kwargs):
"""Overriding save function to compute the slug."""
self.slug = self.get_slug()
return super().save(*args, **kwargs)
def get_slug(self):
"""Compute slug value from name."""
return slugify(self.name)
def get_abilities(self, user):
"""
Compute and return abilities for a given user on the team.

View File

@@ -3,12 +3,10 @@ Tests for Teams API endpoint in People's core app: create
"""
import pytest
from rest_framework.test import APIClient
from rest_framework_simplejwt.tokens import AccessToken
from core.factories import IdentityFactory, TeamFactory, UserFactory
from core.factories import IdentityFactory, TeamFactory
from core.models import Team
from ..utils import OIDCToken
from core.tests.utils import OIDCToken
pytestmark = pytest.mark.django_db
@@ -48,3 +46,73 @@ def test_api_teams_create_authenticated():
team = Team.objects.get()
assert team.name == "my team"
assert team.accesses.filter(role="owner", user=user).exists()
def test_api_teams_create_authenticated_slugify_name():
"""
Creating teams should automatically generate a slug.
"""
identity = IdentityFactory()
jwt_token = OIDCToken.for_user(identity.user)
response = APIClient().post(
"/api/v1.0/teams/",
{"name": "my team"},
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
)
assert response.status_code == 201
team = Team.objects.get()
assert team.name == "my team"
assert team.slug == "my-team"
@pytest.mark.parametrize(
"param",
[
("my team", "my-team"),
("my team", "my-team"),
("MY TEAM TOO", "my-team-too"),
("mon équipe", "mon-equipe"),
("front devs & UX", "front-devs-ux"),
],
)
def test_api_teams_create_authenticated_expected_slug(param):
"""
Creating teams should automatically create unaccented, no unicode, lower-case slug.
"""
identity = IdentityFactory()
jwt_token = OIDCToken.for_user(identity.user)
response = APIClient().post(
"/api/v1.0/teams/",
{
"name": param[0],
},
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
)
assert response.status_code == 201
team = Team.objects.get()
assert team.name == param[0]
assert team.slug == param[1]
def test_api_teams_create_authenticated_unique_slugs():
"""
Creating teams should raise an error if already existing slug.
"""
TeamFactory(name="existing team")
identity = IdentityFactory()
jwt_token = OIDCToken.for_user(identity.user)
response = APIClient().post(
"/api/v1.0/teams/",
{
"name": "èxisting team",
},
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
)
assert response.status_code == 400
assert response.json()["slug"] == ["Team with this Slug already exists."]

View File

@@ -1,16 +1,11 @@
"""
Tests for Teams API endpoint in People's core app: retrieve
"""
import random
from collections import Counter
from unittest import mock
import pytest
from rest_framework.test import APIClient
from core import factories
from ..utils import OIDCToken
from core.tests.utils import OIDCToken
pytestmark = pytest.mark.django_db
@@ -82,5 +77,6 @@ def test_api_teams_retrieve_authenticated_related():
assert response.json() == {
"id": str(team.id),
"name": team.name,
"slug": team.slug,
"abilities": team.get_abilities(user),
}

View File

@@ -5,12 +5,10 @@ import random
import pytest
from rest_framework.test import APIClient
from rest_framework_simplejwt.tokens import AccessToken
from core import factories, models
from core import factories
from core.api import serializers
from ..utils import OIDCToken
from core.tests.utils import OIDCToken
pytestmark = pytest.mark.django_db
@@ -100,28 +98,31 @@ def test_api_teams_update_authenticated_administrators():
jwt_token = OIDCToken.for_user(user)
team = factories.TeamFactory(users=[(user, "administrator")])
old_team_values = serializers.TeamSerializer(instance=team).data
initial_values = serializers.TeamSerializer(instance=team).data
new_team_values = serializers.TeamSerializer(instance=factories.TeamFactory()).data
# generate new random values
new_values = serializers.TeamSerializer(instance=factories.TeamFactory.build()).data
response = APIClient().put(
f"/api/v1.0/teams/{team.id!s}/",
new_team_values,
new_values,
format="json",
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
)
assert response.status_code == 200
team.refresh_from_db()
team_values = serializers.TeamSerializer(instance=team).data
for key, value in team_values.items():
if key in ["id", "accesses"]:
assert value == old_team_values[key]
final_values = serializers.TeamSerializer(instance=team).data
for key, value in final_values.items():
if key in ["id", "accesses"]: # pylint: disable=R1733
assert value == initial_values[key]
else:
assert value == new_team_values[key]
# name, slug and abilities successfully modified
assert value == new_values[key]
def test_api_teams_update_authenticated_owners():
"""Administrators of a team should be allowed to update it."""
"""Administrators of a team should be allowed to update it,
apart from read-only fields."""
identity = factories.IdentityFactory()
user = identity.user
jwt_token = OIDCToken.for_user(user)
@@ -129,7 +130,9 @@ def test_api_teams_update_authenticated_owners():
team = factories.TeamFactory(users=[(user, "owner")])
old_team_values = serializers.TeamSerializer(instance=team).data
new_team_values = serializers.TeamSerializer(instance=factories.TeamFactory()).data
new_team_values = serializers.TeamSerializer(
instance=factories.TeamFactory.build()
).data
response = APIClient().put(
f"/api/v1.0/teams/{team.id!s}/",
new_team_values,
@@ -144,6 +147,7 @@ def test_api_teams_update_authenticated_owners():
if key in ["id", "accesses"]:
assert value == old_team_values[key]
else:
# name, slug and abilities successfully modified
assert value == new_team_values[key]
@@ -174,3 +178,31 @@ def test_api_teams_update_administrator_or_owner_of_another():
team.refresh_from_db()
team_values = serializers.TeamSerializer(instance=team).data
assert team_values == old_team_values
def test_api_teams_update_existing_slug_should_return_error():
"""
Updating a team's name to an existing slug should return a bad request,
instead of creating a duplicate.
"""
identity = factories.IdentityFactory()
user = identity.user
jwt_token = OIDCToken.for_user(user)
factories.TeamFactory(name="Existing team", users=[(user, "administrator")])
my_team = factories.TeamFactory(name="New team", users=[(user, "administrator")])
updated_values = serializers.TeamSerializer(instance=my_team).data
# Update my team's name for existing team. Creates a duplicate slug
updated_values["name"] = "existing team"
response = APIClient().put(
f"/api/v1.0/teams/{my_team.id!s}/",
updated_values,
format="json",
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
)
assert response.status_code == 400
assert response.json()["slug"] == ["Team with this Slug already exists."]
# Both teams names and slugs should be unchanged
assert my_team.name == "New team"
assert my_team.slug == "new-team"

View File

@@ -46,6 +46,12 @@ def test_models_teams_name_max_length():
factories.TeamFactory(name="a " * 51)
def test_models_teams_slug_empty():
"""Slug field should not be empty."""
with pytest.raises(ValidationError, match="This field cannot be blank."):
models.Team.objects.create(slug="")
# get_abilities

View File

@@ -38,7 +38,7 @@ class BulkQueue:
if not objects:
return
objects[0]._meta.model.objects.bulk_create(objects, ignore_conflicts=True) # noqa: SLF001
objects[0]._meta.model.objects.bulk_create(objects, ignore_conflicts=False) # noqa: SLF001
# In debug mode, Django keeps query cache which creates a memory leak in this case
db.reset_queries()
self.queue[objects[0]._meta.model.__name__] = [] # noqa: SLF001
@@ -143,6 +143,8 @@ def create_demo(stdout):
queue.push(
models.Team(
name=f"Team {i:d}",
# slug should be automatic but bulk_create doesn't use save
slug=f"team-{i:d}",
)
)
queue.flush()