Commit 8df2b294 by Zia Fazal Committed by Jonathan Piacenti

API calls to filter user list by username/email

add pagination to /api/users

add support of id based filtering

add num_pages field to users api output

add tests for paging data and ability to parse page_size from request

using ListAPIView and DRF filters

Disallow unfiltered lists

Updated docstrings
parent efec1fff
...@@ -4,8 +4,7 @@ import logging ...@@ -4,8 +4,7 @@ import logging
from django.conf import settings from django.conf import settings
from api_manager.utils import get_client_ip_address, address_exists_in_network from api_manager.utils import get_client_ip_address, address_exists_in_network
from rest_framework import permissions from rest_framework import permissions, generics, filters, pagination, serializers
from rest_framework.views import APIView
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -79,8 +78,37 @@ class IPAddressRestrictedPermission(permissions.BasePermission): ...@@ -79,8 +78,37 @@ class IPAddressRestrictedPermission(permissions.BasePermission):
return True return True
class SecureAPIView(APIView): class IdsInFilterBackend(filters.BaseFilterBackend):
""" """
Inherited from APIView This backend support filtering queryset by a list of ids
"""
def filter_queryset(self, request, queryset, view):
"""
Parse querystring to get ids and the filter the queryset
Max of 100 values are allowed for performance reasons
"""
ids = request.QUERY_PARAMS.get('ids')
if ids:
if ',' in ids:
ids = ids.split(",")[:100]
return queryset.filter(id__in=ids)
return queryset
class CustomPaginationSerializer(pagination.PaginationSerializer):
"""
Custom PaginationSerializer to include num_pages field
"""
num_pages = serializers.Field(source='paginator.num_pages')
class SecureAPIView(generics.ListAPIView):
"""
Inherited from ListAPIView
""" """
permission_classes = (ApiKeyHeaderPermission, IPAddressRestrictedPermission) permission_classes = (ApiKeyHeaderPermission, IPAddressRestrictedPermission)
filter_backends = (filters.DjangoFilterBackend, IdsInFilterBackend,)
pagination_serializer_class = CustomPaginationSerializer
paginate_by = getattr(settings, 'API_PAGE_SIZE', 20)
paginate_by_param = 'page_size'
max_paginate_by = 100
...@@ -10,8 +10,6 @@ import unittest ...@@ -10,8 +10,6 @@ import unittest
import uuid import uuid
from mock import patch from mock import patch
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.conf import settings
from django.contrib.auth.models import User
from django.core.cache import cache from django.core.cache import cache
from django.test import TestCase, Client from django.test import TestCase, Client
from django.test.utils import override_settings from django.test.utils import override_settings
...@@ -91,6 +89,54 @@ class UsersApiTests(TestCase): ...@@ -91,6 +89,54 @@ class UsersApiTests(TestCase):
user_id = response.data['id'] user_id = response.data['id']
return user_id return user_id
@override_settings(API_PAGE_SIZE=10)
def test_user_list_get(self):
test_uri = '/api/users'
# create a 25 new users
for i in xrange(1, 26):
data = {
'email': 'test{}@example.com'.format(i),
'username': 'test_user{}'.format(i),
'password': 'test_pass',
'first_name': 'John{}'.format(i),
'last_name': 'Doe{}'.format(i)
}
response = self.do_post(test_uri, data)
self.assertEqual(response.status_code, 201)
# fetch data without any filters applied
response = self.do_get('{}?page=1'.format(test_uri))
self.assertEqual(response.status_code, 400)
# fetch users data with page outside range
response = self.do_get('{}?ids={}&page=5'.format(test_uri, '2,3,7,11,6,21,34'))
self.assertEqual(response.status_code, 404)
# fetch user data by single id
response = self.do_get('{}?ids={}'.format(test_uri, '3'))
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data['results']), 1)
# fetch user data by multiple ids
response = self.do_get('{}?page_size=5&ids={}'.format(test_uri, '2,3,7,11,6,21,34'))
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['count'], 6)
self.assertEqual(len(response.data['results']), 5)
self.assertEqual(response.data['num_pages'], 2)
self.assertIn('page=2', response.data['next'])
self.assertEqual(response.data['previous'], None)
# fetch user data by username
response = self.do_get('{}?username={}'.format(test_uri, 'test_user1'))
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data['results']), 1)
# fetch user data by email
response = self.do_get('{}?email={}'.format(test_uri, 'test2@example.com'))
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data['results']), 1)
self.assertIsNotNone(response.data['results'][0]['id'])
# fetch by username with a non existing user
response = self.do_get('{}?email={}'.format(test_uri, 'john@example.com'))
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data['results']), 0)
def test_user_list_post(self): def test_user_list_post(self):
test_uri = '/api/users' test_uri = '/api/users'
local_username = self.test_username + str(randint(11, 99)) local_username = self.test_username + str(randint(11, 99))
......
...@@ -15,6 +15,7 @@ from django.db.models import Q ...@@ -15,6 +15,7 @@ from django.db.models import Q
from api_manager.permissions import SecureAPIView from api_manager.permissions import SecureAPIView
from api_manager.models import GroupProfile from api_manager.models import GroupProfile
from .serializers import UserSerializer
from courseware import module_render from courseware import module_render
from courseware.model_data import FieldDataCache from courseware.model_data import FieldDataCache
...@@ -117,6 +118,19 @@ class UsersList(SecureAPIView): ...@@ -117,6 +118,19 @@ class UsersList(SecureAPIView):
""" """
### The UsersList view allows clients to retrieve/append a list of User entities ### The UsersList view allows clients to retrieve/append a list of User entities
- URI: ```/api/users/``` - URI: ```/api/users/```
- GET: Provides paginated list of users, it supports email, username and id filters
Possible use cases
GET /api/users?ids=23
GET /api/users?ids=11,12,13&page=2
GET /api/users?email={john@example.com}
GET /api/users?username={john}
* email: string, filters user set by email address
* username: string, filters user set by username
Example JSON output {'count': '25', 'next': 'https://testserver/api/users?page=2', num_pages='3',
'previous': None, 'results':[]}
'next' and 'previous' keys would have value of None if there are not next or previous page after current page.
- POST: Provides the ability to append to the User entity set - POST: Provides the ability to append to the User entity set
* email: __required__, The unique email address for the User being created * email: __required__, The unique email address for the User being created
* username: __required__, The unique username for the User being created * username: __required__, The unique username for the User being created
...@@ -151,11 +165,25 @@ class UsersList(SecureAPIView): ...@@ -151,11 +165,25 @@ class UsersList(SecureAPIView):
"avatar_url" : "http://example.com/avatar.png" "avatar_url" : "http://example.com/avatar.png"
} }
### Use Cases/Notes: ### Use Cases/Notes:
* GET requests for _all_ users are not currently allowed via the API
* Password formatting policies can be enabled through the "ENFORCE_PASSWORD_POLICY" feature flag * Password formatting policies can be enabled through the "ENFORCE_PASSWORD_POLICY" feature flag
* The first_name and last_name fields are additionally concatenated and stored in the 'name' field of UserProfile * The first_name and last_name fields are additionally concatenated and stored in the 'name' field of UserProfile
* Values for level_of_education can be found in the LEVEL_OF_EDUCATION_CHOICES enum, located in common/student/models.py * Values for level_of_education can be found in the LEVEL_OF_EDUCATION_CHOICES enum, located in common/student/models.py
""" """
queryset = User.objects.all()
serializer_class = UserSerializer
filter_fields = ('email', 'username', )
def get(self, request, *args, **kwargs):
"""
GET /api/users?ids=11,12,13.....&page=2
"""
email = request.QUERY_PARAMS.get('email', None)
username = request.QUERY_PARAMS.get('username', None)
ids = request.QUERY_PARAMS.get('ids', None)
if email or username or ids:
return self.list(request, *args, **kwargs)
else:
return Response({'message': _('Unfiltered request is not allowed.')}, status=status.HTTP_400_BAD_REQUEST)
def post(self, request): def post(self, request):
""" """
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment