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.
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