Merge pull request #331 from edx/gprice/user-pref-service
Add an API to interact with users and preferences
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
0
lms/djangoapps/user_api/__init__.py
Normal file
0
lms/djangoapps/user_api/__init__.py
Normal file
78
lms/djangoapps/user_api/migrations/0001_initial.py
Normal file
78
lms/djangoapps/user_api/migrations/0001_initial.py
Normal file
@@ -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']
|
||||
0
lms/djangoapps/user_api/migrations/__init__.py
Normal file
0
lms/djangoapps/user_api/migrations/__init__.py
Normal file
12
lms/djangoapps/user_api/models.py
Normal file
12
lms/djangoapps/user_api/models.py
Normal file
@@ -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")
|
||||
26
lms/djangoapps/user_api/serializers.py
Normal file
26
lms/djangoapps/user_api/serializers.py
Normal file
@@ -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
|
||||
0
lms/djangoapps/user_api/tests/__init__.py
Normal file
0
lms/djangoapps/user_api/tests/__init__.py
Normal file
10
lms/djangoapps/user_api/tests/factories.py
Normal file
10
lms/djangoapps/user_api/tests/factories.py
Normal file
@@ -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"
|
||||
28
lms/djangoapps/user_api/tests/test_models.py
Normal file
28
lms/djangoapps/user_api/tests/test_models.py
Normal file
@@ -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'"
|
||||
)
|
||||
313
lms/djangoapps/user_api/tests/test_views.py
Normal file
313
lms/djangoapps/user_api/tests/test_views.py
Normal file
@@ -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,
|
||||
}
|
||||
)
|
||||
12
lms/djangoapps/user_api/urls.py
Normal file
12
lms/djangoapps/user_api/urls.py
Normal file
@@ -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)),
|
||||
)
|
||||
37
lms/djangoapps/user_api/views.py
Normal file
37
lms/djangoapps/user_api/views.py
Normal file
@@ -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"
|
||||
@@ -749,6 +749,10 @@ INSTALLED_APPS = (
|
||||
'django_comment_client',
|
||||
'django_comment_common',
|
||||
'notes',
|
||||
|
||||
# User API
|
||||
'rest_framework',
|
||||
'user_api',
|
||||
)
|
||||
|
||||
######################### MARKETING SITE ###############################
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user