# HG changeset patch # User ymh # Date 1500390507 -7200 # Node ID 9864fe2067cd59d109a149ea5dfe8f8d857c89e9 # Parent 672e3c4bbd0c7872c975e425a802d2ae211cc2b1 Add api endpoints for group management diff -r 672e3c4bbd0c -r 9864fe2067cd .hgignore --- 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 diff -r 672e3c4bbd0c -r 9864fe2067cd src/notes/__init__.py --- 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' diff -r 672e3c4bbd0c -r 9864fe2067cd src/notes/api/permissions/__init__.py --- 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"] diff -r 672e3c4bbd0c -r 9864fe2067cd src/notes/api/permissions/auth.py --- /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 diff -r 672e3c4bbd0c -r 9864fe2067cd src/notes/api/permissions/core.py --- 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 diff -r 672e3c4bbd0c -r 9864fe2067cd src/notes/api/serializers/auth.py --- 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 diff -r 672e3c4bbd0c -r 9864fe2067cd src/notes/api/serializers/core.py --- 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 = ( diff -r 672e3c4bbd0c -r 9864fe2067cd src/notes/api/views/auth.py --- 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) diff -r 672e3c4bbd0c -r 9864fe2067cd src/notes/api/views/core.py --- 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__) diff -r 672e3c4bbd0c -r 9864fe2067cd src/notes/apps.py --- 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 diff -r 672e3c4bbd0c -r 9864fe2067cd src/notes/migrations/0001_initial.py --- 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', diff -r 672e3c4bbd0c -r 9864fe2067cd src/notes/models/auth.py --- 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') - diff -r 672e3c4bbd0c -r 9864fe2067cd src/notes/signals.py --- /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() + diff -r 672e3c4bbd0c -r 9864fe2067cd src/notes/tests/__init__.py --- 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 diff -r 672e3c4bbd0c -r 9864fe2067cd src/notes/tests/api/auth.py --- /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())) + ) diff -r 672e3c4bbd0c -r 9864fe2067cd src/notes/tests/models/__init__.py --- /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'] diff -r 672e3c4bbd0c -r 9864fe2067cd src/notes/tests/models/auth.py --- /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) +