Skip to content

Commit

Permalink
WIP - #636
Browse files Browse the repository at this point in the history
  • Loading branch information
nikolas committed Mar 8, 2024
1 parent 7b02d44 commit c0dfbf0
Show file tree
Hide file tree
Showing 4 changed files with 278 additions and 8 deletions.
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='game-login'),
re_path(r'^launch/$', launch, name='game-launch'),
re_path(r'^jwks/$', get_jwks, name='jwks'),
re_path(r'^configure/(?P<launch_id>[\w-]+)/$', configure,
name='lti-configure')
]
259 changes: 254 additions & 5 deletions lti_provider/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import time
import datetime
import os
import pprint

from django.conf import settings
from django.contrib import messages
Expand All @@ -15,11 +18,18 @@
from lti_provider.models import LTICourseContext
from pylti.common import LTIPostMessageException, post_message


try:
from django.urls import reverse
except ImportError:
from django.core.urlresolvers import reverse
from django.http import HttpResponse, HttpResponseForbidden, JsonResponse
from django.shortcuts import render
from django.views.decorators.http import require_POST
from django.urls import reverse
from pylti1p3.contrib.django import (
DjangoOIDCLogin, DjangoMessageLaunch, DjangoCacheDataStorage
)
from pylti1p3.deep_link_resource import DeepLinkResource
from pylti1p3.grade import Grade
from pylti1p3.lineitem import LineItem
from pylti1p3.tool_config import ToolConfJsonFile
from pylti1p3.registration import Registration


class LTIConfigView(TemplateView):
Expand Down Expand Up @@ -222,3 +232,242 @@ def post(self, request, *args, **kwargs):
messages.add_message(request, messages.INFO, msg)

return HttpResponseRedirect(redirect_url)


#
# New pylti1p3 funtionality below, adapted from pylti1.3-django-example
#
# https://github.com/dmitry-viskov/pylti1.3-django-example
#
class ExtendedDjangoMessageLaunch(DjangoMessageLaunch):

def validate_nonce(self):
"""
Probably it is bug on "https://lti-ri.imsglobal.org":
site passes invalid "nonce" value during deep links launch.
Because of this in case of iss == http://imsglobal.org just
skip nonce validation.
"""
iss = self.get_iss()
deep_link_launch = self.is_deep_link_launch()
if iss == "http://imsglobal.org" and deep_link_launch:
return self
return super().validate_nonce()


def get_lti_config_path():
return os.path.join(settings.BASE_DIR, 'configs', 'config.json')


def get_tool_conf():
tool_conf = ToolConfJsonFile(get_lti_config_path())
return tool_conf


def get_jwk_from_public_key(key_name):
key_path = os.path.join(settings.BASE_DIR, 'configs', key_name)
f = open(key_path, 'r')
key_content = f.read()
jwk = Registration.get_jwk(key_content)
f.close()
return jwk


def get_launch_data_storage():
return DjangoCacheDataStorage()


def get_launch_url(request):
target_link_uri = request.POST.get(
'target_link_uri', request.GET.get('target_link_uri'))
if not target_link_uri:
raise Exception('Missing "target_link_uri" param')
return target_link_uri


def login(request):
tool_conf = get_tool_conf()
launch_data_storage = get_launch_data_storage()

oidc_login = DjangoOIDCLogin(
request, tool_conf, launch_data_storage=launch_data_storage)
target_link_uri = get_launch_url(request)
return oidc_login\
.enable_check_cookies()\
.redirect(target_link_uri)


@require_POST
def launch(request):
tool_conf = get_tool_conf()
launch_data_storage = get_launch_data_storage()
message_launch = ExtendedDjangoMessageLaunch(
request, tool_conf, launch_data_storage=launch_data_storage)
message_launch_data = message_launch.get_launch_data()
pprint.pprint(message_launch_data)

return render(request, 'game.html', {
'page_title': 'Page Title',
'is_deep_link_launch': message_launch.is_deep_link_launch(),
'launch_data': message_launch.get_launch_data(),
'launch_id': message_launch.get_launch_id(),
'curr_user_name': message_launch_data.get('name', ''),
})


def get_jwks(request):
tool_conf = get_tool_conf()
return JsonResponse(tool_conf.get_jwks(), safe=False)


def configure(request, launch_id):
tool_conf = get_tool_conf()
launch_data_storage = get_launch_data_storage()
message_launch = ExtendedDjangoMessageLaunch.from_cache(
launch_id, request, tool_conf,
launch_data_storage=launch_data_storage)

if not message_launch.is_deep_link_launch():
return HttpResponseForbidden('Must be a deep link!')

launch_url = request.build_absolute_uri(reverse('game-launch'))

resource = DeepLinkResource()
resource.set_url(launch_url).set_title('Custom title!')

