Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
# pylint: disable=invalid-name, missing-docstring, unused-argument, unused-import, line-too-long
from south.utils import datetime_utils as 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 'LtiConsumer'
db.create_table('lti_provider_lticonsumer', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('consumer_name', self.gf('django.db.models.fields.CharField')(max_length=255)),
('consumer_key', self.gf('django.db.models.fields.CharField')(unique=True, max_length=32, db_index=True)),
('consumer_secret', self.gf('django.db.models.fields.CharField')(unique=True, max_length=32)),
))
db.send_create_signal('lti_provider', ['LtiConsumer'])


def backwards(self, orm):
# Deleting model 'LtiConsumer'
db.delete_table('lti_provider_lticonsumer')


models = {
'lti_provider.lticonsumer': {
'Meta': {'object_name': 'LtiConsumer'},
'consumer_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
'consumer_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'consumer_secret': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
}
}

complete_apps = ['lti_provider']
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as 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 'LtiUser'
db.create_table('lti_provider_ltiuser', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('lti_user_id', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255, db_index=True)),
('edx_user_id', self.gf('django.db.models.fields.CharField')(unique=True, max_length=30)),
('edx_password', self.gf('django.db.models.fields.CharField')(max_length=255)),
))
db.send_create_signal('lti_provider', ['LtiUser'])


def backwards(self, orm):
# Deleting model 'LtiUser'
db.delete_table('lti_provider_ltiuser')


models = {
'lti_provider.lticonsumer': {
'Meta': {'object_name': 'LtiConsumer'},
'consumer_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
'consumer_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'consumer_secret': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'lti_provider.ltiuser': {
'Meta': {'object_name': 'LtiUser'},
'edx_password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'edx_user_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'lti_user_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'})
}
}

complete_apps = ['lti_provider']
Empty file.
17 changes: 15 additions & 2 deletions lms/djangoapps/lti_provider/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
"""
Database models for the LTI provider feature.

This app uses migrations. If you make changes to this model, be sure to create
an appropriate migration file and check it in at the same time as your model
changes. To do that,

1. Go to the edx-platform dir
2. ./manage.py lms schemamigration lti_provider --auto "description" --settings=devstack
"""
from django.db import models
from django.dispatch import receiver
Expand All @@ -13,8 +20,14 @@ class LtiConsumer(models.Model):
specific settings, such as the OAuth key/secret pair and any LTI fields
that must be persisted.
"""
key = models.CharField(max_length=32, unique=True, db_index=True)
secret = models.CharField(max_length=32, unique=True)
consumer_name = models.CharField(max_length=255)
consumer_key = models.CharField(max_length=32, unique=True, db_index=True)
consumer_secret = models.CharField(max_length=32, unique=True)


class LtiUser(models.Model):
lti_user_id = models.CharField(max_length=255, unique=True, db_index=True)
edx_user_id = models.CharField(max_length=30, unique=True)


@receiver(SCORE_CHANGED)
Expand Down
4 changes: 2 additions & 2 deletions lms/djangoapps/lti_provider/signature_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def validate_client_key(self, client_key, request):

:return: True if the key is valid, False if it is not.
"""
return LtiConsumer.objects.filter(key=client_key).count() == 1
return LtiConsumer.objects.filter(consumer_key=client_key).count() == 1

def get_client_secret(self, client_key, request):
"""
Expand All @@ -90,7 +90,7 @@ def get_client_secret(self, client_key, request):
present, or None if the key does not exist in the database.
"""
try:
return LtiConsumer.objects.get(key=client_key).secret
return LtiConsumer.objects.get(consumer_key=client_key).consumer_secret
except ObjectDoesNotExist:
return None

Expand Down
4 changes: 2 additions & 2 deletions lms/djangoapps/lti_provider/tests/test_signature_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def test_validate_existing_key(self):
Verify that validate_client_key succeeds if the client key exists in the
database
"""
LtiConsumer.objects.create(key='client_key', secret='client_secret')
LtiConsumer.objects.create(consumer_key='client_key', consumer_secret='client_secret')
self.assertTrue(SignatureValidator().validate_client_key('client_key', None))

def test_validate_missing_key(self):
Expand All @@ -93,7 +93,7 @@ def test_get_existing_client_secret(self):
Verify that get_client_secret returns the right value if the key is in
the database
"""
LtiConsumer.objects.create(key='client_key', secret='client_secret')
LtiConsumer.objects.create(consumer_key='client_key', consumer_secret='client_secret')
secret = SignatureValidator().get_client_secret('client_key', None)
self.assertEqual(secret, 'client_secret')

Expand Down
37 changes: 16 additions & 21 deletions lms/djangoapps/lti_provider/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from lti_provider import views
from lti_provider.signature_validator import SignatureValidator
from opaque_keys.edx.keys import CourseKey, UsageKey
from student.tests.factories import UserFactory


Expand All @@ -22,10 +23,11 @@
'oauth_nonce': u'OAuth Nonce',
}


COURSE_KEY = CourseKey.from_string('some/course/id')
USAGE_KEY = UsageKey.from_string('i4x://some/course/problem/uuid').map_into_course(COURSE_KEY)
COURSE_PARAMS = {
'course_id': 'CourseID',
'usage_id': 'UsageID'
'course_key': COURSE_KEY,
'usage_key': USAGE_KEY
}


