From 6a97ddf53cebf1cdb84b3d892724084c050e363a Mon Sep 17 00:00:00 2001 From: Greg Price Date: Wed, 3 Jul 2013 10:04:22 -0400 Subject: [PATCH] Add an API to interact with users and preferences The new API uses Django REST Framework. For now, it is designed specifically to support the use cases required by the forum digest notifier (not yet built), with a goal of making it more generally useful over time. --- CHANGELOG.rst | 5 + lms/djangoapps/user_api/__init__.py | 0 .../user_api/migrations/0001_initial.py | 78 +++++ .../user_api/migrations/__init__.py | 0 lms/djangoapps/user_api/models.py | 12 + lms/djangoapps/user_api/serializers.py | 26 ++ lms/djangoapps/user_api/tests/__init__.py | 0 lms/djangoapps/user_api/tests/factories.py | 10 + lms/djangoapps/user_api/tests/test_models.py | 28 ++ lms/djangoapps/user_api/tests/test_views.py | 313 ++++++++++++++++++ lms/djangoapps/user_api/urls.py | 12 + lms/djangoapps/user_api/views.py | 37 +++ lms/envs/common.py | 4 + lms/envs/dev.py | 3 + lms/urls.py | 2 + requirements/edx/base.txt | 2 + 16 files changed, 532 insertions(+) create mode 100644 lms/djangoapps/user_api/__init__.py create mode 100644 lms/djangoapps/user_api/migrations/0001_initial.py create mode 100644 lms/djangoapps/user_api/migrations/__init__.py create mode 100644 lms/djangoapps/user_api/models.py create mode 100644 lms/djangoapps/user_api/serializers.py create mode 100644 lms/djangoapps/user_api/tests/__init__.py create mode 100644 lms/djangoapps/user_api/tests/factories.py create mode 100644 lms/djangoapps/user_api/tests/test_models.py create mode 100644 lms/djangoapps/user_api/tests/test_views.py create mode 100644 lms/djangoapps/user_api/urls.py create mode 100644 lms/djangoapps/user_api/views.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 468db0607c..2b021c40a3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,11 @@ These are notable changes in edx-platform. This is a rolling list of changes, in roughly chronological order, most recent first. Add your entries at or near the top. Include a label indicating the component affected. +LMS: Added user preferences (arbitrary user/key/value tuples, for which +which user/key is unique) and a REST API for reading users and +preferences. Access to the REST API is restricted by use of the +X-Edx-Api-Key HTTP header (which must match settings.EDX_API_KEY; if +the setting is not present, the API is disabled). Common: Added *experimental* support for jsinput type. diff --git a/lms/djangoapps/user_api/__init__.py b/lms/djangoapps/user_api/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/user_api/migrations/0001_initial.py b/lms/djangoapps/user_api/migrations/0001_initial.py new file mode 100644 index 0000000000..754960e36d --- /dev/null +++ b/lms/djangoapps/user_api/migrations/0001_initial.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'UserPreference' + db.create_table('user_api_userpreference', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='+', to=orm['auth.User'])), + ('key', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)), + ('value', self.gf('django.db.models.fields.TextField')()), + )) + db.send_create_signal('user_api', ['UserPreference']) + + # Adding unique constraint on 'UserPreference', fields ['user', 'key'] + db.create_unique('user_api_userpreference', ['user_id', 'key']) + + + def backwards(self, orm): + # Removing unique constraint on 'UserPreference', fields ['user', 'key'] + db.delete_unique('user_api_userpreference', ['user_id', 'key']) + + # Deleting model 'UserPreference' + db.delete_table('user_api_userpreference') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'user_api.userpreference': { + 'Meta': {'unique_together': "(('user', 'key'),)", 'object_name': 'UserPreference'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['auth.User']"}), + 'value': ('django.db.models.fields.TextField', [], {}) + } + } + + complete_apps = ['user_api'] \ No newline at end of file diff --git a/lms/djangoapps/user_api/migrations/__init__.py b/lms/djangoapps/user_api/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/user_api/models.py b/lms/djangoapps/user_api/models.py new file mode 100644 index 0000000000..3450c03aa3 --- /dev/null +++ b/lms/djangoapps/user_api/models.py @@ -0,0 +1,12 @@ +from django.contrib.auth.models import User +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) + value = models.TextField() + + class Meta: + unique_together = ("user", "key") diff --git a/lms/djangoapps/user_api/serializers.py b/lms/djangoapps/user_api/serializers.py new file mode 100644 index 0000000000..42c42cf891 --- /dev/null +++ b/lms/djangoapps/user_api/serializers.py @@ -0,0 +1,26 @@ +from django.contrib.auth.models import User +from rest_framework import serializers +from student.models import UserProfile +from user_api.models import UserPreference + + +class UserSerializer(serializers.HyperlinkedModelSerializer): + name = serializers.SerializerMethodField("get_name") + + def get_name(self, user): + profile = UserProfile.objects.get(user=user) + return profile.name + + class Meta: + model = User + # This list is the minimal set required by the notification service + fields = ("id", "email", "name") + read_only_fields = ("id", "email") + + +class UserPreferenceSerializer(serializers.HyperlinkedModelSerializer): + user = UserSerializer() + + class Meta: + model = UserPreference + depth = 1 diff --git a/lms/djangoapps/user_api/tests/__init__.py b/lms/djangoapps/user_api/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/user_api/tests/factories.py b/lms/djangoapps/user_api/tests/factories.py new file mode 100644 index 0000000000..97cc12df12 --- /dev/null +++ b/lms/djangoapps/user_api/tests/factories.py @@ -0,0 +1,10 @@ +from factory import DjangoModelFactory +from user_api.models import UserPreference + + +class UserPreferenceFactory(DjangoModelFactory): + FACTORY_FOR = UserPreference + + user = None + key = None + value = "default test value" diff --git a/lms/djangoapps/user_api/tests/test_models.py b/lms/djangoapps/user_api/tests/test_models.py new file mode 100644 index 0000000000..db1af152b7 --- /dev/null +++ b/lms/djangoapps/user_api/tests/test_models.py @@ -0,0 +1,28 @@ +from django.db import IntegrityError +from django.test import TestCase +from student.tests.factories import UserFactory +from user_api.tests.factories import UserPreferenceFactory + + +class UserPreferenceModelTest(TestCase): + def test_duplicate_user_key(self): + user = UserFactory.create() + UserPreferenceFactory.create(user=user, key="testkey", value="first") + self.assertRaises( + IntegrityError, + UserPreferenceFactory.create, + user=user, + key="testkey", + value="second" + ) + + def test_arbitrary_values(self): + user = UserFactory.create() + UserPreferenceFactory.create(user=user, key="testkey0", value="") + UserPreferenceFactory.create(user=user, key="testkey1", value="This is some English text!") + UserPreferenceFactory.create(user=user, key="testkey2", value="{'some': 'json'}") + UserPreferenceFactory.create( + user=user, + key="testkey3", + value="\xe8\xbf\x99\xe6\x98\xaf\xe4\xb8\xad\xe5\x9b\xbd\xe6\x96\x87\xe5\xad\x97'" + ) diff --git a/lms/djangoapps/user_api/tests/test_views.py b/lms/djangoapps/user_api/tests/test_views.py new file mode 100644 index 0000000000..3fc630333a --- /dev/null +++ b/lms/djangoapps/user_api/tests/test_views.py @@ -0,0 +1,313 @@ +from django.contrib.auth.models import User +from django.test import TestCase +from django.test.utils import override_settings +import json +import re +from student.tests.factories import UserFactory +from unittest import SkipTest +from user_api.models import UserPreference +from user_api.tests.factories import UserPreferenceFactory + + +TEST_API_KEY = "test_api_key" +USER_LIST_URI = "/user_api/v1/users/" +USER_PREFERENCE_LIST_URI = "/user_api/v1/user_prefs/" + + +@override_settings(EDX_API_KEY=TEST_API_KEY) +class UserApiTestCase(TestCase): + def setUp(self): + super(UserApiTestCase, self).setUp() + self.users = [ + UserFactory.create( + email="test{0}@test.org".format(i), + profile__name="Test {0}".format(i) + ) + for i in range(5) + ] + self.prefs = [ + UserPreferenceFactory.create(user=self.users[0], key="key0"), + UserPreferenceFactory.create(user=self.users[0], key="key1"), + UserPreferenceFactory.create(user=self.users[1], key="key0") + ] + + def request_with_auth(self, method, *args, **kwargs): + """Issue a get request to the given URI with the API key header""" + return getattr(self.client, method)(*args, HTTP_X_EDX_API_KEY=TEST_API_KEY, **kwargs) + + def get_json(self, *args, **kwargs): + """Make a request with the given args and return the parsed JSON repsonse""" + resp = self.request_with_auth("get", *args, **kwargs) + self.assertHttpOK(resp) + self.assertTrue(resp["Content-Type"].startswith("application/json")) + return json.loads(resp.content) + + def get_uri_for_user(self, target_user): + """Given a user object, get the URI for the corresponding resource""" + users = self.get_json(USER_LIST_URI)["results"] + for user in users: + if user["id"] == target_user.id: + return user["url"] + self.fail() + + def get_uri_for_pref(self, target_pref): + """Given a user preference object, get the URI for the corresponding resource""" + prefs = self.get_json(USER_PREFERENCE_LIST_URI)["results"] + for pref in prefs: + if (pref["user"]["id"] == target_pref.user.id and pref["key"] == target_pref.key): + return pref["url"] + self.fail() + + def assertAllowedMethods(self, uri, expected_methods): + """Assert that the allowed methods for the given URI match the expected list""" + resp = self.request_with_auth("options", uri) + self.assertHttpOK(resp) + allow_header = resp.get("Allow") + self.assertIsNotNone(allow_header) + allowed_methods = re.split('[^A-Z]+', allow_header) + self.assertItemsEqual(allowed_methods, expected_methods) + + def assertSelfReferential(self, obj): + """Assert that accessing the "url" entry in the given object returns the same object""" + copy = self.get_json(obj["url"]) + self.assertEqual(obj, copy) + + def assertUserIsValid(self, user): + """Assert that the given user result is valid""" + self.assertItemsEqual(user.keys(), ["email", "id", "name", "url"]) + self.assertSelfReferential(user) + + def assertPrefIsValid(self, pref): + self.assertItemsEqual(pref.keys(), ["user", "key", "value", "url"]) + self.assertSelfReferential(pref) + self.assertUserIsValid(pref["user"]) + + def assertHttpOK(self, response): + """Assert that the given response has the status code 200""" + self.assertEqual(response.status_code, 200) + + def assertHttpForbidden(self, response): + """Assert that the given response has the status code 403""" + self.assertEqual(response.status_code, 403) + + def assertHttpMethodNotAllowed(self, response): + """Assert that the given response has the status code 405""" + self.assertEqual(response.status_code, 405) + + +class UserViewSetTest(UserApiTestCase): + LIST_URI = USER_LIST_URI + + def setUp(self): + super(UserViewSetTest, self).setUp() + self.detail_uri = self.get_uri_for_user(self.users[0]) + + # List view tests + + 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)) + + def test_get_list_empty(self): + User.objects.all().delete() + result = self.get_json(self.LIST_URI) + self.assertEqual(result["count"], 0) + self.assertIsNone(result["next"]) + self.assertIsNone(result["previous"]) + self.assertEqual(result["results"], []) + + def test_get_list_nonempty(self): + result = self.get_json(self.LIST_URI) + self.assertEqual(result["count"], 5) + self.assertIsNone(result["next"]) + self.assertIsNone(result["previous"]) + users = result["results"] + self.assertEqual(len(users), 5) + for user in users: + self.assertUserIsValid(user) + + def test_get_list_pagination(self): + first_page = self.get_json(self.LIST_URI, data={"page_size": 3}) + 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) + + # Detail view tests + + def test_options_detail(self): + self.assertAllowedMethods(self.detail_uri, ["OPTIONS", "GET", "HEAD"]) + + def test_post_detail_not_allowed(self): + self.assertHttpMethodNotAllowed(self.request_with_auth("post", self.detail_uri)) + + def test_put_detail_not_allowed(self): + self.assertHttpMethodNotAllowed(self.request_with_auth("put", self.detail_uri)) + + def test_patch_detail_not_allowed(self): + raise SkipTest("Django 1.4's test client does not support patch") + + def test_delete_detail_not_allowed(self): + self.assertHttpMethodNotAllowed(self.request_with_auth("delete", self.detail_uri)) + + def test_get_detail_unauthorized(self): + self.assertHttpForbidden(self.client.get(self.detail_uri)) + + def test_get_detail(self): + user = self.users[1] + uri = self.get_uri_for_user(user) + self.assertEqual( + self.get_json(uri), + { + "email": user.email, + "id": user.id, + "name": user.profile.name, + "url": uri, + } + ) + + +class UserPreferenceViewSetTest(UserApiTestCase): + LIST_URI = USER_PREFERENCE_LIST_URI + + def setUp(self): + super(UserPreferenceViewSetTest, self).setUp() + self.detail_uri = self.get_uri_for_pref(self.prefs[0]) + + # List view tests + + def test_options_list(self): + self.assertAllowedMethods(self.LIST_URI, ["OPTIONS", "GET", "HEAD"]) + + 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)) + + def test_get_list_empty(self): + UserPreference.objects.all().delete() + result = self.get_json(self.LIST_URI) + self.assertEqual(result["count"], 0) + self.assertIsNone(result["next"]) + self.assertIsNone(result["previous"]) + self.assertEqual(result["results"], []) + + def test_get_list_nonempty(self): + result = self.get_json(self.LIST_URI) + self.assertEqual(result["count"], 3) + self.assertIsNone(result["next"]) + self.assertIsNone(result["previous"]) + prefs = result["results"] + self.assertEqual(len(prefs), 3) + for pref in prefs: + self.assertPrefIsValid(pref) + + def test_get_list_filter_key_empty(self): + result = self.get_json(self.LIST_URI, data={"key": "non-existent"}) + self.assertEqual(result["count"], 0) + self.assertEqual(result["results"], []) + + def test_get_list_filter_key_nonempty(self): + result = self.get_json(self.LIST_URI, data={"key": "key0"}) + self.assertEqual(result["count"], 2) + prefs = result["results"] + self.assertEqual(len(prefs), 2) + for pref in prefs: + self.assertPrefIsValid(pref) + self.assertEqual(pref["key"], "key0") + + def test_get_list_pagination(self): + first_page = self.get_json(self.LIST_URI, data={"page_size": 2}) + self.assertEqual(first_page["count"], 3) + first_page_next_uri = first_page["next"] + self.assertIsNone(first_page["previous"]) + first_page_prefs = first_page["results"] + self.assertEqual(len(first_page_prefs), 2) + + second_page = self.get_json(first_page_next_uri) + self.assertEqual(second_page["count"], 3) + self.assertIsNone(second_page["next"]) + second_page_prev_uri = second_page["previous"] + second_page_prefs = second_page["results"] + self.assertEqual(len(second_page_prefs), 1) + + self.assertEqual(self.get_json(second_page_prev_uri), first_page) + + for pref in first_page_prefs + second_page_prefs: + self.assertPrefIsValid(pref) + all_pref_uris = [pref["url"] for pref in first_page_prefs + second_page_prefs] + self.assertEqual(len(set(all_pref_uris)), 3) + + # Detail view tests + + def test_options_detail(self): + self.assertAllowedMethods(self.detail_uri, ["OPTIONS", "GET", "HEAD"]) + + def test_post_detail_not_allowed(self): + self.assertHttpMethodNotAllowed(self.request_with_auth("post", self.detail_uri)) + + def test_put_detail_not_allowed(self): + self.assertHttpMethodNotAllowed(self.request_with_auth("put", self.detail_uri)) + + def test_patch_detail_not_allowed(self): + raise SkipTest("Django 1.4's test client does not support patch") + + def test_delete_detail_not_allowed(self): + self.assertHttpMethodNotAllowed(self.request_with_auth("delete", self.detail_uri)) + + def test_detail_unauthorized(self): + self.assertHttpForbidden(self.client.get(self.detail_uri)) + + def test_get_detail(self): + pref = self.prefs[1] + uri = self.get_uri_for_pref(pref) + self.assertEqual( + self.get_json(uri), + { + "user": { + "email": pref.user.email, + "id": pref.user.id, + "name": pref.user.profile.name, + "url": self.get_uri_for_user(pref.user), + }, + "key": pref.key, + "value": pref.value, + "url": uri, + } + ) diff --git a/lms/djangoapps/user_api/urls.py b/lms/djangoapps/user_api/urls.py new file mode 100644 index 0000000000..de24b67f02 --- /dev/null +++ b/lms/djangoapps/user_api/urls.py @@ -0,0 +1,12 @@ +from django.conf.urls import include, patterns, url +from rest_framework import routers +from user_api import views as user_api_views + + +user_api_router = routers.DefaultRouter() +user_api_router.register(r'users', user_api_views.UserViewSet) +user_api_router.register(r'user_prefs', user_api_views.UserPreferenceViewSet) +urlpatterns = patterns( + '', + url(r'^v1/', include(user_api_router.urls)), +) diff --git a/lms/djangoapps/user_api/views.py b/lms/djangoapps/user_api/views.py new file mode 100644 index 0000000000..ae1ecef8d9 --- /dev/null +++ b/lms/djangoapps/user_api/views.py @@ -0,0 +1,37 @@ +from django.conf import settings +from django.contrib.auth.models import User +from rest_framework import filters +from rest_framework import permissions +from rest_framework import viewsets +from user_api.models import UserPreference +from user_api.serializers import UserSerializer, UserPreferenceSerializer + + +class ApiKeyHeaderPermission(permissions.BasePermission): + def has_permission(self, request, view): + """ + Check for permissions by matching the configured API key and header + + settings.EDX_API_KEY must be set, and the X-Edx-Api-Key HTTP header must + be present in the request and match the setting. + """ + api_key = getattr(settings, "EDX_API_KEY", None) + return api_key is not None and request.META.get("HTTP_X_EDX_API_KEY") == api_key + + +class UserViewSet(viewsets.ReadOnlyModelViewSet): + permission_classes = (ApiKeyHeaderPermission,) + queryset = User.objects.all() + serializer_class = UserSerializer + paginate_by = 10 + paginate_by_param = "page_size" + + +class UserPreferenceViewSet(viewsets.ReadOnlyModelViewSet): + permission_classes = (ApiKeyHeaderPermission,) + queryset = UserPreference.objects.all() + filter_backends = (filters.DjangoFilterBackend,) + filter_fields = ("key",) + serializer_class = UserPreferenceSerializer + paginate_by = 10 + paginate_by_param = "page_size" diff --git a/lms/envs/common.py b/lms/envs/common.py index 7dee15a8c8..f258b2f4a5 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -749,6 +749,10 @@ INSTALLED_APPS = ( 'django_comment_client', 'django_comment_common', 'notes', + + # User API + 'rest_framework', + 'user_api', ) ######################### MARKETING SITE ############################### diff --git a/lms/envs/dev.py b/lms/envs/dev.py index 2ceebf39b8..ee37cacc82 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -256,6 +256,9 @@ if SEGMENT_IO_LMS_KEY: MITX_FEATURES['SEGMENT_IO_LMS'] = True +########################## USER API ######################## +EDX_API_KEY = '' + ##################################################################### # Lastly, see if the developer has any local overrides. try: diff --git a/lms/urls.py b/lms/urls.py index 085a35b9f4..8c73a5555c 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -59,6 +59,8 @@ urlpatterns = ('', # nopep8 name='auth_password_reset_done'), url(r'^heartbeat$', include('heartbeat.urls')), + + url(r'^user_api/', include('user_api.urls')), ) # University profiles only make sense in the default edX context diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 910b6f3def..4c4ab5a29f 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -7,6 +7,7 @@ celery==3.0.19 distribute>=0.6.28 django-celery==3.0.17 django-countries==1.5 +django-filter==0.6.0 django-followit==0.0.3 django-keyedcache==1.4-6 django-kombu==0.9.4 @@ -20,6 +21,7 @@ django-ses==0.4.1 django-storages==1.1.5 django-threaded-multihost==1.4-1 django-method-override==0.1.0 +djangorestframework==2.3.5 django==1.4.5 feedparser==5.1.3 fs==0.4.0