Add api endpoints for group management
authorymh <ymh.work@gmail.com>
Tue, 18 Jul 2017 17:08:27 +0200
changeset 117 9864fe2067cd
parent 116 672e3c4bbd0c
child 118 2cb8d11aa9ca
Add api endpoints for group management
.hgignore
src/notes/__init__.py
src/notes/api/permissions/__init__.py
src/notes/api/permissions/auth.py
src/notes/api/permissions/core.py
src/notes/api/serializers/auth.py
src/notes/api/serializers/core.py
src/notes/api/views/auth.py
src/notes/api/views/core.py
src/notes/apps.py
src/notes/migrations/0001_initial.py
src/notes/models/auth.py
src/notes/signals.py
src/notes/tests/__init__.py
src/notes/tests/api/auth.py
src/notes/tests/models/__init__.py
src/notes/tests/models/auth.py
--- a/.hgignore	Mon Jul 17 14:13:32 2017 +0200
+++ b/.hgignore	Tue Jul 18 17:08:27 2017 +0200
@@ -36,3 +36,5 @@
 
 ^design/Gemfile
 .ruby-version$
+
+^design/api/node_modules
--- a/src/notes/__init__.py	Mon Jul 17 14:13:32 2017 +0200
+++ b/src/notes/__init__.py	Tue Jul 18 17:08:27 2017 +0200
@@ -0,0 +1,1 @@
+default_app_config = 'notes.apps.NotesConfig'
--- a/src/notes/api/permissions/__init__.py	Mon Jul 17 14:13:32 2017 +0200
+++ b/src/notes/api/permissions/__init__.py	Tue Jul 18 17:08:27 2017 +0200
@@ -2,5 +2,6 @@
 Permissions classes fro notes
 """
 from .core import SessionPermission, NotePermission
+from .auth import GroupPermission
 
-__all__ = ["SessionPermission", "NotePermission"]
+__all__ = ["SessionPermission", "NotePermission", "GroupPermission"]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/notes/api/permissions/auth.py	Tue Jul 18 17:08:27 2017 +0200
@@ -0,0 +1,25 @@
+"""
+Permissions for auth objects
+"""
+import logging
+
+from rest_framework import permissions
+from rest_framework.permissions import BasePermission
+from rest_framework.compat import is_authenticated
+
+logger = logging.getLogger(__name__)
+
+
+class GroupPermission(BasePermission):
+    """
+    Pemissions for Groups objects
+    """
+
+    def has_permission(self, request, view):
+        return request.user and is_authenticated(request.user)
+
+
+    def has_object_permission(self, request, view, obj):
+        if request.method not in permissions.SAFE_METHODS:
+            return request.user == obj.profile.owner
+        return True
--- a/src/notes/api/permissions/core.py	Mon Jul 17 14:13:32 2017 +0200
+++ b/src/notes/api/permissions/core.py	Tue Jul 18 17:08:27 2017 +0200
@@ -17,7 +17,6 @@
     def has_object_permission(self, request, view, obj):
         return request.user == obj.owner
 
-
 class NotePermission(IsAuthenticated):
     """
     Permissions for notes
--- a/src/notes/api/serializers/auth.py	Mon Jul 17 14:13:32 2017 +0200
+++ b/src/notes/api/serializers/auth.py	Tue Jul 18 17:08:27 2017 +0200
@@ -1,11 +1,82 @@
 import logging
 
+from django.contrib.auth import get_user_model
 from django.contrib.auth.models import Group
 from rest_framework import serializers
 
 logger = logging.getLogger(__name__)
 
+User = get_user_model()
+
 class GroupSerializer(serializers.ModelSerializer):
+    owner = serializers.CharField(source='profile.owner.username', read_only=True)
+    description = serializers.CharField(source='profile.description')
+
+    class Meta:
+        model = Group
+        fields = ['name', 'owner', 'description']
+
+
+class DetailGroupSerializer(GroupSerializer):
+    users = serializers.SlugRelatedField(
+        many=True,
+        read_only=True,
+        slug_field='username',
+        source='user_set'
+    )
+
     class Meta:
         model = Group
