diff --git a/AUTHORS b/AUTHORS index 9249c6353be41a017d92a03877a616600b3eb054..4db59127e8d1d275727653f47f02c50ef76622ec 100644 --- a/AUTHORS +++ b/AUTHORS @@ -148,3 +148,4 @@ Sébastien Hinderer <Sebastien.Hinderer@inria.fr> Kristin Stephens <ksteph@cs.berkeley.edu> Ben Patterson <bpatterson@edx.org> Luis Duarte <lduarte1991@gmail.com> +Steven Burch <stv@stanford.edu> diff --git a/common/djangoapps/user_api/tests/test_views.py b/common/djangoapps/user_api/tests/test_views.py index d187b21b9466ac87323a875cbf0ff0397e34edee..2cc187599e4f8596fcb6f9c1aec7459aff9f78b6 100644 --- a/common/djangoapps/user_api/tests/test_views.py +++ b/common/djangoapps/user_api/tests/test_views.py @@ -8,11 +8,14 @@ from student.tests.factories import UserFactory from unittest import SkipTest from user_api.models import UserPreference from user_api.tests.factories import UserPreferenceFactory +from django_comment_common import models +from xmodule.modulestore.locations import SlashSeparatedCourseKey TEST_API_KEY = "test_api_key" USER_LIST_URI = "/user_api/v1/users/" USER_PREFERENCE_LIST_URI = "/user_api/v1/user_prefs/" +ROLE_LIST_URI = "/user_api/v1/forum_roles/Moderator/users/" @override_settings(EDX_API_KEY=TEST_API_KEY) @@ -104,6 +107,20 @@ class EmptyUserTestCase(ApiTestCase): self.assertEqual(result["results"], []) +class EmptyRoleTestCase(ApiTestCase): + """Test that the endpoint supports empty result sets""" + course_id = SlashSeparatedCourseKey.from_deprecated_string("org/course/run") + LIST_URI = ROLE_LIST_URI + "?course_id=" + course_id.to_deprecated_string() + + def test_get_list_empty(self): + """Test that the endpoint properly returns empty result sets""" + result = self.get_json(self.LIST_URI) + self.assertEqual(result["count"], 0) + self.assertIsNone(result["next"]) + self.assertIsNone(result["previous"]) + self.assertEqual(result["results"], []) + + class UserApiTestCase(ApiTestCase): def setUp(self): super(UserApiTestCase, self).setUp() @@ -121,6 +138,92 @@ class UserApiTestCase(ApiTestCase): ] +class RoleTestCase(UserApiTestCase): + course_id = SlashSeparatedCourseKey.from_deprecated_string("org/course/run") + LIST_URI = ROLE_LIST_URI + "?course_id=" + course_id.to_deprecated_string() + + def setUp(self): + super(RoleTestCase, self).setUp() + (role, _) = models.Role.objects.get_or_create( + name=models.FORUM_ROLE_MODERATOR, + course_id=self.course_id + ) + for user in self.users: + user.roles.add(role) + + def test_options_list(self): + self.assertAllowedMethods(self.LIST_URI, ["OPTIONS", "GET", "HEAD"]) + + def test_post_list_not_allowed(self): + self.assertHttpMethodNotAllowed(self.request_with_auth("post", self.LIST_URI)) + + def test_put_list_not_allowed(self): + self.assertHttpMethodNotAllowed(self.request_with_auth("put", self.LIST_URI)) + + def test_patch_list_not_allowed(self): + raise SkipTest("Django 1.4's test client does not support patch") + + def test_delete_list_not_allowed(self): + self.assertHttpMethodNotAllowed(self.request_with_auth("delete", self.LIST_URI)) + + def test_list_unauthorized(self): + self.assertHttpForbidden(self.client.get(self.LIST_URI)) + + @override_settings(DEBUG=True) + @override_settings(EDX_API_KEY=None) + def test_debug_auth(self): + self.assertHttpOK(self.client.get(self.LIST_URI)) + + @override_settings(DEBUG=False) + @override_settings(EDX_API_KEY=TEST_API_KEY) + def test_basic_auth(self): + # ensure that having basic auth headers in the mix does not break anything + self.assertHttpOK( + self.request_with_auth("get", self.LIST_URI, + **self.basic_auth("someuser", "somepass"))) + self.assertHttpForbidden( + self.client.get(self.LIST_URI, **self.basic_auth("someuser", "somepass"))) + + def test_get_list_nonempty(self): + result = self.get_json(self.LIST_URI) + users = result["results"] + self.assertEqual(result["count"], len(self.users)) + self.assertEqual(len(users), len(self.users)) + self.assertIsNone(result["next"]) + self.assertIsNone(result["previous"]) + for user in users: + self.assertUserIsValid(user) + + def test_required_parameter(self): + response = self.request_with_auth("get", ROLE_LIST_URI) + self.assertHttpBadRequest(response) + + def test_get_list_pagination(self): + first_page = self.get_json(self.LIST_URI, data={ + "page_size": 3, + "course_id": self.course_id.to_deprecated_string(), + }) + self.assertEqual(first_page["count"], 5) + first_page_next_uri = first_page["next"] + self.assertIsNone(first_page["previous"]) + first_page_users = first_page["results"] + self.assertEqual(len(first_page_users), 3) + + second_page = self.get_json(first_page_next_uri) + self.assertEqual(second_page["count"], 5) + self.assertIsNone(second_page["next"]) + second_page_prev_uri = second_page["previous"] + second_page_users = second_page["results"] + self.assertEqual(len(second_page_users), 2) + + self.assertEqual(self.get_json(second_page_prev_uri), first_page) + + for user in first_page_users + second_page_users: + self.assertUserIsValid(user) + all_user_uris = [user["url"] for user in first_page_users + second_page_users] + self.assertEqual(len(set(all_user_uris)), 5) + + class UserViewSetTest(UserApiTestCase): LIST_URI = USER_LIST_URI diff --git a/common/djangoapps/user_api/urls.py b/common/djangoapps/user_api/urls.py index c9d86c96207f1529806b184a7f06fd477d398dd4..9fd20194eae1003d91c4a306762ce79c8d5b00c5 100644 --- a/common/djangoapps/user_api/urls.py +++ b/common/djangoapps/user_api/urls.py @@ -14,4 +14,8 @@ urlpatterns = patterns( r'^v1/preferences/(?P<pref_key>{})/users/$'.format(UserPreference.KEY_REGEX), user_api_views.PreferenceUsersListView.as_view() ), + url( + r'^v1/forum_roles/(?P<name>[a-zA-Z]+)/users/$', + user_api_views.ForumRoleUsersListView.as_view() + ), ) diff --git a/common/djangoapps/user_api/views.py b/common/djangoapps/user_api/views.py index 53bb99b38c37406a48769e020cfc7e7e17618ca6..c75db2e177df03f9615aed40dd7f47d72a333536 100644 --- a/common/djangoapps/user_api/views.py +++ b/common/djangoapps/user_api/views.py @@ -4,9 +4,14 @@ from rest_framework import authentication from rest_framework import filters from rest_framework import generics from rest_framework import permissions +from rest_framework import status from rest_framework import viewsets +from rest_framework.exceptions import ParseError +from rest_framework.response import Response from user_api.serializers import UserSerializer, UserPreferenceSerializer from user_api.models import UserPreference +from django_comment_common.models import Role +from xmodule.modulestore.locations import SlashSeparatedCourseKey class ApiKeyHeaderPermission(permissions.BasePermission): @@ -35,6 +40,30 @@ class UserViewSet(viewsets.ReadOnlyModelViewSet): paginate_by_param = "page_size" +class ForumRoleUsersListView(generics.ListAPIView): + """ + Forum roles are represented by a list of user dicts + """ + authentication_classes = (authentication.SessionAuthentication,) + permission_classes = (ApiKeyHeaderPermission,) + serializer_class = UserSerializer + paginate_by = 10 + paginate_by_param = "page_size" + + def get_queryset(self): + """ + Return a list of users with the specified role/course pair + """ + name = self.kwargs['name'] + course_id_string = self.request.QUERY_PARAMS.get('course_id') + if not course_id_string: + raise ParseError('course_id must be specified') + course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id_string) + role = Role.objects.get_or_create(course_id=course_id, name=name)[0] + users = role.users.all() + return users + + class UserPreferenceViewSet(viewsets.ReadOnlyModelViewSet): authentication_classes = (authentication.SessionAuthentication,) permission_classes = (ApiKeyHeaderPermission,)