From e4df2c2ceb3a1830ae665ec3ef2af42b9a308986 Mon Sep 17 00:00:00 2001
From: stv <stv@stanford.edu>
Date: Mon, 5 May 2014 15:50:13 -0700
Subject: [PATCH] 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.
---
 AUTHORS                                       |   1 +
 .../djangoapps/user_api/tests/test_views.py   | 103 ++++++++++++++++++
 common/djangoapps/user_api/urls.py            |   4 +
 common/djangoapps/user_api/views.py           |  29 +++++
 4 files changed, 137 insertions(+)

diff --git a/AUTHORS b/AUTHORS
index 9249c6353be..4db59127e8d 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 d187b21b946..2cc187599e4 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 c9d86c96207..9fd20194eae 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 53bb99b38c3..c75db2e177d 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,)
-- 
GitLab