From f64c4b5a7dd63511ad43ae9ca9a7158e0df3dc9b Mon Sep 17 00:00:00 2001
From: Paul Marillonnet <pmarillonnet@entrouvert.com>
Date: Thu, 17 Sep 2020 11:40:09 +0200
Subject: [PATCH] api_views: provide a default slug for roles (#22251)

---
 src/authentic2/api_views.py | 40 ++++++++++++++++++++++++++-----------
 tests/test_api.py           | 29 +++++++++++++++++++++++++++
 2 files changed, 57 insertions(+), 12 deletions(-)

diff --git a/src/authentic2/api_views.py b/src/authentic2/api_views.py
index f3266f402..cabf579d7 100644
--- a/src/authentic2/api_views.py
+++ b/src/authentic2/api_views.py
@@ -35,6 +35,7 @@ from django.shortcuts import get_object_or_404
 from django_rbac.utils import get_ou_model, get_role_model
 
 from rest_framework import serializers, pagination
+from rest_framework.validators import UniqueTogetherValidator
 from rest_framework.views import APIView
 from rest_framework.viewsets import ModelViewSet
 from rest_framework.routers import SimpleRouter
@@ -522,6 +523,18 @@ class BaseUserSerializer(serializers.ModelSerializer):
         exclude = ('user_permissions', 'groups')
 
 
+class SlugFromNameDefault:
+    requires_context = False
+    serializer_instance = None
+
+    def set_context(self, instance):
+        self.serializer_instance = instance
+
+    def __call__(self):
+        name = self.serializer_instance.context['request'].data.get('name', '')
+        return slugify(name)
+
+
 class RoleSerializer(serializers.ModelSerializer):
     ou = serializers.SlugRelatedField(
         many=False,
@@ -529,6 +542,11 @@ class RoleSerializer(serializers.ModelSerializer):
         default=CreateOnlyDefault(get_default_ou),
         queryset=get_ou_model().objects.all(),
         slug_field='slug')
+    slug = serializers.SlugField(
+        required=False,
+        allow_blank=False,
+        max_length=256,
+        default=SlugFromNameDefault())
 
     @property
     def user(self):
@@ -564,6 +582,16 @@ class RoleSerializer(serializers.ModelSerializer):
         model = get_role_model()
         fields = ('uuid', 'name', 'slug', 'ou',)
         extra_kwargs = {'uuid': {'read_only': True}}
+        validators = [
+            UniqueTogetherValidator(
+                queryset=get_role_model().objects.all(),
+                fields=['name', 'ou']
+            ),
+            UniqueTogetherValidator(
+                queryset=get_role_model().objects.all(),
+                fields=['slug', 'ou']
+            )
+        ]
 
 
 # override to handle ambiguous naive DateTime on DST change
@@ -888,18 +916,6 @@ class RoleMembershipsAPI(ExceptionHandlerMixin, APIView):
 role_memberships = RoleMembershipsAPI.as_view()
 
 
-class SlugFromNameDefault:
-    requires_context = False
-    serializer_instance = None
-
-    def set_context(self, instance):
-        self.serializer_instance = instance
-
-    def __call__(self):
-        name = self.serializer_instance.context['request'].data.get('name', '')
-        return slugify(name)
-
-
 class BaseOrganizationalUnitSerializer(serializers.ModelSerializer):
     slug = serializers.SlugField(
             required=False,
diff --git a/tests/test_api.py b/tests/test_api.py
index cbfa99132..dd747da0b 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -1255,6 +1255,35 @@ def test_api_post_role_no_ou(app, superuser):
     assert role.ou == get_default_ou()
 
 
+def test_api_post_role_no_slug(app, superuser):
+    app.authorization = ('Basic', (superuser.username, superuser.username))
+    Role = get_role_model()
+
+    role_data = {
+        'name': 'Some Role',
+        }
+    resp = app.post_json('/api/roles/', params=role_data)
+    uuid = resp.json['uuid']
+    role = Role.objects.get(uuid=uuid)
+    assert role.slug == slugify(role.name)
+    assert role.slug == slugify(role_data['name'])
+    # another call with same role name
+    role_data = {
+        'name': 'Some Role',
+        }
+    resp = app.post_json('/api/roles/', params=role_data, status=400)
+    assert resp.json['errors']['__all__'] == [
+            'The fields name, ou must make a unique set.',
+            'The fields slug, ou must make a unique set.',
+        ]
+    # no slug no name
+    role_data = {
+        'id': 42,
+        }
+    resp = app.post_json('/api/roles/', params=role_data, status=400)
+    assert resp.json['errors']['name'] == ['This field is required.']
+
+
 def test_api_post_ou_no_slug(app, superuser):
     app.authorization = ('Basic', (superuser.username, superuser.username))
     OU = get_ou_model()
-- 
2.28.0

