Skip to content

Commit 8ada822

Browse files
authored
Merge pull request #555 from jbernal0019/master
Implement API to manage groups and their users
2 parents f95f05a + fcde144 commit 8ada822

File tree

10 files changed

+521
-24
lines changed

10 files changed

+521
-24
lines changed

chris_backend/config/urls.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from django.conf import settings
2121

2222
from plugins import admin as plugin_admin_views
23+
from users import views as group_admin_views
2324

2425

2526
urlpatterns = [
@@ -38,6 +39,26 @@
3839
plugin_admin_views.ComputeResourceAdminDetail.as_view(),
3940
name='admin-computeresource-detail'),
4041

42+
path('chris-admin/api/v1/groups/',
43+
group_admin_views.GroupList.as_view(),
44+
name='group-list'),
45+
46+
path('chris-admin/api/v1/groups/search/',
47+
group_admin_views.GroupListQuerySearch.as_view(),
48+
name='group-list-query-search'),
49+
50+
path('chris-admin/api/v1/groups/<int:pk>/',
51+
group_admin_views.GroupDetail.as_view(),
52+
name='group-detail'),
53+
54+
path('chris-admin/api/v1/groups/<int:pk>/users/',
55+
group_admin_views.GroupUserList.as_view(),
56+
name='group-user-list'),
57+
58+
path('chris-admin/api/v1/groups/users/<int:pk>/',
59+
group_admin_views.GroupUserDetail.as_view(),
60+
name='user_groups-detail'),
61+
4162
path('chris-admin/', admin.site.urls),
4263

4364
path('api/', include('core.api')),

chris_backend/core/admin.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,4 @@ def has_delete_permission(self, request, obj=None):
1818

1919
admin.site.site_header = 'ChRIS Administration'
2020
admin.site.site_title = 'ChRIS Admin'
21-
admin.site.unregister(Group)
2221
admin.site.register(ChrisInstance, ChrisInstanceAdmin)

chris_backend/core/api.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
path('v1/users/<int:pk>/',
2929
user_views.UserDetail.as_view(), name='user-detail'),
3030

31+
path('v1/users/<int:pk>/groups/',
32+
user_views.UserGroupList.as_view(), name='user-group-list'),
33+
3134

