Add 'forum_roles' endpoint to user_api
Expose a new endpoint to query a list of users based on a role name (Moderator, Student, etc.) and a course_id. This will initially be used by the notifier to send daily digest messages to forum moderators.
This commit is contained in:
1
AUTHORS
1
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>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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,)
|
||||
|
||||
Reference in New Issue
Block a user