html = message_launch.get_deep_link().output_response_form([resource])
return HttpResponse(html)


@require_POST
def score(request, launch_id, earned_score, time_spent):
tool_conf = get_tool_conf()
launch_data_storage = get_launch_data_storage()
message_launch = ExtendedDjangoMessageLaunch.from_cache(
launch_id, request, tool_conf,
launch_data_storage=launch_data_storage)
resource_link_id = message_launch.get_launch_data() \
.get(
'https://purl.imsglobal.org/spec/lti/claim/resource_link', {}
).get('id')

if not message_launch.has_ags():
return HttpResponseForbidden("Don't have grades!")

sub = message_launch.get_launch_data().get('sub')
timestamp = datetime.datetime.utcnow().isoformat() + 'Z'
earned_score = int(earned_score)
time_spent = int(time_spent)

ags = message_launch.get_ags()

if ags.can_create_lineitem():
sc = Grade()
sc.set_score_given(earned_score)\
.set_score_maximum(100)\
.set_timestamp(timestamp)\
.set_activity_progress('Completed')\
.set_grading_progress('FullyGraded')\
.set_user_id(sub)

sc_line_item = LineItem()
sc_line_item.set_tag('score')\
.set_score_maximum(100)\
.set_label('Score')
if resource_link_id:
sc_line_item.set_resource_id(resource_link_id)

ags.put_grade(sc, sc_line_item)

tm = Grade()
tm.set_score_given(time_spent)\
.set_score_maximum(999)\
.set_timestamp(timestamp)\
.set_activity_progress('Completed')\
.set_grading_progress('FullyGraded')\
.set_user_id(sub)

tm_line_item = LineItem()
tm_line_item.set_tag('time')\
.set_score_maximum(999)\
.set_label('Time Taken')
if resource_link_id:
tm_line_item.set_resource_id(resource_link_id)

result = ags.put_grade(tm, tm_line_item)
else:
sc = Grade()
sc.set_score_given(earned_score) \
.set_score_maximum(100) \
.set_timestamp(timestamp) \
.set_activity_progress('Completed') \
.set_grading_progress('FullyGraded') \
.set_user_id(sub)
result = ags.put_grade(sc)

return JsonResponse({'success': True, 'result': result.get('body')})


def scoreboard(request, launch_id):
tool_conf = get_tool_conf()
launch_data_storage = get_launch_data_storage()
message_launch = ExtendedDjangoMessageLaunch.from_cache(
launch_id, request, tool_conf,
launch_data_storage=launch_data_storage)
resource_link_id = message_launch.get_launch_data() \
.get(
'https://purl.imsglobal.org/spec/lti/claim/resource_link', {}
).get('id')

if not message_launch.has_nrps():
return HttpResponseForbidden("Don't have names and roles!")

if not message_launch.has_ags():
return HttpResponseForbidden("Don't have grades!")

ags = message_launch.get_ags()

if ags.can_create_lineitem():
score_line_item = LineItem()
score_line_item.set_tag('score') \
.set_score_maximum(100) \
.set_label('Score')
if resource_link_id:
score_line_item.set_resource_id(resource_link_id)

score_line_item = ags.find_or_create_lineitem(score_line_item)
scores = ags.get_grades(score_line_item)

time_line_item = LineItem()
time_line_item.set_tag('time') \
.set_score_maximum(999) \
.set_label('Time Taken')
if resource_link_id:
time_line_item.set_resource_id(resource_link_id)

time_line_item = ags.find_or_create_lineitem(time_line_item)
times = ags.get_grades(time_line_item)
else:
scores = ags.get_grades()
times = None

members = message_launch.get_nrps().get_members()
scoreboard_result = []

for sc in scores:
result = {'score': sc['resultScore']}
if times is None:
result['time'] = 'Not set'
else:
for tm in times:
if tm['userId'] == sc['userId']:
result['time'] = tm['resultScore']
break
for member in members:
if member['user_id'] == sc['userId']:
result['name'] = member.get('name', 'Unknown')
break
scoreboard_result.append(result)

return JsonResponse(scoreboard_result, safe=False)
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"oauth2",
"oauthlib",
"pylti",
"pylti1p3",
],
scripts=[],
license="BSD",
Expand Down
9 changes: 9 additions & 0 deletions test_reqs.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,12 @@ importlib-metadata<7.1 # for flake8
entrypoints==0.4
typing_extensions==4.10.0
pyparsing==3.1.2

certifi==2024.2.2 # requests
idna==3.6 # requests
charset_normalizer==3.3.2 # requests
urllib3==2.2.1 # requests
requests==2.31.0 # pylti1p3
pyjwt==2.8.0 # pylti1p3
jwcrypto==1.5.6 # pylti1p3
pylti1p3==2.0.0

0 comments on commit c0dfbf0

Please sign in to comment.