-        fields = '__all__'
+        fields = ['name', 'owner', 'description', 'users']
+
+class WriteGroupSerializer(serializers.ModelSerializer):
+    '''
+    Serializers for writing groups.
+    '''
+
+    description = serializers.CharField(source='profile.description')
+    users = serializers.SlugRelatedField(
+        many=True,
+        slug_field='username',
+        source='user_set',
+        queryset=User.objects.all(),
+        default=[]
+    )
+
+    class Meta:
+        model = Group
+        fields = ['name', 'description', 'users']
+
+
+    def create(self, validated_data):
+        profile_data = validated_data.pop('profile', None)
+
+        group = super().create(validated_data)
+
+        if profile_data is not None:
+            group.profile.description = profile_data.get('description')
+
+        user = None
+        request = self.context.get("request")
+        if request and hasattr(request, "user"):
+            user = request.user
+            group.user_set.add(user)
+            group.save()
+        group.profile.owner = user
+        group.profile.save()
+
+        return group
+
+    def update(self, instance, validated_data):
+        profile_data = validated_data.pop('profile', None)
+        group = super().update(instance, validated_data)
+
+        if profile_data is not None:
+            group.profile.description = profile_data.get('description')
+            group.profile.save()
+
+        # check that owner is still in user list
+        if group.profile.owner and group.profile.owner not in group.user_set.all():
+            group.user_set.add(group.profile.owner)
+
+        return group
--- a/src/notes/api/serializers/core.py	Mon Jul 17 14:13:32 2017 +0200
+++ b/src/notes/api/serializers/core.py	Tue Jul 18 17:08:27 2017 +0200
@@ -88,6 +88,7 @@
     owner = serializers.SlugRelatedField(
         read_only=True, slug_field='username', default=serializers.CurrentUserDefault())
 
