Commit 6574d611 by Saqib

Add new fields to users list API and add organizaton, name and course enrolment filters

parent b09b56c8
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
from rest_framework import serializers from rest_framework import serializers
from api_manager.models import APIUser from api_manager.models import APIUser, GroupProfile
from organizations.serializers import BasicOrganizationSerializer from organizations.serializers import BasicOrganizationSerializer
from student.models import UserProfile from student.models import UserProfile
...@@ -27,6 +27,15 @@ class DynamicFieldsModelSerializer(serializers.ModelSerializer): ...@@ -27,6 +27,15 @@ class DynamicFieldsModelSerializer(serializers.ModelSerializer):
self.fields.pop(field_name) self.fields.pop(field_name)
class GroupProfileSerializer(serializers.ModelSerializer):
""" Serializer for GroupProfile model interactions """
class Meta(object):
""" Serializer/field specification """
model = GroupProfile
fields = ('id', 'name', )
class UserSerializer(DynamicFieldsModelSerializer): class UserSerializer(DynamicFieldsModelSerializer):
""" Serializer for User model interactions """ """ Serializer for User model interactions """
...@@ -37,11 +46,45 @@ class UserSerializer(DynamicFieldsModelSerializer): ...@@ -37,11 +46,45 @@ class UserSerializer(DynamicFieldsModelSerializer):
title = serializers.CharField(source='profile.title') title = serializers.CharField(source='profile.title')
country = serializers.CharField(source='profile.country') country = serializers.CharField(source='profile.country')
full_name = serializers.CharField(source='profile.name') full_name = serializers.CharField(source='profile.name')
courses_enrolled = serializers.SerializerMethodField('get_courses_enrolled')
roles = serializers.SerializerMethodField('get_permission_group_type_roles')
class Meta: def get_courses_enrolled(self, user):
""" Serialize user enrolled courses """
if hasattr(user, 'courses_enrolled'):
return user.courses_enrolled
return user.courseenrollment_set.count
def get_permission_group_type_roles(self, user):
""" Serialize GroupProfile for permission group type """
queryset = GroupProfile.objects.filter(group__user=user, group_type='permission')
serializer = GroupProfileSerializer(queryset, many=True)
return serializer.data
class Meta(object):
""" Serializer/field specification """ """ Serializer/field specification """
model = APIUser model = APIUser
fields = ("id", "email", "username", "first_name", "last_name", "created", "is_active", "organizations", "avatar_url", "city", "title", "country", "full_name", "is_staff") fields = (
"id",
"email",
"username",
"first_name",
"last_name",
"created",
"is_active",
"organizations",
"avatar_url",
"city",
"title",
"country",
"full_name",
"is_staff",
"last_login",
"courses_enrolled",
"roles"
)
read_only_fields = ("id", "email", "username") read_only_fields = ("id", "email", "username")
......
...@@ -24,13 +24,15 @@ from django.test.utils import override_settings ...@@ -24,13 +24,15 @@ from django.test.utils import override_settings
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from api_manager.models import GroupProfile
from capa.tests.response_xml_factory import StringResponseXMLFactory from capa.tests.response_xml_factory import StringResponseXMLFactory
from courseware import module_render from courseware import module_render
from courseware.model_data import FieldDataCache from courseware.model_data import FieldDataCache
from django_comment_common.models import Role, FORUM_ROLE_MODERATOR from django_comment_common.models import Role, FORUM_ROLE_MODERATOR
from instructor.access import allow_access from instructor.access import allow_access
from organizations.models import Organization
from projects.models import Project, Workgroup from projects.models import Project, Workgroup
from student.tests.factories import UserFactory from student.tests.factories import UserFactory, CourseEnrollmentFactory, GroupFactory
from student.models import anonymous_id_for_user from student.models import anonymous_id_for_user
from openedx.core.djangoapps.user_api.models import UserPreference from openedx.core.djangoapps.user_api.models import UserPreference
...@@ -75,7 +77,6 @@ class SecureClient(Client): ...@@ -75,7 +77,6 @@ class SecureClient(Client):
@override_settings(MODULESTORE=MODULESTORE_CONFIG) @override_settings(MODULESTORE=MODULESTORE_CONFIG)
@override_settings(EDX_API_KEY=TEST_API_KEY) @override_settings(EDX_API_KEY=TEST_API_KEY)
@override_settings(PASSWORD_MIN_LENGTH=4) @override_settings(PASSWORD_MIN_LENGTH=4)
@override_settings(API_PAGE_SIZE=10)
@mock.patch.dict("django.conf.settings.FEATURES", {'ENFORCE_PASSWORD_POLICY': True}) @mock.patch.dict("django.conf.settings.FEATURES", {'ENFORCE_PASSWORD_POLICY': True})
class UsersApiTests(ModuleStoreTestCase): class UsersApiTests(ModuleStoreTestCase):
""" Test suite for Users API views """ """ Test suite for Users API views """
...@@ -262,7 +263,14 @@ class UsersApiTests(ModuleStoreTestCase): ...@@ -262,7 +263,14 @@ class UsersApiTests(ModuleStoreTestCase):
# fetch data without any filters applied # fetch data without any filters applied
response = self.do_get('{}?page=1'.format(test_uri)) response = self.do_get('{}?page=1'.format(test_uri))
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 200)
# test default page size
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data['results']), 20)
self.assertEqual(response.data['num_pages'], 2)
# fetch users data with page outside range # fetch users data with page outside range
response = self.do_get('{}?ids={}&page=5'.format(test_uri, '2,3,7,11,6,21,34')) response = self.do_get('{}?ids={}&page=5'.format(test_uri, '2,3,7,11,6,21,34'))
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
...@@ -274,6 +282,7 @@ class UsersApiTests(ModuleStoreTestCase): ...@@ -274,6 +282,7 @@ class UsersApiTests(ModuleStoreTestCase):
self.assertIsNotNone(response.data['results'][0]['organizations'][0]['name']) self.assertIsNotNone(response.data['results'][0]['organizations'][0]['name'])
self.assertIsNotNone(response.data['results'][0]['organizations'][0]['id']) self.assertIsNotNone(response.data['results'][0]['organizations'][0]['id'])
self.assertIsNotNone(response.data['results'][0]['organizations'][0]['url']) self.assertIsNotNone(response.data['results'][0]['organizations'][0]['url'])
self.assertIsNotNone(response.data['results'][0]['last_login'])
self.assertIsNotNone(response.data['results'][0]['created']) self.assertIsNotNone(response.data['results'][0]['created'])
# fetch user data by multiple ids # fetch user data by multiple ids
response = self.do_get('{}?page_size=5&ids={}'.format(test_uri, '2,3,7,11,6,21,34')) response = self.do_get('{}?page_size=5&ids={}'.format(test_uri, '2,3,7,11,6,21,34'))
...@@ -306,7 +315,57 @@ class UsersApiTests(ModuleStoreTestCase): ...@@ -306,7 +315,57 @@ class UsersApiTests(ModuleStoreTestCase):
if 'id' in response.data['results'][0]: if 'id' in response.data['results'][0]:
self.fail("Dynamic field filtering error in UserSerializer") self.fail("Dynamic field filtering error in UserSerializer")
def test_user_list_get_with_org_filter(self): def test_user_list_get_courses_enrolled(self):
test_uri = self.users_base_uri
# create a 2 new users
users = UserFactory.create_batch(2)
# create course enrollments
CourseEnrollmentFactory.create(user=users[1], course_id=self.course.id)
CourseEnrollmentFactory.create(user=users[1], course_id=self.course2.id)
# fetch user 1
response = self.do_get('{}?ids={}'.format(test_uri, users[0].id))
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data['results']), 1)
self.assertEqual(response.data['results'][0]['courses_enrolled'], 0)
# fetch user 2
response = self.do_get('{}?ids={}'.format(test_uri, users[1].id))
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data['results']), 1)
self.assertEqual(response.data['results'][0]['courses_enrolled'], 2)
def test_user_list_get_roles(self):
test_uri = self.users_base_uri
# create a 3 new users
users = UserFactory.create_batch(3)
groups = GroupFactory.create_batch(3)
group_profile1 = GroupProfile.objects.create(group=groups[0], name='role1', group_type='permission')
group_profile2 = GroupProfile.objects.create(group=groups[1], name='role2', group_type='permission')
GroupProfile.objects.create(group=groups[2], name='role3', group_type='test')
users[0].groups.add(*groups)
users[1].groups.add(groups[0])
users[2].groups.add(groups[2])
# fetch users
user_ids = ','.join([str(user.id) for user in users])
response = self.do_get('{}?ids={}'.format(test_uri, user_ids))
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data['results']), 3)
self.assertEqual(len(response.data['results'][0]['roles']), 2)
self.assertEqual(response.data['results'][0]['roles'][0]['id'], group_profile1.id)
self.assertEqual(response.data['results'][0]['roles'][0]['name'], 'role1')
self.assertEqual(response.data['results'][0]['roles'][1]['id'], group_profile2.id)
self.assertEqual(response.data['results'][0]['roles'][1]['name'], 'role2')
self.assertEqual(len(response.data['results'][1]['roles']), 1)
self.assertEqual(response.data['results'][1]['roles'][0]['id'], group_profile1.id)
self.assertEqual(response.data['results'][1]['roles'][0]['name'], 'role1')
self.assertEqual(len(response.data['results'][2]['roles']), 0)
def test_user_list_get_with_has_organization_filter(self):
test_uri = self.users_base_uri test_uri = self.users_base_uri
users = [] users = []
# create a 7 new users # create a 7 new users
...@@ -344,6 +403,73 @@ class UsersApiTests(ModuleStoreTestCase): ...@@ -344,6 +403,73 @@ class UsersApiTests(ModuleStoreTestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertGreaterEqual(len(response.data['results']), 4) self.assertGreaterEqual(len(response.data['results']), 4)
def test_user_list_get_with_organizations_filter(self):
test_uri = self.users_base_uri
# create a 8 new users
users = UserFactory.create_batch(8)
# create organization and add 4 users to it
organizations = []
for i in xrange(2):
organization = Organization.objects.create(
name='Test Organization{}'.format(i),
display_name='Test Org Display Name{}'.format(i),
)
organizations.append(organization)
organizations[0].users.add(*users)
organizations[1].users.add(*users[:4])
# fetch users for organization 1
response = self.do_get('{}?organizations={}'.format(test_uri, organizations[1].id))
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data['results']), 4)
self.assertIsNotNone(response.data['results'][0]['is_active'])
# fetch users in multiple organization
organization_ids = ','.join([str(organization.id) for organization in organizations])
response = self.do_get('{}?organizations={}'.format(test_uri, organization_ids))
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data['results']), 8)
self.assertIsNotNone(response.data['results'][0]['is_active'])
def test_user_list_get_with_course_enrollment_filter(self):
test_uri = self.users_base_uri
# create a 8 new users
users = UserFactory.create_batch(8)
# create course enrollments
for user in users[:4]:
CourseEnrollmentFactory.create(user=user, course_id=self.course.id)
for user in users:
CourseEnrollmentFactory.create(user=user, course_id=self.course2.id)
# fetch users enrolled in course 1
response = self.do_get('{}?courses={}'.format(test_uri, self.course.id))
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data['results']), 4)
self.assertIsNotNone(response.data['results'][0]['is_active'])
# fetch users enrolled in course 1 and 2
response = self.do_get('{}?courses={},{}'.format(test_uri, self.course.id, self.course2.id))
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data['results']), 8)
self.assertIsNotNone(response.data['results'][0]['is_active'])
def test_user_list_get_with_name_filter(self):
test_uri = self.users_base_uri
# create a 8 new users
users = UserFactory.create_batch(2)
users.append(UserFactory.create_batch(2, first_name="John", last_name="Doe"))
# fetch users by name
response = self.do_get('{}?name=John Doe'.format(test_uri))
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data['results']), 2)
self.assertEqual(response.data['results'][0]['first_name'], 'John')
self.assertEqual(response.data['results'][0]['last_name'], 'Doe')
def test_user_list_post(self): def test_user_list_post(self):
test_uri = self.users_base_uri test_uri = self.users_base_uri
local_username = self.test_username + str(randint(11, 99)) local_username = self.test_username + str(randint(11, 99))
......
...@@ -209,14 +209,19 @@ class UsersList(SecureListAPIView): ...@@ -209,14 +209,19 @@ class UsersList(SecureListAPIView):
""" """
### 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, has_organizations and id filters - GET: Provides paginated list of users, it supports email, username, name, organizations, courses enrolled,
has_organizations and id filters
Possible use cases Possible use cases
GET /api/users?ids=23 GET /api/users?ids=23
GET /api/users?ids=11,12,13&page=2 GET /api/users?ids=11,12,13&page=2
GET /api/users?organizations=1,2,3
GET /api/users?courses={course_id},{course_id2}
GET /api/users?email={john@example.com} GET /api/users?email={john@example.com}
GET /api/users?name={john doe}
GET /api/users?username={john} GET /api/users?username={john}
* email: string, filters user set by email address * email: string, filters user set by email address
* username: string, filters user set by username * username: string, filters user set by username
* name: string, filters user set by full name
GET /api/users?has_organizations={true} GET /api/users?has_organizations={true}
* has_organizations: boolean, filters user set with organization association * has_organizations: boolean, filters user set with organization association
GET /api/users?has_organizations={false} GET /api/users?has_organizations={false}
...@@ -269,18 +274,36 @@ class UsersList(SecureListAPIView): ...@@ -269,18 +274,36 @@ class UsersList(SecureListAPIView):
filter_backends = (filters.DjangoFilterBackend, IdsInFilterBackend, HasOrgsFilterBackend) filter_backends = (filters.DjangoFilterBackend, IdsInFilterBackend, HasOrgsFilterBackend)
filter_fields = ('email', 'username', ) filter_fields = ('email', 'username', )
def get_queryset(self):
"""
Optionally filter users by organizations and course enrollments
"""
queryset = self.queryset
org_ids = self.request.QUERY_PARAMS.get('organizations', None)
if org_ids is not None:
org_ids = map(int, org_ids.split(','))
queryset = queryset.filter(organizations__id__in=org_ids).distinct()
course_ids = self.request.QUERY_PARAMS.get('courses', None)
if course_ids is not None:
course_ids = map(CourseKey.from_string, course_ids.split(','))
queryset = queryset.filter(courseenrollment__course_id__in=course_ids).distinct()
name = self.request.QUERY_PARAMS.get('name', None)
if name is not None:
queryset = queryset.filter(profile__name=name)
queryset = queryset.prefetch_related('organizations')\
.select_related('courseenrollment_set', 'profile')\
.annotate(courses_enrolled=Count('courseenrollment'))
return queryset
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
""" """
GET /api/users?ids=11,12,13.....&page=2 GET /api/users?ids=11,12,13.....&page=2
""" """
email = request.QUERY_PARAMS.get('email', None) return self.list(request, *args, **kwargs)
username = request.QUERY_PARAMS.get('username', None)
ids = request.QUERY_PARAMS.get('ids', None)
has_orgs = request.QUERY_PARAMS.get('has_organizations', None)
if email or username or ids or has_orgs:
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