Merge pull request #2419 from edx/talbs/lms-languagemenu
LMS: Adds Language Selection Menu
This commit is contained in:
@@ -19,6 +19,8 @@ class DarkLangConfig(ConfigurationModel):
|
||||
def released_languages_list(self):
|
||||
"""
|
||||
``released_languages`` as a list of language codes.
|
||||
|
||||
Example: ['it', 'de-at', 'es', 'pt-br']
|
||||
"""
|
||||
if not self.released_languages.strip(): # pylint: disable=no-member
|
||||
return []
|
||||
|
||||
6
common/djangoapps/lang_pref/__init__.py
Normal file
6
common/djangoapps/lang_pref/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""
|
||||
Useful information for setting the language preference
|
||||
"""
|
||||
|
||||
# this is the UserPreference key for the user's preferred language
|
||||
LANGUAGE_KEY = 'pref-lang'
|
||||
25
common/djangoapps/lang_pref/middleware.py
Normal file
25
common/djangoapps/lang_pref/middleware.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
Middleware for Language Preferences
|
||||
"""
|
||||
|
||||
from user_api.models import UserPreference
|
||||
from lang_pref import LANGUAGE_KEY
|
||||
|
||||
|
||||
class LanguagePreferenceMiddleware(object):
|
||||
"""
|
||||
Middleware for user preferences.
|
||||
|
||||
Ensures that, once set, a user's preferences are reflected in the page
|
||||
whenever they are logged in.
|
||||
"""
|
||||
|
||||
def process_request(self, request):
|
||||
"""
|
||||
If a user's UserPreference contains a language preference and there is
|
||||
no language set on the session (i.e. from dark language overrides), use the user's preference.
|
||||
"""
|
||||
if request.user.is_authenticated() and 'django_language' not in request.session:
|
||||
user_pref = UserPreference.get_preference(request.user, LANGUAGE_KEY)
|
||||
if user_pref:
|
||||
request.session['django_language'] = user_pref
|
||||
42
common/djangoapps/lang_pref/tests/test_middleware.py
Normal file
42
common/djangoapps/lang_pref/tests/test_middleware.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
|
||||
from lang_pref.middleware import LanguagePreferenceMiddleware
|
||||
from user_api.models import UserPreference
|
||||
from lang_pref import LANGUAGE_KEY
|
||||
from student.tests.factories import UserFactory
|
||||
|
||||
|
||||
class TestUserPreferenceMiddleware(TestCase):
|
||||
"""
|
||||
Tests to make sure user preferences are getting properly set in the middleware
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.middleware = LanguagePreferenceMiddleware()
|
||||
self.session_middleware = SessionMiddleware()
|
||||
self.user = UserFactory.create()
|
||||
self.request = RequestFactory().get('/somewhere')
|
||||
self.request.user = self.user
|
||||
self.session_middleware.process_request(self.request)
|
||||
|
||||
def test_no_language_set_in_session_or_prefs(self):
|
||||
# nothing set in the session or the prefs
|
||||
self.middleware.process_request(self.request)
|
||||
self.assertNotIn('django_language', self.request.session)
|
||||
|
||||
def test_language_in_user_prefs(self):
|
||||
# language set in the user preferences and not the session
|
||||
UserPreference.set_preference(self.user, LANGUAGE_KEY, 'eo')
|
||||
self.middleware.process_request(self.request)
|
||||
self.assertEquals(self.request.session['django_language'], 'eo')
|
||||
|
||||
def test_language_in_session(self):
|
||||
# language set in both the user preferences and session,
|
||||
# session should get precedence
|
||||
self.request.session['django_language'] = 'en'
|
||||
UserPreference.set_preference(self.user, LANGUAGE_KEY, 'eo')
|
||||
self.middleware.process_request(self.request)
|
||||
|
||||
self.assertEquals(self.request.session['django_language'], 'en')
|
||||
34
common/djangoapps/lang_pref/tests/test_views.py
Normal file
34
common/djangoapps/lang_pref/tests/test_views.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""
|
||||
Tests for the language setting view
|
||||
"""
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import TestCase
|
||||
from student.tests.factories import UserFactory
|
||||
from user_api.models import UserPreference
|
||||
from lang_pref import LANGUAGE_KEY
|
||||
|
||||
|
||||
class TestLanguageSetting(TestCase):
|
||||
"""
|
||||
Test setting languages
|
||||
"""
|
||||
def test_set_preference_happy(self):
|
||||
user = UserFactory.create()
|
||||
self.client.login(username=user.username, password='test')
|
||||
|
||||
lang = 'en'
|
||||
response = self.client.post(reverse('lang_pref_set_language'), {'language': lang})
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
user_pref = UserPreference.get_preference(user, LANGUAGE_KEY)
|
||||
self.assertEqual(user_pref, lang)
|
||||
|
||||
def test_set_preference_missing_lang(self):
|
||||
user = UserFactory.create()
|
||||
self.client.login(username=user.username, password='test')
|
||||
|
||||
response = self.client.post(reverse('lang_pref_set_language'))
|
||||
|
||||
self.assertEquals(response.status_code, 400)
|
||||
|
||||
self.assertIsNone(UserPreference.get_preference(user, LANGUAGE_KEY))
|
||||
10
common/djangoapps/lang_pref/urls.py
Normal file
10
common/djangoapps/lang_pref/urls.py
Normal file
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
Urls for managing language preferences
|
||||
"""
|
||||
|
||||
from django.conf.urls import patterns, url
|
||||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
url(r'^setlang/', 'lang_pref.views.set_language', name='lang_pref_set_language')
|
||||
)
|
||||
23
common/djangoapps/lang_pref/views.py
Normal file
23
common/djangoapps/lang_pref/views.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""
|
||||
Views for accessing language preferences
|
||||
"""
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import HttpResponse, HttpResponseBadRequest
|
||||
|
||||
from user_api.models import UserPreference
|
||||
from lang_pref import LANGUAGE_KEY
|
||||
|
||||
|
||||
@login_required
|
||||
def set_language(request):
|
||||
"""
|
||||
This view is called when the user would like to set a language preference
|
||||
"""
|
||||
user = request.user
|
||||
lang_pref = request.POST.get('language', None)
|
||||
|
||||
if lang_pref:
|
||||
UserPreference.set_preference(user, LANGUAGE_KEY, lang_pref)
|
||||
return HttpResponse('{"success": true}')
|
||||
|
||||
return HttpResponseBadRequest('no language provided')
|
||||
@@ -45,6 +45,7 @@ from student.firebase_token_generator import create_token
|
||||
|
||||
from verify_student.models import SoftwareSecurePhotoVerification, MidcourseReverificationWindow
|
||||
from certificates.models import CertificateStatuses, certificate_status_for_student
|
||||
from dark_lang.models import DarkLangConfig
|
||||
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
@@ -61,6 +62,8 @@ import external_auth.views
|
||||
|
||||
from bulk_email.models import Optout, CourseAuthorization
|
||||
import shoppingcart
|
||||
from user_api.models import UserPreference
|
||||
from lang_pref import LANGUAGE_KEY
|
||||
|
||||
import track.views
|
||||
|
||||
@@ -468,23 +471,42 @@ def dashboard(request):
|
||||
# we'll display the banner
|
||||
denied_banner = any(item.display for item in reverifications["denied"])
|
||||
|
||||
context = {'course_enrollment_pairs': course_enrollment_pairs,
|
||||
'course_optouts': course_optouts,
|
||||
'message': message,
|
||||
'external_auth_map': external_auth_map,
|
||||
'staff_access': staff_access,
|
||||
'errored_courses': errored_courses,
|
||||
'show_courseware_links_for': show_courseware_links_for,
|
||||
'all_course_modes': course_modes,
|
||||
'cert_statuses': cert_statuses,
|
||||
'show_email_settings_for': show_email_settings_for,
|
||||
'reverifications': reverifications,
|
||||
'verification_status': verification_status,
|
||||
'verification_msg': verification_msg,
|
||||
'show_refund_option_for': show_refund_option_for,
|
||||
'denied_banner': denied_banner,
|
||||
'billing_email': settings.PAYMENT_SUPPORT_EMAIL,
|
||||
}
|
||||
language_options = DarkLangConfig.current().released_languages_list
|
||||
|
||||
# add in the default language if it's not in the list of released languages
|
||||
if settings.LANGUAGE_CODE not in language_options:
|
||||
language_options.append(settings.LANGUAGE_CODE)
|
||||
|
||||
# try to get the prefered language for the user
|
||||
cur_lang_code = UserPreference.get_preference(request.user, LANGUAGE_KEY)
|
||||
if cur_lang_code:
|
||||
# if the user has a preference, get the name from the code
|
||||
current_language = settings.LANGUAGE_DICT[cur_lang_code]
|
||||
else:
|
||||
# if the user doesn't have a preference, use the default language
|
||||
current_language = settings.LANGUAGE_DICT[settings.LANGUAGE_CODE]
|
||||
|
||||
context = {
|
||||
'course_enrollment_pairs': course_enrollment_pairs,
|
||||
'course_optouts': course_optouts,
|
||||
'message': message,
|
||||
'external_auth_map': external_auth_map,
|
||||
'staff_access': staff_access,
|
||||
'errored_courses': errored_courses,
|
||||
'show_courseware_links_for': show_courseware_links_for,
|
||||
'all_course_modes': course_modes,
|
||||
'cert_statuses': cert_statuses,
|
||||
'show_email_settings_for': show_email_settings_for,
|
||||
'reverifications': reverifications,
|
||||
'verification_status': verification_status,
|
||||
'verification_msg': verification_msg,
|
||||
'show_refund_option_for': show_refund_option_for,
|
||||
'denied_banner': denied_banner,
|
||||
'billing_email': settings.PAYMENT_SUPPORT_EMAIL,
|
||||
'language_options': language_options,
|
||||
'current_language': current_language,
|
||||
'current_language_code': cur_lang_code,
|
||||
}
|
||||
|
||||
return render_to_response('dashboard.html', context)
|
||||
|
||||
|
||||
0
common/djangoapps/user_api/__init__.py
Normal file
0
common/djangoapps/user_api/__init__.py
Normal file
78
common/djangoapps/user_api/migrations/0001_initial.py
Normal file
78
common/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
common/djangoapps/user_api/migrations/__init__.py
Normal file
0
common/djangoapps/user_api/migrations/__init__.py
Normal file
35
common/djangoapps/user_api/models.py
Normal file
35
common/djangoapps/user_api/models.py
Normal file
@@ -0,0 +1,35 @@
|
||||
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")
|
||||
|
||||
@classmethod
|
||||
def set_preference(cls, user, preference_key, preference_value):
|
||||
"""
|
||||
Sets the user preference for a given key
|
||||
"""
|
||||
user_pref, _ = cls.objects.get_or_create(user=user, key=preference_key)
|
||||
user_pref.value = preference_value
|
||||
user_pref.save()
|
||||
|
||||
@classmethod
|
||||
def get_preference(cls, user, preference_key, default=None):
|
||||
"""
|
||||
Gets the user preference value for a given key
|
||||
|
||||
Returns the given default if there isn't a preference for the given key
|
||||
"""
|
||||
|
||||
try:
|
||||
user_pref = cls.objects.get(user=user, key=preference_key)
|
||||
return user_pref.value
|
||||
except cls.DoesNotExist:
|
||||
return default
|
||||
26
common/djangoapps/user_api/serializers.py
Normal file
26
common/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", "username")
|
||||
read_only_fields = ("id", "email", "username")
|
||||
|
||||
|
||||
class UserPreferenceSerializer(serializers.HyperlinkedModelSerializer):
|
||||
user = UserSerializer()
|
||||
|
||||
class Meta:
|
||||
model = UserPreference
|
||||
depth = 1
|
||||
0
common/djangoapps/user_api/tests/__init__.py
Normal file
0
common/djangoapps/user_api/tests/__init__.py
Normal file
10
common/djangoapps/user_api/tests/factories.py
Normal file
10
common/djangoapps/user_api/tests/factories.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from factory.django import DjangoModelFactory
|
||||
from user_api.models import UserPreference
|
||||
|
||||
|
||||
class UserPreferenceFactory(DjangoModelFactory):
|
||||
FACTORY_FOR = UserPreference
|
||||
|
||||
user = None
|
||||
key = None
|
||||
value = "default test value"
|
||||
47
common/djangoapps/user_api/tests/test_models.py
Normal file
47
common/djangoapps/user_api/tests/test_models.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from django.db import IntegrityError
|
||||
from django.test import TestCase
|
||||
from student.tests.factories import UserFactory
|
||||
from user_api.tests.factories import UserPreferenceFactory
|
||||
from user_api.models import UserPreference
|
||||
|
||||
|
||||
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'"
|
||||
)
|
||||
|
||||
def test_get_set_preference(self):
|
||||
# Checks that you can set a preference and get that preference later
|
||||
# Also, tests that no preference is returned for keys that are not set
|
||||
|
||||
user = UserFactory.create()
|
||||
key = 'testkey'
|
||||
value = 'testvalue'
|
||||
|
||||
# does a round trip
|
||||
UserPreference.set_preference(user, key, value)
|
||||
pref = UserPreference.get_preference(user, key)
|
||||
|
||||
self.assertEqual(pref, value)
|
||||
|
||||
# get preference for key that doesn't exist for user
|
||||
pref = UserPreference.get_preference(user, 'testkey_none')
|
||||
self.assertIsNone(pref)
|
||||
361
common/djangoapps/user_api/tests/test_views.py
Normal file
361
common/djangoapps/user_api/tests/test_views.py
Normal file
@@ -0,0 +1,361 @@
|
||||
import base64
|
||||
|
||||
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 ApiTestCase(TestCase):
|
||||
|
||||
LIST_URI = USER_LIST_URI
|
||||
|
||||
def basic_auth(self, username, password):
|
||||
return {'HTTP_AUTHORIZATION': 'Basic ' + base64.b64encode('%s:%s' % (username, password))}
|
||||
|
||||
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", "username", "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 assertHttpBadRequest(self, response):
|
||||
"""Assert that the given response has the status code 400"""
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def assertHttpMethodNotAllowed(self, response):
|
||||
"""Assert that the given response has the status code 405"""
|
||||
self.assertEqual(response.status_code, 405)
|
||||
|
||||
|
||||
class EmptyUserTestCase(ApiTestCase):
|
||||
def test_get_list_empty(self):
|
||||
result = self.get_json(self.LIST_URI)
|
||||
self.assertEqual(result["count"], 0)
|
||||
self.assertIsNone(result["next"])
|
||||
self.assertIsNone(result["previous"])
|
||||
self.assertEqual(result["results"], [])
|
||||
|
||||
|
||||
class UserApiTestCase(ApiTestCase):
|
||||
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")
|
||||
]
|
||||
|
||||
|
||||
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))
|
||||
|
||||
@override_settings(DEBUG=True)
|
||||
@override_settings(EDX_API_KEY=None)
|
||||
def test_debug_auth(self):
|
||||
self.assertHttpOK(self.client.get(self.LIST_URI))
|
||||
|
||||
@override_settings(DEBUG=False)
|
||||
@override_settings(EDX_API_KEY=TEST_API_KEY)
|
||||
def test_basic_auth(self):
|
||||
# ensure that having basic auth headers in the mix does not break anything
|
||||
self.assertHttpOK(
|
||||
self.request_with_auth("get", self.LIST_URI,
|
||||
**self.basic_auth('someuser', 'somepass')))
|
||||
self.assertHttpForbidden(
|
||||
self.client.get(self.LIST_URI, **self.basic_auth('someuser', 'somepass')))
|
||||
|
||||
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,
|
||||
"username": user.username,
|
||||
"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))
|
||||
|
||||
@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_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_filter_user_empty(self):
|
||||
def test_id(user_id):
|
||||
result = self.get_json(self.LIST_URI, data={"user": user_id})
|
||||
self.assertEqual(result["count"], 0)
|
||||
self.assertEqual(result["results"], [])
|
||||
test_id(self.users[2].id)
|
||||
# TODO: If the given id does not match a user, then the filter is a no-op
|
||||
# test_id(42)
|
||||
# test_id("asdf")
|
||||
|
||||
def test_get_list_filter_user_nonempty(self):
|
||||
user_id = self.users[0].id
|
||||
result = self.get_json(self.LIST_URI, data={"user": user_id})
|
||||
self.assertEqual(result["count"], 2)
|
||||
prefs = result["results"]
|
||||
self.assertEqual(len(prefs), 2)
|
||||
for pref in prefs:
|
||||
self.assertPrefIsValid(pref)
|
||||
self.assertEqual(pref["user"]["id"], user_id)
|
||||
|
||||
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,
|
||||
"username": pref.user.username,
|
||||
"url": self.get_uri_for_user(pref.user),
|
||||
},
|
||||
"key": pref.key,
|
||||
"value": pref.value,
|
||||
"url": uri,
|
||||
}
|
||||
)
|
||||
12
common/djangoapps/user_api/urls.py
Normal file
12
common/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)),
|
||||
)
|
||||
45
common/djangoapps/user_api/views.py
Normal file
45
common/djangoapps/user_api/views.py
Normal file
@@ -0,0 +1,45 @@
|
||||
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 permissions
|
||||
from rest_framework import viewsets
|
||||
from user_api.serializers import UserSerializer, UserPreferenceSerializer
|
||||
from user_api.models import UserPreference
|
||||
|
||||
|
||||
class ApiKeyHeaderPermission(permissions.BasePermission):
|
||||
def has_permission(self, request, view):
|
||||
"""
|
||||
Check for permissions by matching the configured API key and header
|
||||
|
||||
If settings.DEBUG is True and settings.EDX_API_KEY is not set or None,
|
||||
then allow the request. Otherwise, allow the request if and only if
|
||||
settings.EDX_API_KEY is set and the X-Edx-Api-Key HTTP header is
|
||||
present in the request and matches the setting.
|
||||
"""
|
||||
api_key = getattr(settings, "EDX_API_KEY", None)
|
||||
return (
|
||||
(settings.DEBUG and api_key is None) or
|
||||
(api_key is not None and request.META.get("HTTP_X_EDX_API_KEY") == api_key)
|
||||
)
|
||||
|
||||
|
||||
class UserViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
authentication_classes = (authentication.SessionAuthentication,)
|
||||
permission_classes = (ApiKeyHeaderPermission,)
|
||||
queryset = User.objects.all()
|
||||
serializer_class = UserSerializer
|
||||
paginate_by = 10
|
||||
paginate_by_param = "page_size"
|
||||
|
||||
|
||||
class UserPreferenceViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
authentication_classes = (authentication.SessionAuthentication,)
|
||||
permission_classes = (ApiKeyHeaderPermission,)
|
||||
queryset = UserPreference.objects.all()
|
||||
filter_backends = (filters.DjangoFilterBackend,)
|
||||
filter_fields = ("key", "user")
|
||||
serializer_class = UserPreferenceSerializer
|
||||
paginate_by = 10
|
||||
paginate_by_param = "page_size"
|
||||
Reference in New Issue
Block a user