Skip to content
This repository was archived by the owner on Jun 30, 2024. It is now read-only.

Commit 674eebc

Browse files
authored
Merge pull request #1289 from bjones1/lti
LTI
2 parents 8623baa + f5be9fd commit 674eebc

File tree

6 files changed

+103
-697
lines changed

6 files changed

+103
-697
lines changed

controllers/assignments.py

Lines changed: 5 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,14 @@
1818
# Third-party imports
1919
# -------------------
2020
from psycopg2 import IntegrityError
21-
from rs_grading import do_autograde, do_calculate_totals, do_check_answer, send_lti_grade
22-
from db_dashboard import DashboardDataAnalyzer
2321
import six
2422
import bleach
2523

24+
# Local application imports
25+
# -------------------------
26+
from rs_grading import do_autograde, do_calculate_totals, do_check_answer, send_lti_grade, _get_lti_record, _try_to_send_lti_grade
27+
from db_dashboard import DashboardDataAnalyzer
28+
2629
logger = logging.getLogger(settings.logger)
2730
logger.setLevel(settings.log_level)
2831

@@ -362,39 +365,6 @@ def _autograde(sid=None, student_rownum=None, question_name=None, enforce_deadli
362365
return {'success': False, 'message': "Select an assignment before trying to autograde."}
363366

364367

365-
def _get_assignment(assignment_id):
366-
return db(db.assignments.id == assignment_id).select().first()
367-
368-
def _try_to_send_lti_grade(student_row_num, assignment_id):
369-
# try to send lti grades
370-
assignment = _get_assignment(assignment_id)
371-
if not assignment:
372-
session.flash = "Failed to find assignment object for assignment {}".format(assignment_id)
373-
return False
374-
else:
375-
grade = db(
376-
(db.grades.auth_user == student_row_num) &
377-
(db.grades.assignment == assignment_id)).select().first()
378-
if not grade:
379-
session.flash = "Failed to find grade object for user {} and assignment {}".format(auth.user.id,
380-
assignment_id)
381-
return False
382-
else:
383-
lti_record = _get_lti_record(session.oauth_consumer_key)
384-
if (not lti_record) or (not grade.lis_result_sourcedid) or (not grade.lis_outcome_url):
385-
session.flash = "Failed to send grade back to LMS (Coursera, Canvas, Blackboard...), probably because the student accessed this assignment directly rather than using a link from the LMS, or because there is an error in the assignment link in the LMS. Please report this error."
386-
return False
387-
else:
388-
# really sending
389-
# print("send_lti_grade({}, {}, {}, {}, {}, {}".format(assignment.points, grade.score, lti_record.consumer, lti_record.secret, grade.lis_outcome_url, grade.lis_result_sourcedid))
390-
send_lti_grade(assignment.points,
391-
score=grade.score,
392-
consumer=lti_record.consumer,
393-
secret=lti_record.secret,
394-
outcome_url=grade.lis_outcome_url,
395-
result_sourcedid=grade.lis_result_sourcedid)
396-
return True
397-
398368
@auth.requires_login()
399369
def student_autograde():
400370
"""
@@ -731,9 +701,6 @@ def chooseAssignment():
731701

732702
# The rest of the file is about the the spaced practice:
733703

734-
def _get_lti_record(oauth_consumer_key):
735-
return db(db.lti_keys.consumer == oauth_consumer_key).select().first()
736-
737704
def _get_course_practice_record(course_name):
738705
return db(db.course_practice.course_name == course_name).select().first()
739706

controllers/lti.py

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import uuid
2+
import six
23

3-
from applications.runestone.modules import oauth
4-
from applications.runestone.modules import oauth_store
4+
5+
from rs_grading import _try_to_send_lti_grade
6+
import oauth2
57

68

79
# For some reason, URL query parameters are being processed twice by Canvas and returned as a list, like [23, 23]. So, just take the first element in the list.
@@ -61,20 +63,26 @@ def index():
6163
masterapp = 'welcome'
6264
session.connect(request, response, masterapp=masterapp, db=db)
6365

64-
oauth_server = oauth.OAuthServer(oauth_store.LTI_OAuthDataStore(myrecord.consumer,myrecord.secret))
65-
oauth_server.add_signature_method(oauth.OAuthSignatureMethod_PLAINTEXT())
66-
oauth_server.add_signature_method(oauth.OAuthSignatureMethod_HMAC_SHA1())
67-
68-
# Use ``setting.lti_uri`` if it's defined; otherwise, use the current URI (which must be built from its components). Don't include query parameters, which causes a filure in OAuth security validation.
69-
full_uri = settings.get('lti_uri',
70-
'{}://{}{}'.format(request.env.wsgi_url_scheme,
71-
request.env.http_host, request.url))
72-
oauth_request = oauth.OAuthRequest.from_request('POST', full_uri, None,
73-
dict(request.vars), query_string=request.env.query_string)
66+
oauth_server = oauth2.Server()
67+
oauth_server.add_signature_method(oauth2.SignatureMethod_PLAINTEXT())
68+
oauth_server.add_signature_method(oauth2.SignatureMethod_HMAC_SHA1())
69+
70+
# Use ``setting.lti_uri`` if it's defined; otherwise, use the current URI (which must be built from its components). Don't include query parameters, which causes a failure in OAuth security validation.
71+
full_uri = settings.get('lti_uri', '{}://{}{}'.format(
72+
request.env.wsgi_url_scheme, request.env.http_host, request.url
73+
))
74+
oauth_request = oauth2.Request.from_request(
75+
'POST', full_uri, None, dict(request.vars),
76+
query_string=request.env.query_string
77+
)
78+
# Fix encoding -- the signed keys are in bytes, but the oauth2 Request constructor translates everything to a string. Therefore, they never compare as equal. ???
79+
if isinstance(oauth_request.get('oauth_signature'), six.string_types):
80+
oauth_request['oauth_signature'] = oauth_request['oauth_signature'].encode('utf-8')
81+
consumer = oauth2.Consumer(myrecord.consumer, myrecord.secret)
7482

7583
try:
76-
consumer, token, params = oauth_server.verify_request(oauth_request)
77-
except oauth.OAuthError as err:
84+
oauth_server.verify_request(oauth_request, consumer, None)
85+
except oauth2.Error as err:
7886
return dict(logged_in=False, lti_errors=["OAuth Security Validation failed:"+err.message, request.vars],
7987
masterapp=masterapp)
8088
consumer = None
@@ -131,12 +139,26 @@ def index():
131139
auth.login_user(user)
132140

133141
if assignment_id:
142+
# If the assignment is released, but this is the first time a student has visited the assignment, auto-upload the grade.
143+
assignment = db(db.assignments.id == assignment_id).select(
144+
db.assignments.released).first()
145+
grade = db(
146+
(db.grades.auth_user == user.id) &
147+
(db.grades.assignment == assignment_id)
148+
).select(db.grades.lis_result_sourcedid, db.grades.lis_outcome_url).first()
149+
send_grade = (assignment and assignment.released and grade and
150+
not grade.lis_result_sourcedid and
151+
not grade.lis_outcome_url)
152+
134153
# save the guid and url for reporting back the grade
135154
db.grades.update_or_insert((db.grades.auth_user == user.id) & (db.grades.assignment == assignment_id),
136155
auth_user=user.id,
137156
assignment=assignment_id,
138157
lis_result_sourcedid=result_source_did,
139158
lis_outcome_url=outcome_url)
159+
if send_grade:
160+
_try_to_send_lti_grade(user.id, assignment_id)
161+
140162
redirect(URL('assignments', 'doAssignment', vars={'assignment_id':assignment_id}))
141163

142164
elif practice:
@@ -153,3 +175,4 @@ def index():
153175
redirect(URL('assignments', 'settz_then_practice', vars={'course_name':user['course_name']}))
154176

155177
redirect(get_course_url('index.html'))
178+

models/grouped_assignments.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,14 @@
2020
Field('score', 'double'),
2121
Field('manual_total', 'boolean'),
2222
Field('projected', 'double'),
23-
Field('lis_result_sourcedid', 'string'), # guid for the student x assignment cell in the external gradebook
24-
Field('lis_outcome_url', 'string'), #web service endpoint where you send signed xml messages to insert into gradebook; guid above will be one parameter you send in that xml; the actual grade and comment will be others
23+
# guid for the student x assignment cell in the external gradebook
24+
#
25+
# Guessing that the ``lis_outcome_url`` length is actually inteded for this field, use that as its maximum length.
26+
Field('lis_result_sourcedid', 'string', length=1024),
27+
# web service endpoint where you send signed xml messages to insert into gradebook; guid above will be one parameter you send in that xml; the actual grade and comment will be others
28+
#
29+
# Per the ``LTI spec v1.1.1 <https://www.imsglobal.org/specs/ltiv1p1p1/implementation-guide>`_ in section 6, the maximum length of the ``lis_outcome_url`` field is 1023 characters.
30+
Field('lis_outcome_url', 'string', length=1024),
2531
migrate=table_migrate_prefix + 'grades.table',
2632
)
2733

0 commit comments

Comments
 (0)