Skip to content

Commit

Permalink
Add basic LTI 1.3 support - #636
Browse files Browse the repository at this point in the history
Preliminary launch steps are working, using updated code in our
django-lti-provider-example library.
  • Loading branch information
nikolas committed Mar 8, 2024
1 parent 7b02d44 commit a668e65
Show file tree
Hide file tree
Showing 10 changed files with 189 additions and 50 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: build-and-test
on: [push, workflow_dispatch]
jobs:
build:
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
strategy:
matrix:
python-version: ["3.8", "3.11"]
Expand Down
7 changes: 5 additions & 2 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
1.0.0
1.1.0
==================
* Add LTI 1.3 support

1.0.0 (2023-10-26)
==================
* Django 3 compatability
* No longer testing against Django 2.2


0.4.7
==================
* Provide a default value for allow_ta_access setting.
Expand Down
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ VE ?= ./ve
REQUIREMENTS ?= test_reqs.txt
SYS_PYTHON ?= python3
PY_SENTINAL ?= $(VE)/sentinal
WHEEL_VERSION ?= 0.41.2
PIP_VERSION ?= 23.2.1
WHEEL_VERSION ?= 0.42.0
PIP_VERSION ?= 24.0
MAX_COMPLEXITY ?= 7
INTERFACE ?= localhost
RUNSERVER_PORT ?= 8000
Expand Down Expand Up @@ -34,7 +34,7 @@ $(PY_SENTINAL):
$(PIP) install pip==$(PIP_VERSION)
$(PIP) install --upgrade setuptools
$(PIP) install wheel==$(WHEEL_VERSION)
$(PIP) install --no-deps --requirement $(REQUIREMENTS) --no-binary cryptography
$(PIP) install --no-deps --requirement $(REQUIREMENTS)
$(PIP) install "$(DJANGO)"
touch $@

Expand Down
24 changes: 12 additions & 12 deletions lti_provider/tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,62 +26,62 @@ def setUp(self):
def test_create_user(self):
user = self.backend.create_user(self.request, self.lti, '12345')
self.assertFalse(user.has_usable_password())
self.assertEquals(user.email, '[email protected]')
self.assertEquals(user.get_full_name(), 'Foo Baz')
self.assertEqual(user.email, '[email protected]')
self.assertEqual(user.get_full_name(), 'Foo Baz')

def test_create_user_no_full_name(self):
self.request.session.pop('lis_person_name_full')
user = self.backend.create_user(self.request, self.lti, '12345')
self.assertEquals(user.get_full_name(), 'student')
self.assertEqual(user.get_full_name(), 'student')

def test_create_user_empty_full_name(self):
self.request.session['lis_person_name_full'] = ''
user = self.backend.create_user(self.request, self.lti, '12345')
self.assertEquals(user.get_full_name(), 'student')
self.assertEqual(user.get_full_name(), 'student')