+
     class Meta:
         model = Session
         fields = (
--- a/src/notes/api/views/auth.py	Mon Jul 17 14:13:32 2017 +0200
+++ b/src/notes/api/views/auth.py	Tue Jul 18 17:08:27 2017 +0200
@@ -3,13 +3,25 @@
 from django.contrib.auth.models import Group
 from rest_framework import viewsets
 
-from ..serializers.auth import (GroupSerializer)
+from ..serializers.auth import (GroupSerializer, WriteGroupSerializer, DetailGroupSerializer)
+from ..permissions.auth import (GroupPermission, )
 
 logger = logging.getLogger(__name__)
 
 
 class GroupViewSet(viewsets.ModelViewSet):
     serializer_class = GroupSerializer
-    permission_classes = ()
+    permission_classes = (GroupPermission, )
+    lookup_field = 'name'
+
     def get_queryset(self):
         return Group.objects.all()
+
+    serializers = {
+        'create': WriteGroupSerializer,
+        'update': WriteGroupSerializer,
+        'retrieve': DetailGroupSerializer,
+    }
+
+    def get_serializer_class(self):
+        return self.serializers.get(self.action, GroupSerializer)
--- a/src/notes/api/views/core.py	Mon Jul 17 14:13:32 2017 +0200
+++ b/src/notes/api/views/core.py	Tue Jul 18 17:08:27 2017 +0200
@@ -1,11 +1,15 @@
+"""
+Core viewsets
+"""
 import logging
 
 from notes.models import Note, Session
-from rest_framework import viewsets, serializers
+from rest_framework import viewsets
 
 from ..permissions import NotePermission, SessionPermission
-from ..serializers.core import (DetailNoteSerializer, UpdateNoteSerializer, DetailSessionSerializer, CreateNoteSerializer,
-                                ListNoteSerializer, ListSessionSerializer, CreateSessionSerializer)
+from ..serializers.core import (DetailNoteSerializer, UpdateNoteSerializer, DetailSessionSerializer,
+                                CreateNoteSerializer, ListNoteSerializer, ListSessionSerializer,
+                                CreateSessionSerializer)
 
 logger = logging.getLogger(__name__)
 
--- a/src/notes/apps.py	Mon Jul 17 14:13:32 2017 +0200
+++ b/src/notes/apps.py	Tue Jul 18 17:08:27 2017 +0200
@@ -3,3 +3,7 @@
 
 class NotesConfig(AppConfig):
     name = 'notes'
+
+    def ready(self):
+        # import signal handlers
+        import notes.signals
--- a/src/notes/migrations/0001_initial.py	Mon Jul 17 14:13:32 2017 +0200
+++ b/src/notes/migrations/0001_initial.py	Tue Jul 18 17:08:27 2017 +0200
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Generated by Django 1.11.2 on 2017-06-21 10:50
+# Generated by Django 1.11.2 on 2017-07-07 09:59
 from __future__ import unicode_literals
 
 import colorful.fields
@@ -66,7 +66,8 @@
             fields=[
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                 ('description', models.TextField(blank=True, null=True)),
-                ('group', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='auth.Group')),
+                ('group', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to='auth.Group')),
+                ('owner', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
             ],
             options={
                 'verbose_name': 'GroupProfile',
--- a/src/notes/models/auth.py	Mon Jul 17 14:13:32 2017 +0200
+++ b/src/notes/models/auth.py	Tue Jul 18 17:08:27 2017 +0200
@@ -11,17 +11,22 @@
         verbose_name = _('User')
         verbose_name_plural = _('Users')
 
+
 class UserProfile(models.Model):
     user = models.OneToOneField(User, on_delete=models.CASCADE)
+
     class Meta:
         verbose_name = _('UserProfile')
         verbose_name_plural = _('UserProfiles')
 
 
 class GroupProfile(models.Model):
-    group = models.OneToOneField(Group, unique=True, on_delete=models.CASCADE)
+    group = models.OneToOneField(
+        Group, unique=True, on_delete=models.CASCADE, related_name='profile')
     description = models.TextField(null=True, blank=True)
+    # TODO: manage when user is deleted: put first user as owner. delete group if empty
+    owner = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
+
     class Meta:
         verbose_name = _('GroupProfile')
         verbose_name_plural = _('GroupProfiles')
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/notes/signals.py	Tue Jul 18 17:08:27 2017 +0200
@@ -0,0 +1,15 @@
+"""
+Signals for notes app
+"""
+from django.db.models.signals import post_save
+from django.dispatch import receiver
+from django.contrib.auth.models import Group
+from notes.models import GroupProfile
+
+@receiver(post_save, sender=Group, dispatch_uid="group_created_signal")
+def group_saved_callback(sender, instance, **kwargs):
+    created = kwargs.pop('created')
+    if instance and created:
+        profile = GroupProfile(group=instance)
+        profile.save()
+
--- a/src/notes/tests/__init__.py	Mon Jul 17 14:13:32 2017 +0200
+++ b/src/notes/tests/__init__.py	Tue Jul 18 17:08:27 2017 +0200
@@ -1,1 +1,2 @@
 from .api import SessionApiTests, NoteApiTests
+from .models import AuthModelTests
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/notes/tests/api/auth.py	Tue Jul 18 17:08:27 2017 +0200
@@ -0,0 +1,137 @@
+"""
+Tests the core api for sessions
+"""
+import logging
+from uuid import uuid4
+
+from django.contrib.auth import get_user_model
+from django.urls import reverse
+from django.utils import timezone
+from rest_framework import status
+from rest_framework.test import APITransactionTestCase
+from django.contrib.auth.models import Group
+
+logger = logging.getLogger(__name__)
+
+
+class GroupApiTests(APITransactionTestCase):
+
+    def setUp(self):
+        User = get_user_model()
+        self.user1 = User.objects.create_user(
+            username='test_user1',
+            email='test_user@emial.com',
+            password='top_secret'
+        )
+        self.user2 = User.objects.create_user(
+            username='test_user2',
+            email='test_user@emial.com',
+            password='top_secret'
+        )
+
+        self.group1 = Group(name='group1')
+        self.group1.save()
+        self.group1.user_set.add(self.user1)
+        self.group1.profile.owner = self.user1
+        self.group1.profile.description = "This is the group 1"
+        self.group1.profile.save()
+
+        self.group2 = Group(name='group2')
+        self.group2.save()
+        self.group2.profile.owner = self.user2
+        self.group2.profile.description = "This is the group 2"
+        self.group2.profile.save()
+
+
+    def test_list_group_no_login(self):
+        url = reverse('auth_group-list')
+        response = self.client.get(url)
+        logger.debug("LIST group response %r", response.data)
+
+        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
+
+
+    def test_list_group(self):
+        url = reverse('auth_group-list')
+        self.client.login(username='test_user1', password='top_secret')
+        response = self.client.get(url)
+
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+        self.assertEqual(2, len(response.data), "Must have 2 groups")
+        for group_def in response.data:
+            self.assertIn('name', group_def.keys())
+            self.assertIn('owner', group_def.keys())
+            self.assertIn('description', group_def.keys())
+            self.assertIn(group_def.get('owner'), ['test_user1', 'test_user2'])
+            self.assertIn(group_def.get('description'), ['This is the group 1', 'This is the group 2'])
+            self.assertIn(group_def.get('name'), ['group1', 'group2'])
+
+    def test_create_group(self):
+        url = reverse('auth_group-list')
+        self.client.login(username='test_user1', password='top_secret')
+        response = self.client.post(
+            url,
+            {'name':"group3", 'description': "this is group 3", 'users': ['test_user2']})
+
+        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+
+        self.assertEqual('group3', response.data.get('name'))
+        self.assertSetEqual(set(['test_user1', 'test_user2']), set(response.data.get('users')))
+
+        group3 = Group.objects.get(name='group3')
+        self.assertEqual('this is group 3', group3.profile.description)
+        self.assertSetEqual(
+            set(['test_user1', 'test_user2']),
+            set(map(lambda u: u.username, group3.user_set.all()))
+        )
+
+    def test_create_group_no_user(self):
+        url = reverse('auth_group-list')
+        self.client.login(username='test_user1', password='top_secret')
+        response = self.client.post(
+            url,
+            {'name':"group3", 'description': "this is group 3"})
+
+        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+
+        self.assertEqual('group3', response.data.get('name'))
+        self.assertSetEqual(set(['test_user1']), set(response.data.get('users')))
+
+        group3 = Group.objects.get(name='group3')
+        self.assertEqual('this is group 3', group3.profile.description)
+        self.assertSetEqual(
+            set(['test_user1']),
+            set(map(lambda u: u.username, group3.user_set.all()))
+        )
+
+
+    def test_detail_group(self):
+        url = reverse('auth_group-detail', kwargs={'name': self.group1.name})
+        self.client.login(username='test_user1', password='top_secret')
+        response = self.client.get(url)
+
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual('group1', response.data.get('name'))
+        self.assertEqual('This is the group 1', response.data.get('description'))
+        self.assertSetEqual(set(['test_user1']), set(response.data.get('users')))
+
+    def test_update_group(self):
+        url = reverse('auth_group-detail', kwargs={'name': self.group1.name})
+        self.client.login(username='test_user1', password='top_secret')
+        response = self.client.put(
+            url,
+            {'name': 'group1', 'description': "this is group 1 changed", 'users': ['test_user2']})
+
+        logger.debug("RESPONSE %r", response.data)
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual('group1', response.data.get('name'))
+        self.assertEqual('this is group 1 changed', response.data.get('description'))
+        self.assertSetEqual(set(['test_user1', 'test_user2']), set(response.data.get('users')))
+
+        group1 = Group.objects.get(name='group1')
+        self.assertEqual('this is group 1 changed', group1.profile.description)
+        self.assertSetEqual(
+            set(['test_user1', 'test_user2']),
+            set(map(lambda u: u.username, group1.user_set.all()))
+        )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/notes/tests/models/__init__.py	Tue Jul 18 17:08:27 2017 +0200
@@ -0,0 +1,3 @@
+from .auth import AuthModelTests
+
+__all__ = ['AuthModelTests']
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/notes/tests/models/auth.py	Tue Jul 18 17:08:27 2017 +0200
@@ -0,0 +1,34 @@
+"""
+Tests the core models for auth
+"""
+import logging
+from uuid import uuid4
+
+from django.contrib.auth import get_user_model
+from django.contrib.auth.models import Group
+from django.test import TransactionTestCase
+
+logger = logging.getLogger(__name__)
+
+
+class AuthModelTests(TransactionTestCase):
+
+    def setUp(self):
+        User = get_user_model()
+        self.user1 = User.objects.create_user(
+            username='test_user1',
+            email='test_user@emial.com',
+            password='top_secret'
+        )
+        self.user2 = User.objects.create_user(
+            username='test_user2',
+            email='test_user@emial.com',
+            password='top_secret'
+        )
+        self.group = Group(name='group1')
+        self.group.save()
+
+    def test_create_profile(self):
+        self.assertIsNotNone(self.group)
+        self.assertIsNotNone(self.group.profile)
+