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
from django.conf import settings
from api_manager.utils import get_client_ip_address, address_exists_in_network
from rest_framework import permissions
from rest_framework.views import APIView
from rest_framework import permissions, generics, filters, pagination, serializers
log = logging.getLogger(__name__)
......@@ -79,8 +78,37 @@ class IPAddressRestrictedPermission(permissions.BasePermission):
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)
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
import uuid
from mock import patch
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.test import TestCase, Client
from django.test.utils import override_settings
......@@ -91,6 +89,54 @@ class UsersApiTests(TestCase):
user_id = response.data['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):
test_uri = '/api/users'
local_username = self.test_username + str(randint(11, 99))
......
......@@ -15,6 +15,7 @@ from django.db.models import Q
from api_manager.permissions import SecureAPIView
from api_manager.models import GroupProfile
from .serializers import UserSerializer
from courseware import module_render
from courseware.model_data import FieldDataCache
......@@ -117,6 +118,19 @@ class UsersList(SecureAPIView):
"""
### The UsersList view allows clients to retrieve/append a list of User entities
- 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
* email: __required__, The unique email address for the User being created
* username: __required__, The unique username for the User being created
......@@ -151,11 +165,25 @@ class UsersList(SecureAPIView):
"avatar_url" : "http://example.com/avatar.png"
}
### 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
* 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
"""
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):
"""
......
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