Expand Down Expand Up @@ -72,7 +74,7 @@ def test_valid_launch(self, render):
Verifies that the LTI launch succeeds when passed a valid request.
"""
request = build_launch_request()
views.lti_launch(request, COURSE_PARAMS['course_id'], COURSE_PARAMS['usage_id'])
views.lti_launch(request, str(COURSE_PARAMS['course_key']), str(COURSE_PARAMS['usage_key']))
render.assert_called_with(request, ALL_PARAMS)

def launch_with_missing_parameter(self, missing_param):
Expand Down Expand Up @@ -112,10 +114,10 @@ def test_session_contents_after_launch(self, _run):
properly stored in the session
"""
request = build_launch_request()
views.lti_launch(request, COURSE_PARAMS['course_id'], COURSE_PARAMS['usage_id'])
views.lti_launch(request, str(COURSE_PARAMS['course_key']), str(COURSE_PARAMS['usage_key']))
session = request.session[views.LTI_SESSION_KEY]
self.assertEqual(session['course_id'], 'CourseID', 'Course ID not set in the session')
self.assertEqual(session['usage_id'], 'UsageID', 'Usage ID not set in the session')
self.assertEqual(session['course_key'], COURSE_KEY, 'Course key not set in the session')
self.assertEqual(session['usage_key'], USAGE_KEY, 'Usage key not set in the session')
for key in views.REQUIRED_PARAMETERS:
self.assertEqual(session[key], request.POST[key], key + ' not set in the session')

Expand All @@ -126,7 +128,9 @@ def test_redirect_for_non_authenticated_user(self):
URL
"""
request = build_launch_request(False)
response = views.lti_launch(request, None, None)
response = views.lti_launch(
request, str(COURSE_PARAMS['course_key']), str(COURSE_PARAMS['usage_key'])
)
self.assertEqual(response.status_code, 302)
self.assertEqual(response['Location'], '/accounts/login?next=/lti_provider/lti_run')

Expand Down Expand Up @@ -170,7 +174,7 @@ def test_forbidden_if_session_incomplete(self):
Verifies that the lti_run view returns a Forbidden status if the session
is missing any of the required LTI parameters or course information.
"""
extra_keys = ['course_id', 'usage_id']
extra_keys = ['course_key', 'usage_key']
for key in views.REQUIRED_PARAMETERS + extra_keys:
request = build_run_request()
del request.session[views.LTI_SESSION_KEY][key]
Expand Down Expand Up @@ -208,7 +212,6 @@ def setUp(self):
self.module_mock = self.setup_patch('lti_provider.views.get_module_by_usage_id', (self.module_instance, None))
self.access_mock = self.setup_patch('lti_provider.views.has_access', 'StaffAccess')
self.course_mock = self.setup_patch('lti_provider.views.get_course_with_access', 'CourseWithAccess')
self.key_mock = self.setup_patch('lti_provider.views.CourseKey.from_string', 'CourseKey')

def setup_patch(self, function_name, return_value):
"""
Expand All @@ -228,21 +231,13 @@ def test_valid_launch(self):
response = views.render_courseware(request, ALL_PARAMS.copy())
self.assertEqual(response, 'Rendered page')

def test_course_key(self):
"""
Verify that the correct course key is requested
"""
request = build_run_request()
views.render_courseware(request, ALL_PARAMS.copy())
self.key_mock.assert_called_with(ALL_PARAMS['course_id'])

def test_course_with_access(self):
"""
Verify that get_course_with_access is called with the right parameters
"""
request = build_run_request()
views.render_courseware(request, ALL_PARAMS.copy())
self.course_mock.assert_called_with(request.user, 'load', 'CourseKey')
self.course_mock.assert_called_with(request.user, 'load', COURSE_KEY)

def test_has_access(self):
"""
Expand All @@ -258,7 +253,7 @@ def test_get_module(self):
"""
request = build_run_request()
views.render_courseware(request, ALL_PARAMS.copy())
self.module_mock.assert_called_with(request, ALL_PARAMS['course_id'], ALL_PARAMS['usage_id'])
self.module_mock.assert_called_with(request, str(ALL_PARAMS['course_key']), str(ALL_PARAMS['usage_key']))

def test_render(self):
"""
Expand All @@ -278,7 +273,7 @@ def test_context(self):
'disable_footer': True,
'disable_tabs': True,
'staff_access': 'StaffAccess',
'xqa_server': 'http://your_xqa_server.com',
'xqa_server': 'http://example.com/xqa',
}
request = build_run_request()
views.render_courseware(request, ALL_PARAMS.copy())
Expand Down
42 changes: 42 additions & 0 deletions lms/djangoapps/lti_provider/users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import string
import random
import uuid

from django.contrib.auth import authenticate, login, get_backends
from django.contrib.auth.models import User
from lti_provider.models import LtiUser


def create_lti_user(params):
lti_user_id = params['user_id']
edx_user_id = generate_random_edx_username()
edx_password = uuid.uuid4()

edx_user = User.objects.create_user(
username=edx_user_id,
password=edx_password,
email='{}@lti.example.com'.format(edx_user_id)
)
edx_user.save()

lti_user = LtiUser(
lti_user_id=lti_user_id,
edx_user_id=edx_user_id,
)
lti_user.save()
return lti_user


def switch_user(request, lti_user):
user = User.objects.get(username=lti_user.edx_user_id)
user.backend = 'nobody'
if user:
login(request, user)


def generate_random_edx_username():
allowable_chars = string.ascii_letters + string.digits
username = ''
for _i in range(30):
username = username + random.SystemRandom().choice(allowable_chars)
return username
Loading