3235
path('v1/downloadtokens/',
3336
core_views.FileDownloadTokenList.as_view(),

chris_backend/plugins/admin.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -755,10 +755,13 @@ def list(self, request, *args, **kwargs):
755755
to the response.
756756
"""
757757
response = super(PluginAdminList, self).list(request, *args, **kwargs)
758+
758759
# append document-level link relations
759760
links = {'compute_resources': reverse('admin-computeresource-list',
760-
request=request)}
761+
request=request),
762+
'groups': reverse('group-list', request=request)}
761763
response = services.append_collection_links(response, links)
764+
762765
# append write template
763766
template_data = {'fname': '', 'compute_names': '', 'plugin_store_url': ''}
764767
return services.append_collection_template(response, template_data)

chris_backend/users/models.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1-
from django.db import models
21

3-
# Create your models here.
2+
from django.contrib.auth.models import Group
3+
4+
import django_filters
5+
from django_filters.rest_framework import FilterSet
6+
7+
8+
class GroupFilter(FilterSet):
9+
name_icontains = django_filters.CharFilter(field_name='name', lookup_expr='icontains')
10+
11+
class Meta:
12+
model = Group
13+
fields = ['id', 'name', 'name_icontains']

chris_backend/users/permissions.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,17 @@ def has_object_permission(self, request, view, obj):
1818
return (obj == request.user) or (request.user.username == 'chris')
1919

2020

21+
class IsAdminOrReadOnly(permissions.BasePermission):
22+
"""
23+
Custom permission to only allow admin users to modify/edit it. Read only is allowed
24+
to normal users.
25+
"""
26+
27+
def has_object_permission(self, request, view, obj):
28+
# Read permissions are allowed to normal users.
29+
if request.method in permissions.SAFE_METHODS:
30+
#if obj.user_set.filter(username=request.user.username).exists():
31+
return True
32+
33+
# Raed/Write permissions are allowed to the admin users
34+
return request.user.is_staff

chris_backend/users/serializers.py

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import logging
33
import io
44

5-
from django.contrib.auth.models import User
5+
from django.contrib.auth.models import User, Group
66
from django.conf import settings
77
from rest_framework import serializers
88
from rest_framework.validators import UniqueValidator
@@ -16,19 +16,18 @@
1616

1717

1818
class UserSerializer(serializers.HyperlinkedModelSerializer):
19-
feed = serializers.HyperlinkedRelatedField(many=True, view_name='feed-detail',
20-
read_only=True)
2119
username = serializers.CharField(min_length=4, max_length=32,
2220
validators=[UniqueValidator(
2321
queryset=User.objects.all())])
2422
email = serializers.EmailField(required=True,
2523
validators=[UniqueValidator(
2624
queryset=User.objects.all())])
2725
password = serializers.CharField(min_length=8, max_length=100, write_only=True)
26+
groups = serializers.HyperlinkedIdentityField(view_name='user-group-list')
2827

2928
class Meta:
3029
model = User
31-
fields = ('url', 'id', 'username', 'email', 'password', 'is_staff', 'feed')
30+
fields = ('url', 'id', 'username', 'email', 'password', 'is_staff', 'groups')
3231

3332
def create(self, validated_data):
3433
"""
@@ -73,3 +72,48 @@ def validate_username(self, username):
7372
raise serializers.ValidationError(
7473
["Username %s is not available." % username])
7574
return username
75+
76+
77+
class GroupSerializer(serializers.HyperlinkedModelSerializer):
78+
users = serializers.HyperlinkedIdentityField(view_name='group-user-list')
79+
80+
class Meta:
81+
model = Group
82+
fields = ('url', 'id', 'name', 'users')
83+
84+
def validate_name(self, name):
85+
"""
86+
Overriden to check that the name does not contain forward slashes.
87+
"""
88+
if '/' in name:
89+
raise serializers.ValidationError(
90+
["This field may not contain forward slashes."])
91+
return name
92+
93+
94+
95+
class GroupUserSerializer(serializers.HyperlinkedModelSerializer):
96+
username = serializers.CharField(write_only=True, min_length=4, max_length=32)
97+
group_id = serializers.ReadOnlyField(source='group.id')
98+
group_name = serializers.ReadOnlyField(source='group.name')
99+
user_id = serializers.ReadOnlyField(source='user.id')
100+
user_username = serializers.ReadOnlyField(source='user.username')
101+
user_email = serializers.ReadOnlyField(source='user.email')
102+
group = serializers.HyperlinkedRelatedField(view_name='group-detail', read_only=True)
103+
user = serializers.HyperlinkedRelatedField(view_name='user-detail', read_only=True)
104+
105+
class Meta:
106+
model = User.groups.through
107+
fields = ('url', 'id', 'group_id', 'group_name', 'user_id', 'user_username',
108+
'user_email', 'group', 'user', 'username')
109+
110+
def validate_username(self, username):
111+
"""
112+
Custom method to check whether the provided username exists in the DB.
113+
"""
114+
try:
115+
user = User.objects.get(username=username)
116+
except User.DoesNotExist:
117+
raise serializers.ValidationError(
118+
{'username': [f"Couldn't find any user with username '{username}'."]})
119+
return user

chris_backend/users/tests/test_serializers.py

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77
from rest_framework import serializers
88

99
from userfiles.models import UserFile
10-
from users.serializers import UserSerializer
10+
from users.serializers import UserSerializer, GroupSerializer, GroupUserSerializer
1111
from core.storage.helpers import mock_storage
1212

1313

14-
class UserSerializerTests(TestCase):
14+
class SerializerTests(TestCase):
1515
"""
16-
Generic user view tests' setup and tearDown
16+
Generic serializers tests' setup and tearDown.
1717
"""
1818

1919
def setUp(self):
@@ -34,6 +34,12 @@ def tearDown(self):
3434
# re-enable logging
3535
logging.disable(logging.NOTSET)
3636

37+
38+
class UserSerializerTests(SerializerTests):
39+
"""
40+
User serializer tests.
41+
"""
42+
3743
def test_create(self):
3844
"""
3945
Test whether overriden create method takes care of the password hashing and
@@ -62,9 +68,50 @@ def test_validate_username(self):
6268
the 'chris' special user.
6369
"""
6470
user_serializer = UserSerializer()
71+
6572
with self.assertRaises(serializers.ValidationError):
6673
user_serializer.validate_username('user/')
74+
6775
with self.assertRaises(serializers.ValidationError):
6876
user_serializer.validate_username('chris')
77+
6978
username = user_serializer.validate_username(self.username)
7079
self.assertEqual(username, self.username)
80+
81+
82+
class GroupSerializerTests(SerializerTests):
83+
"""
84+
Group serializer tests.
85+
"""
86+
87+
def test_validate_name(self):
88+
"""
89+
Test whether overriden validate_name method raises a
90+
serializers.ValidationError when the group name contains forward slashes.
91+
"""
92+
group_serializer = GroupSerializer()
93+
94+
with self.assertRaises(serializers.ValidationError):
95+
group_serializer.validate_name('user/')
96+
97+
group_name = group_serializer.validate_name('students')
98+
self.assertEqual(group_name, 'students')
99+
100+
101+
class GroupUserSerializerTests(SerializerTests):
102+
"""
103+
Group user serializer tests.
104+
"""
105+
106+
def test_validate_name(self):
107+
"""
108+
Test whether overriden validate_username method raises a
109+
serializers.ValidationError when the passed username doesn't exist in the DB.
110+
"""
111+
group_user_serializer = GroupUserSerializer()
112+
113+
with self.assertRaises(serializers.ValidationError):
114+
group_user_serializer.validate_username('foo')
115+
116+
user = group_user_serializer.validate_username(self.chris_username)
117+
self.assertEqual(user.username, self.chris_username)

0 commit comments

Comments
 (0)