Merge pull request #5347 from edx/gprice/notifier-api-cohorts
Add a new user API for the notifier
This commit is contained in:
0
lms/djangoapps/notifier_api/__init__.py
Normal file
0
lms/djangoapps/notifier_api/__init__.py
Normal file
1
lms/djangoapps/notifier_api/models.py
Normal file
1
lms/djangoapps/notifier_api/models.py
Normal file
@@ -0,0 +1 @@
|
||||
# Django requires this module.
|
||||
64
lms/djangoapps/notifier_api/serializers.py
Normal file
64
lms/djangoapps/notifier_api/serializers.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from django.contrib.auth.models import User
|
||||
from rest_framework import serializers
|
||||
|
||||
from course_groups.cohorts import is_course_cohorted
|
||||
from notification_prefs import NOTIFICATION_PREF_KEY
|
||||
from lang_pref import LANGUAGE_KEY
|
||||
|
||||
|
||||
class NotifierUserSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
A serializer containing all information about a user needed by the notifier
|
||||
(namely the user's name, email address, notification and language
|
||||
preferences, and course enrollment and cohort information).
|
||||
|
||||
Because these pieces of information reside in different tables, this is
|
||||
designed to work well with prefetch_related and select_related, which
|
||||
require the use of all() instead of get() or filter(). The following fields
|
||||
should be prefetched on the user objects being serialized:
|
||||
* profile
|
||||
* preferences
|
||||
* courseenrollment_set
|
||||
* course_groups
|
||||
* roles__permissions
|
||||
"""
|
||||
name = serializers.SerializerMethodField("get_name")
|
||||
preferences = serializers.SerializerMethodField("get_preferences")
|
||||
course_info = serializers.SerializerMethodField("get_course_info")
|
||||
|
||||
def get_name(self, user):
|
||||
return user.profile.name
|
||||
|
||||
def get_preferences(self, user):
|
||||
return {
|
||||
pref.key: pref.value
|
||||
for pref
|
||||
in user.preferences.all()
|
||||
if pref.key in [LANGUAGE_KEY, NOTIFICATION_PREF_KEY]
|
||||
}
|
||||
|
||||
def get_course_info(self, user):
|
||||
cohort_id_map = {
|
||||
cohort.course_id: cohort.id
|
||||
for cohort in user.course_groups.all()
|
||||
}
|
||||
see_all_cohorts_set = {
|
||||
role.course_id
|
||||
for role in user.roles.all()
|
||||
for perm in role.permissions.all() if perm.name == "see_all_cohorts"
|
||||
}
|
||||
return {
|
||||
unicode(enrollment.course_id): {
|
||||
"cohort_id": cohort_id_map.get(enrollment.course_id),
|
||||
"see_all_cohorts": (
|
||||
enrollment.course_id in see_all_cohorts_set or
|
||||
not is_course_cohorted(enrollment.course_id)
|
||||
),
|
||||
}
|
||||
for enrollment in user.courseenrollment_set.all() if enrollment.is_active
|
||||
}
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ("id", "email", "name", "preferences", "course_info")
|
||||
read_only_fields = ("id", "email")
|
||||
246
lms/djangoapps/notifier_api/tests.py
Normal file
246
lms/djangoapps/notifier_api/tests.py
Normal file
@@ -0,0 +1,246 @@
|
||||
import itertools
|
||||
|
||||
import ddt
|
||||
from django.conf import settings
|
||||
from django.test.client import RequestFactory
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from course_groups.models import CourseUserGroup
|
||||
from django_comment_common.models import Role, Permission
|
||||
from lang_pref import LANGUAGE_KEY
|
||||
from notification_prefs import NOTIFICATION_PREF_KEY
|
||||
from notifier_api.views import NotifierUsersViewSet
|
||||
from student.models import CourseEnrollment
|
||||
from student.tests.factories import UserFactory, CourseEnrollmentFactory
|
||||
from user_api.models import UserPreference
|
||||
from user_api.tests.factories import UserPreferenceFactory
|
||||
from util.testing import UrlResetMixin
|
||||
from xmodule.modulestore.tests.django_utils import mixed_store_config, ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@override_settings(EDX_API_KEY="test_api_key")
|
||||
class NotifierUsersViewSetTest(UrlResetMixin, ModuleStoreTestCase):
|
||||
def setUp(self):
|
||||
super(NotifierUsersViewSetTest, self).setUp()
|
||||
self.courses = []
|
||||
self.cohorts = []
|
||||
self.user = UserFactory()
|
||||
self.notification_pref = UserPreferenceFactory(
|
||||
user=self.user,
|
||||
key=NOTIFICATION_PREF_KEY,
|
||||
value="notification pref test value"
|
||||
)
|
||||
|
||||
self.list_view = NotifierUsersViewSet.as_view({"get": "list"})
|
||||
self.detail_view = NotifierUsersViewSet.as_view({"get": "retrieve"})
|
||||
|
||||
def _set_up_course(self, is_course_cohorted, is_user_cohorted, is_moderator):
|
||||
cohort_config = {"cohorted": True} if is_course_cohorted else {}
|
||||
course = CourseFactory(
|
||||
number=("TestCourse{}".format(len(self.courses))),
|
||||
cohort_config=cohort_config
|
||||
)
|
||||
self.courses.append(course)
|
||||
CourseEnrollmentFactory(user=self.user, course_id=course.id)
|
||||
if is_user_cohorted:
|
||||
cohort = CourseUserGroup.objects.create(
|
||||
name="Test Cohort",
|
||||
course_id=course.id,
|
||||
group_type=CourseUserGroup.COHORT
|
||||
)
|
||||
cohort.users.add(self.user)
|
||||
self.cohorts.append(cohort)
|
||||
if is_moderator:
|
||||
moderator_perm, _ = Permission.objects.get_or_create(name="see_all_cohorts")
|
||||
moderator_role = Role.objects.create(name="Moderator", course_id=course.id)
|
||||
moderator_role.permissions.add(moderator_perm)
|
||||
self.user.roles.add(moderator_role)
|
||||
|
||||
def _assert_basic_user_info_correct(self, user, result_user):
|
||||
self.assertEqual(result_user["id"], user.id)
|
||||
self.assertEqual(result_user["email"], user.email)
|
||||
self.assertEqual(result_user["name"], user.profile.name)
|
||||
|
||||
def test_without_api_key(self):
|
||||
request = RequestFactory().get("dummy")
|
||||
for view in [self.list_view, self.detail_view]:
|
||||
response = view(request)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
# Detail view tests
|
||||
|
||||
def _make_detail_request(self):
|
||||
request = RequestFactory().get("dummy", HTTP_X_EDX_API_KEY=settings.EDX_API_KEY)
|
||||
return self.detail_view(
|
||||
request,
|
||||
**{NotifierUsersViewSet.lookup_field: str(self.user.id)}
|
||||
)
|
||||
|
||||
def _get_detail(self):
|
||||
response = self._make_detail_request()
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(
|
||||
set(response.data.keys()),
|
||||
{"id", "email", "name", "preferences", "course_info"}
|
||||
)
|
||||
return response.data
|
||||
|
||||
def test_detail_invalid_user(self):
|
||||
UserPreference.objects.all().delete()
|
||||
response = self._make_detail_request()
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_basic_user_info(self):
|
||||
result = self._get_detail()
|
||||
self._assert_basic_user_info_correct(self.user, result)
|
||||
|
||||
def test_course_info(self):
|
||||
expected_course_info = {}
|
||||
for is_course_cohorted, is_user_cohorted, is_moderator in (
|
||||
itertools.product([True, False], [True, False], [True, False])
|
||||
):
|
||||
self._set_up_course(is_course_cohorted, is_user_cohorted, is_moderator)
|
||||
expected_course_info[unicode(self.courses[-1].id)] = {
|
||||
"cohort_id": self.cohorts[-1].id if is_user_cohorted else None,
|
||||
"see_all_cohorts": is_moderator or not is_course_cohorted
|
||||
}
|
||||
result = self._get_detail()
|
||||
self.assertEqual(result["course_info"], expected_course_info)
|
||||
|
||||
def test_course_info_unenrolled(self):
|
||||
self._set_up_course(False, False, False)
|
||||
course_id = self.courses[0].id
|
||||
CourseEnrollment.unenroll(self.user, course_id)
|
||||
result = self._get_detail()
|
||||
self.assertNotIn(unicode(course_id), result["course_info"])
|
||||
|
||||
def test_course_info_no_enrollments(self):
|
||||
result = self._get_detail()
|
||||
self.assertEqual(result["course_info"], {})
|
||||
|
||||
def test_preferences(self):
|
||||
lang_pref = UserPreferenceFactory(
|
||||
user=self.user,
|
||||
key=LANGUAGE_KEY,
|
||||
value="language pref test value"
|
||||
)
|
||||
UserPreferenceFactory(user=self.user, key="non_included_key")
|
||||
result = self._get_detail()
|
||||
self.assertEqual(
|
||||
result["preferences"],
|
||||
{
|
||||
NOTIFICATION_PREF_KEY: self.notification_pref.value,
|
||||
LANGUAGE_KEY: lang_pref.value,
|
||||
}
|
||||
)
|
||||
|
||||
# List view tests
|
||||
|
||||
def _make_list_request(self, page, page_size):
|
||||
request = RequestFactory().get(
|
||||
"dummy",
|
||||
{"page": page, "page_size": page_size},
|
||||
HTTP_X_EDX_API_KEY=settings.EDX_API_KEY
|
||||
)
|
||||
return self.list_view(request)
|
||||
|
||||
def _get_list(self, page=1, page_size=None):
|
||||
response = self._make_list_request(page, page_size)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(
|
||||
set(response.data.keys()),
|
||||
{"count", "next", "previous", "results"}
|
||||
)
|
||||
return response.data["results"]
|
||||
|
||||
def test_no_users(self):
|
||||
UserPreference.objects.all().delete()
|
||||
results = self._get_list()
|
||||
self.assertEqual(len(results), 0)
|
||||
|
||||
def test_multiple_users(self):
|
||||
other_user = UserFactory()
|
||||
other_notification_pref = UserPreferenceFactory(
|
||||
user=other_user,
|
||||
key=NOTIFICATION_PREF_KEY,
|
||||
value="other value"
|
||||
)
|
||||
self._set_up_course(is_course_cohorted=True, is_user_cohorted=True, is_moderator=False)
|
||||
self._set_up_course(is_course_cohorted=False, is_user_cohorted=False, is_moderator=False)
|
||||
# Users have different sets of enrollments
|
||||
CourseEnrollmentFactory(user=other_user, course_id=self.courses[0].id)
|
||||
|
||||
result_map = {result["id"]: result for result in self._get_list()}
|
||||
self.assertEqual(set(result_map.keys()), {self.user.id, other_user.id})
|
||||
for user in [self.user, other_user]:
|
||||
self._assert_basic_user_info_correct(user, result_map[user.id])
|
||||
self.assertEqual(
|
||||
result_map[self.user.id]["preferences"],
|
||||
{NOTIFICATION_PREF_KEY: self.notification_pref.value}
|
||||
)
|
||||
self.assertEqual(
|
||||
result_map[other_user.id]["preferences"],
|
||||
{NOTIFICATION_PREF_KEY: other_notification_pref.value}
|
||||
)
|
||||
self.assertEqual(
|
||||
result_map[self.user.id]["course_info"],
|
||||
{
|
||||
unicode(self.courses[0].id): {
|
||||
"cohort_id": self.cohorts[0].id,
|
||||
"see_all_cohorts": False,
|
||||
},
|
||||
unicode(self.courses[1].id): {
|
||||
"cohort_id": None,
|
||||
"see_all_cohorts": True,
|
||||
},
|
||||
}
|
||||
)
|
||||
self.assertEqual(
|
||||
result_map[other_user.id]["course_info"],
|
||||
{
|
||||
unicode(self.courses[0].id): {
|
||||
"cohort_id": None,
|
||||
"see_all_cohorts": False,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@ddt.data(
|
||||
3, # Factor of num of results
|
||||
5, # Non-factor of num of results
|
||||
12, # Num of results
|
||||
15 # More than num of results
|
||||
)
|
||||
def test_pagination(self, page_size):
|
||||
num_users = 12
|
||||
users = [self.user]
|
||||
while len(users) < num_users:
|
||||
new_user = UserFactory()
|
||||
users.append(new_user)
|
||||
UserPreferenceFactory(user=new_user, key=NOTIFICATION_PREF_KEY)
|
||||
|
||||
num_pages = (num_users - 1) / page_size + 1
|
||||
result_list = []
|
||||
for i in range(1, num_pages + 1):
|
||||
result_list.extend(self._get_list(page=i, page_size=page_size))
|
||||
result_map = {result["id"]: result for result in result_list}
|
||||
|
||||
self.assertEqual(len(result_list), num_users)
|
||||
for user in users:
|
||||
self._assert_basic_user_info_correct(user, result_map[user.id])
|
||||
self.assertEqual(
|
||||
self._make_list_request(page=(num_pages + 1), page_size=page_size).status_code,
|
||||
404
|
||||
)
|
||||
|
||||
def test_db_access(self):
|
||||
for _ in range(10):
|
||||
new_user = UserFactory()
|
||||
UserPreferenceFactory(user=new_user, key=NOTIFICATION_PREF_KEY)
|
||||
|
||||
# The number of queries is one for the users plus one for each prefetch
|
||||
# in NotifierUsersViewSet (roles__permissions does one for each table).
|
||||
with self.assertNumQueries(6):
|
||||
self._get_list()
|
||||
12
lms/djangoapps/notifier_api/urls.py
Normal file
12
lms/djangoapps/notifier_api/urls.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from django.conf.urls import include, patterns, url
|
||||
from rest_framework import routers
|
||||
|
||||
from notifier_api.views import NotifierUsersViewSet
|
||||
|
||||
|
||||
notifier_api_router = routers.DefaultRouter()
|
||||
notifier_api_router.register(r'users', NotifierUsersViewSet, base_name="notifier_users")
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
url(r'^v1/', include(notifier_api_router.urls)),
|
||||
)
|
||||
30
lms/djangoapps/notifier_api/views.py
Normal file
30
lms/djangoapps/notifier_api/views.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from django.contrib.auth.models import User
|
||||
from rest_framework.viewsets import ReadOnlyModelViewSet
|
||||
|
||||
from notification_prefs import NOTIFICATION_PREF_KEY
|
||||
from notifier_api.serializers import NotifierUserSerializer
|
||||
from user_api.views import ApiKeyHeaderPermission
|
||||
|
||||
|
||||
class NotifierUsersViewSet(ReadOnlyModelViewSet):
|
||||
"""
|
||||
An endpoint that the notifier can use to retrieve users who have enabled
|
||||
daily forum digests, including all information that the notifier needs about
|
||||
such users.
|
||||
"""
|
||||
permission_classes = (ApiKeyHeaderPermission,)
|
||||
serializer_class = NotifierUserSerializer
|
||||
paginate_by = 10
|
||||
paginate_by_param = "page_size"
|
||||
|
||||
# See NotifierUserSerializer for notes about related tables
|
||||
queryset = User.objects.filter(
|
||||
preferences__key=NOTIFICATION_PREF_KEY
|
||||
).select_related(
|
||||
"profile"
|
||||
).prefetch_related(
|
||||
"preferences",
|
||||
"courseenrollment_set",
|
||||
"course_groups",
|
||||
"roles__permissions"
|
||||
)
|
||||
@@ -1417,6 +1417,8 @@ INSTALLED_APPS = (
|
||||
# Notification preferences setting
|
||||
'notification_prefs',
|
||||
|
||||
'notifier_api',
|
||||
|
||||
# Different Course Modes
|
||||
'course_modes',
|
||||
|
||||
|
||||
@@ -62,6 +62,8 @@ urlpatterns = ('', # nopep8
|
||||
|
||||
url(r'^user_api/', include('user_api.urls')),
|
||||
|
||||
url(r'^notifier_api/', include('notifier_api.urls')),
|
||||
|
||||
url(r'^lang_pref/', include('lang_pref.urls')),
|
||||
|
||||
url(r'^i18n/', include('django.conf.urls.i18n')),
|
||||
|
||||
Reference in New Issue
Block a user