diff --git a/common/djangoapps/user_api/models.py b/common/djangoapps/user_api/models.py index 36ed30eddc3b8921be36d214c05d40b5a37fb234..76b8cd5053f4f0ce349962ba66ab7047b80d18fa 100644 --- a/common/djangoapps/user_api/models.py +++ b/common/djangoapps/user_api/models.py @@ -1,11 +1,13 @@ from django.contrib.auth.models import User +from django.core.validators import RegexValidator from django.db import models class UserPreference(models.Model): """A user's preference, stored as generic text to be processed by client""" - user = models.ForeignKey(User, db_index=True, related_name="+") - key = models.CharField(max_length=255, db_index=True) + KEY_REGEX = r"[-_a-zA-Z0-9]+" + user = models.ForeignKey(User, db_index=True, related_name="preferences") + key = models.CharField(max_length=255, db_index=True, validators=[RegexValidator(KEY_REGEX)]) value = models.TextField() class Meta: # pylint: disable=missing-docstring diff --git a/common/djangoapps/user_api/serializers.py b/common/djangoapps/user_api/serializers.py index 8822817933330d7d221c49f62c5ee315576c433d..edd9b1e7ccb8fc08759060c6252f0680a0681885 100644 --- a/common/djangoapps/user_api/serializers.py +++ b/common/djangoapps/user_api/serializers.py @@ -6,15 +6,19 @@ from user_api.models import UserPreference class UserSerializer(serializers.HyperlinkedModelSerializer): name = serializers.SerializerMethodField("get_name") + preferences = serializers.SerializerMethodField("get_preferences") def get_name(self, user): profile = UserProfile.objects.get(user=user) return profile.name + def get_preferences(self, user): + return dict([(pref.key, pref.value) for pref in user.preferences.all()]) + class Meta: model = User # This list is the minimal set required by the notification service - fields = ("id", "email", "name", "username") + fields = ("id", "email", "name", "username", "preferences") read_only_fields = ("id", "email", "username") diff --git a/common/djangoapps/user_api/tests/test_views.py b/common/djangoapps/user_api/tests/test_views.py index 4143a467d796c28f1ee271ba25f620a9273e6a18..d187b21b9466ac87323a875cbf0ff0397e34edee 100644 --- a/common/djangoapps/user_api/tests/test_views.py +++ b/common/djangoapps/user_api/tests/test_views.py @@ -66,7 +66,11 @@ class ApiTestCase(TestCase): def assertUserIsValid(self, user): """Assert that the given user result is valid""" - self.assertItemsEqual(user.keys(), ["email", "id", "name", "username", "url"]) + self.assertItemsEqual(user.keys(), ["email", "id", "name", "username", "preferences", "url"]) + self.assertItemsEqual( + user["preferences"].items(), + [(pref.key, pref.value) for pref in self.prefs if pref.user.id == user["id"]] + ) self.assertSelfReferential(user) def assertPrefIsValid(self, pref): @@ -221,6 +225,11 @@ class UserViewSetTest(UserApiTestCase): "id": user.id, "name": user.profile.name, "username": user.username, + "preferences": dict([ + (user_pref.key, user_pref.value) + for user_pref in self.prefs + if user_pref.user == user + ]), "url": uri } ) @@ -352,6 +361,11 @@ class UserPreferenceViewSetTest(UserApiTestCase): "id": pref.user.id, "name": pref.user.profile.name, "username": pref.user.username, + "preferences": dict([ + (user_pref.key, user_pref.value) + for user_pref in self.prefs + if user_pref.user == pref.user + ]), "url": self.get_uri_for_user(pref.user), }, "key": pref.key, @@ -359,3 +373,59 @@ class UserPreferenceViewSetTest(UserApiTestCase): "url": uri, } ) + + +class PreferenceUsersListViewTest(UserApiTestCase): + LIST_URI = "/user_api/v1/preferences/key0/users/" + + def test_options(self): + self.assertAllowedMethods(self.LIST_URI, ["OPTIONS", "GET", "HEAD"]) + + def test_put_not_allowed(self): + self.assertHttpMethodNotAllowed(self.request_with_auth("put", self.LIST_URI)) + + def test_patch_not_allowed(self): + raise SkipTest("Django 1.4's test client does not support patch") + + def test_delete_not_allowed(self): + self.assertHttpMethodNotAllowed(self.request_with_auth("delete", self.LIST_URI)) + + def test_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)) + + def test_get_basic(self): + result = self.get_json(self.LIST_URI) + self.assertEqual(result["count"], 2) + self.assertIsNone(result["next"]) + self.assertIsNone(result["previous"]) + users = result["results"] + self.assertEqual(len(users), 2) + for user in users: + self.assertUserIsValid(user) + + def test_get_pagination(self): + first_page = self.get_json(self.LIST_URI, data={"page_size": 1}) + self.assertEqual(first_page["count"], 2) + first_page_next_uri = first_page["next"] + self.assertIsNone(first_page["previous"]) + first_page_users = first_page["results"] + self.assertEqual(len(first_page_users), 1) + + second_page = self.get_json(first_page_next_uri) + self.assertEqual(second_page["count"], 2) + self.assertIsNone(second_page["next"]) + second_page_prev_uri = second_page["previous"] + second_page_users = second_page["results"] + self.assertEqual(len(second_page_users), 1) + + 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)), 2) diff --git a/common/djangoapps/user_api/urls.py b/common/djangoapps/user_api/urls.py index de24b67f02de0c61444b64456687240a91f34fc8..c9d86c96207f1529806b184a7f06fd477d398dd4 100644 --- a/common/djangoapps/user_api/urls.py +++ b/common/djangoapps/user_api/urls.py @@ -1,6 +1,7 @@ from django.conf.urls import include, patterns, url from rest_framework import routers from user_api import views as user_api_views +from user_api.models import UserPreference user_api_router = routers.DefaultRouter() @@ -9,4 +10,8 @@ user_api_router.register(r'user_prefs', user_api_views.UserPreferenceViewSet) urlpatterns = patterns( '', url(r'^v1/', include(user_api_router.urls)), + url( + r'^v1/preferences/(?P<pref_key>{})/users/$'.format(UserPreference.KEY_REGEX), + user_api_views.PreferenceUsersListView.as_view() + ), ) diff --git a/common/djangoapps/user_api/views.py b/common/djangoapps/user_api/views.py index 23066920d8e320956da14ff523604caf892b60f2..53bb99b38c37406a48769e020cfc7e7e17618ca6 100644 --- a/common/djangoapps/user_api/views.py +++ b/common/djangoapps/user_api/views.py @@ -2,6 +2,7 @@ from django.conf import settings from django.contrib.auth.models import User from rest_framework import authentication from rest_framework import filters +from rest_framework import generics from rest_framework import permissions from rest_framework import viewsets from user_api.serializers import UserSerializer, UserPreferenceSerializer @@ -28,7 +29,7 @@ class ApiKeyHeaderPermission(permissions.BasePermission): class UserViewSet(viewsets.ReadOnlyModelViewSet): authentication_classes = (authentication.SessionAuthentication,) permission_classes = (ApiKeyHeaderPermission,) - queryset = User.objects.all() + queryset = User.objects.all().prefetch_related("preferences") serializer_class = UserSerializer paginate_by = 10 paginate_by_param = "page_size" @@ -43,3 +44,14 @@ class UserPreferenceViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = UserPreferenceSerializer paginate_by = 10 paginate_by_param = "page_size" + + +class PreferenceUsersListView(generics.ListAPIView): + authentication_classes = (authentication.SessionAuthentication,) + permission_classes = (ApiKeyHeaderPermission,) + serializer_class = UserSerializer + paginate_by = 10 + paginate_by_param = "page_size" + + def get_queryset(self): + return User.objects.filter(preferences__key=self.kwargs["pref_key"]).prefetch_related("preferences")