def test_create_user_long_name(self):
self.request.session['lis_person_name_full'] = (
'Pneumonoultramicroscopicsilicovolcanoconiosis '
'Supercalifragilisticexpialidocious')
user = self.backend.create_user(self.request, self.lti, '12345')
self.assertEquals(
self.assertEqual(
user.get_full_name(),
'Pneumonoultramicroscopicsilico Supercalifragilisticexpialidoc')

def test_find_or_create_user1(self):
# via email
user = UserFactory(email='[email protected]')
self.assertEquals(
self.assertEqual(
self.backend.find_or_create_user(self.request, self.lti), user)

def test_find_or_create_user2(self):
# via lms username
username = 'uni123'
self.request.session['custom_canvas_user_login_id'] = username
user = UserFactory(username=username)
self.assertEquals(
self.assertEqual(
self.backend.find_or_create_user(self.request, self.lti), user)

def test_find_or_create_user3(self):
# via hashed username
self.request.session['oauth_consumer_key'] = '1234567890'
username = self.backend.get_hashed_username(self.request, self.lti)
user = UserFactory(username=username)
self.assertEquals(
self.assertEqual(
self.backend.find_or_create_user(self.request, self.lti), user)

def test_find_or_create_user4(self):
# new user
self.request.session['oauth_consumer_key'] = '1234567890'
user = self.backend.find_or_create_user(self.request, self.lti)
self.assertFalse(user.has_usable_password())
self.assertEquals(user.email, '[email protected]')
self.assertEquals(user.get_full_name(), 'Foo Baz')
self.assertEqual(user.email, '[email protected]')
self.assertEqual(user.get_full_name(), 'Foo Baz')

username = self.backend.get_hashed_username(self.request, self.lti)
self.assertEquals(user.username, username)
self.assertEqual(user.username, username)

def test_get_user(self):
user = UserFactory()
self.assertIsNone(self.backend.get_user(1234))
self.assertEquals(self.backend.get_user(user.id), user)
self.assertEqual(self.backend.get_user(user.id), user)
18 changes: 9 additions & 9 deletions lti_provider/tests/test_lti.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,27 +28,27 @@ def setUp(self):
self.emptyRequest.session.save()

def test_init(self):
self.assertEquals(self.lti.request_type, 'initial')
self.assertEquals(self.lti.role_type, 'any')
self.assertEqual(self.lti.request_type, 'initial')
self.assertEqual(self.lti.role_type, 'any')

def test_consumer_user_id(self):
self.request.session['oauth_consumer_key'] = '1234567890'
self.assertEquals(
self.assertEqual(
self.lti.consumer_user_id(self.request), '1234567890-student')

def test_user_email(self):
self.assertIsNone(self.lti.user_email(self.emptyRequest))
self.assertEquals(self.lti.user_email(self.request), '[email protected]')
self.assertEqual(self.lti.user_email(self.request), '[email protected]')

def test_user_fullname(self):
self.assertEquals(self.lti.user_fullname(self.emptyRequest), '')
self.assertEqual(self.lti.user_fullname(self.emptyRequest), '')

self.assertEquals(self.lti.user_fullname(self.request), 'Foo Bar Baz')
self.assertEqual(self.lti.user_fullname(self.request), 'Foo Bar Baz')

def test_user_roles(self):
self.assertEquals(self.lti.user_roles(self.emptyRequest), [])
self.assertEqual(self.lti.user_roles(self.emptyRequest), [])

self.assertEquals(self.lti.user_roles(self.request), [
self.assertEqual(self.lti.user_roles(self.request), [
u'urn:lti:instrole:ims/lis/instructor',
u'urn:lti:instrole:ims/lis/staff'])

Expand All @@ -57,7 +57,7 @@ def test_user_roles(self):

def test_consumers(self):
with self.settings(PYLTI_CONFIG={'consumers': CONSUMERS}):
self.assertEquals(self.lti.consumers(), CONSUMERS)
self.assertEqual(self.lti.consumers(), CONSUMERS)

def test_params(self):
factory = RequestFactory()
Expand Down
30 changes: 15 additions & 15 deletions lti_provider/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,9 @@ def test_launch_invalid_user(self):
request = generate_lti_request()

response = LTIRoutingView().dispatch(request)
self.assertEquals(response.status_code, 302)
self.assertEqual(response.status_code, 302)

self.assertEquals(response.url, reverse('lti-fail-auth'))
self.assertEqual(response.url, reverse('lti-fail-auth'))
self.assertFalse(request.session.get(LTI_SESSION_KEY, False))

def test_launch_invalid_course(self):
Expand All @@ -127,8 +127,8 @@ def test_launch_invalid_course(self):
request = generate_lti_request()

response = LTIRoutingView().dispatch(request)
self.assertEquals(response.status_code, 302)
self.assertEquals(response.url, reverse('lti-course-config'))
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse('lti-course-config'))
self.assertTrue(request.session.get(LTI_SESSION_KEY, False))

def test_launch(self):
Expand All @@ -143,17 +143,17 @@ def test_launch(self):
view.request = request

response = view.dispatch(request)
self.assertEquals(response.status_code, 302)
self.assertEqual(response.status_code, 302)

landing = 'http://testserver/?lti_version=LTI-1p0&'
self.assertEquals(
self.assertEqual(
response.url, landing.format(ctx.lms_course_context))

self.assertIsNotNone(request.session[LTI_SESSION_KEY])
user = request.user
self.assertFalse(user.has_usable_password())
self.assertEquals(user.email, '[email protected]')
self.assertEquals(user.get_full_name(), 'Foo Baz')
self.assertEqual(user.email, '[email protected]')
self.assertEqual(user.get_full_name(), 'Foo Baz')
self.assertTrue(user in ctx.group.user_set.all())
self.assertTrue(user in ctx.faculty_group.user_set.all())

Expand All @@ -170,16 +170,16 @@ def test_launch_custom_landing_page(self):

response = view.dispatch(request)
landing = 'http://testserver/lti/landing/{}/?lti_version=LTI-1p0&'
self.assertEquals(response.status_code, 302)
self.assertEqual(response.status_code, 302)
self.assertTrue(
response.url,
landing.format(ctx.lms_course_context))

self.assertIsNotNone(request.session[LTI_SESSION_KEY])
user = request.user
self.assertFalse(user.has_usable_password())
self.assertEquals(user.email, '[email protected]')
self.assertEquals(user.get_full_name(), 'Foo Baz')
self.assertEqual(user.email, '[email protected]')
self.assertEqual(user.get_full_name(), 'Foo Baz')
self.assertTrue(user in ctx.group.user_set.all())
self.assertTrue(user in ctx.faculty_group.user_set.all())

Expand All @@ -194,17 +194,17 @@ def test_embed(self):
view.request = request

response = view.dispatch(request)
self.assertEquals(response.status_code, 302)
self.assertEquals(
self.assertEqual(response.status_code, 302)
self.assertEqual(
response.url,
'http://testserver/asset/embed/?return_url=/asset/'
'&lti_version=LTI-1p0&')

self.assertIsNotNone(request.session[LTI_SESSION_KEY])
user = request.user
self.assertFalse(user.has_usable_password())
self.assertEquals(user.email, '[email protected]')
self.assertEquals(user.get_full_name(), 'Foo Baz')
self.assertEqual(user.email, '[email protected]')
self.assertEqual(user.get_full_name(), 'Foo Baz')
self.assertTrue(user in ctx.group.user_set.all())
self.assertTrue(user in ctx.faculty_group.user_set.all())

Expand Down
17 changes: 14 additions & 3 deletions lti_provider/urls.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
from django.urls import re_path

from lti_provider.views import LTIConfigView, LTILandingPage, LTIRoutingView, \
LTICourseEnableView, LTIPostGrade, LTIFailAuthorization, LTICourseConfigure
from lti_provider.views import (
LTIConfigView, LTILandingPage, LTIRoutingView,
LTICourseEnableView, LTIPostGrade, LTIFailAuthorization,
LTICourseConfigure,
login, launch, get_jwks, configure
)


urlpatterns = [
Expand All @@ -17,5 +21,12 @@
re_path(r'^assignment/(?P<assignment_name>.*)/(?P<pk>\d+)/$',
LTIRoutingView.as_view(), {}, 'lti-assignment-view'),
re_path(r'^assignment/(?P<assignment_name>.*)/$',
LTIRoutingView.as_view(), {}, 'lti-assignment-view')
LTIRoutingView.as_view(), {}, 'lti-assignment-view'),

# New pylti1.3 routes
re_path(r'^login/$', login, name='lti-login'),
re_path(r'^launch/$', launch, name='lti-launch'),
re_path(r'^jwks/$', get_jwks, name='jwks'),
re_path(r'^configure/(?P<launch_id>[\w-]+)/$', configure,
name='lti-configure')
]
Loading

0 comments on commit a668e65

Please sign in to comment.