Skip to content
This repository has been archived by the owner on Sep 5, 2023. It is now read-only.

Commit

Permalink
refactor: Update control file logic for LTI 1.1 (#524)
Browse files Browse the repository at this point in the history
  • Loading branch information
jgwerner authored Mar 15, 2021
1 parent 04ad4bf commit 638a73e
Show file tree
Hide file tree
Showing 8 changed files with 275 additions and 113 deletions.
84 changes: 63 additions & 21 deletions src/grader-service/grader-service/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,14 @@
import shutil
import sys

from secrets import token_hex

from flask import jsonify

from pathlib import Path

from illumidesk.grades.senders import LTIGradesSenderControlFile

from . import create_app
from .models import db
from .models import GraderService
Expand All @@ -32,14 +36,53 @@
app = create_app()


@app.route(
'/control-file/<assignment_name>/<lis_outcome_service_url>/<lis_result_sourcedid>/<lms_user_id>/<course_id>',
methods=['POST'],
)
def register_control_file(
assignment_name: str, lis_outcome_service_url: str, lis_result_sourcedid: str, lms_user_id: str, course_id: str
):
"""
Creates a new grades control file
Args:
assignment_name: string representation of the assignment name from the LMS (normalized)
lis_outcome_service_url: url endpoint that is used to send grades to the LMS with LTI 1.1
lis_result_sourcedid: unique assignment or module identifier used with LTI 1.1
lms_user_id: unique (opaque) user id
course_id: the course id within the lms
Returns:
JSON: True/False on whether or not the grader service was successfully launched
example:
```
{
success: "True"
}
```
"""
# launcher = GraderServiceLauncher(org_name=org_name, course_id=course_id)
# if not launcher.grader_deployment_exists():
try:
control_file = LTIGradesSenderControlFile(f'/home/grader-{course_id}/{course_id}')
control_file.register_data(assignment_name, lis_outcome_service_url, lis_result_sourcedid, lms_user_id)
except Exception as e:
return jsonify(success=False, message=str(e)), 500

else:
return jsonify(success=True)


@app.route('/services/<org_name>/<course_id>', methods=['POST'])
def launch(org_name: str, course_id: str):
"""
Creates a new grader-notebook pod if not exists
Creates a new grader-notebook pod running as a JupyterHub unmanaged service if one does not exist.
Args:
org_name: the organization name
course_id: the grader's course id (label)
course_id: the grader's course id
Returns:
JSON: True/False on whether or not the grader service was successfully launched
Expand All @@ -51,26 +94,25 @@ def launch(org_name: str, course_id: str):
}
```
"""
launcher = GraderServiceLauncher(org_name=org_name, course_id=course_id)
if not launcher.grader_deployment_exists():
try:
launcher.create_grader_deployment()
# Register the new service to local database
new_service = GraderService(
name=course_id,
course_id=course_id,
url=f'http://{launcher.grader_name}:8888',
api_token=launcher.grader_token,
)
db.session.add(new_service)
db.session.commit()
# then do patch for jhub deployment
# with this the jhub pod will be restarted and get/load new services
launcher.update_jhub_deployment()
except Exception as e:
return jsonify(success=False, message=str(e)), 500
# launcher = GraderServiceLauncher(org_name=org_name, course_id=course_id)
# if not launcher.grader_deployment_exists():
try:
# launcher.create_grader_deployment()
# Register the new service to local database
new_service = GraderService(
name=course_id,
course_id=course_id,
url=f'http://{course_id}:8888',
api_token=token_hex(32),
)
db.session.add(new_service)
db.session.commit()
# then do patch for jhub deployment
# with this the jhub pod will be restarted and get/load new services
# launcher.update_jhub_deployment()
except Exception as e:
return jsonify(success=False, message=str(e)), 500

return jsonify(success=True)
else:
return jsonify(success=False, message=f'A grader service already exists for this course_id:{course_id}'), 409

Expand Down
2 changes: 2 additions & 0 deletions src/grader-service/grader-service/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,7 @@ class GraderService(db.Model):
admin = db.Column(db.Boolean, default=True)
api_token = db.Column(db.String(150), nullable=True)

db.create_all()

def __repr__(self):
return "<Service name: {} at {}>".format(self.name, self.url)
37 changes: 36 additions & 1 deletion src/illumidesk/illumidesk/apis/setup_course_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ async def create_assignment_source_dir(org_name: str, course_id: str, assignment
return False


async def register_new_service(org_name: str, course_id: str) -> Bool:
async def register_new_service(org_name: str, course_id: str) -> bool:
"""
Helps to register (asynchronously) new course definition through the grader setup service
Args:
Expand All @@ -67,3 +67,38 @@ async def register_new_service(org_name: str, course_id: str) -> Bool:
# the response can be found in e.response.
logger.error(f'Grader-setup service returned an error: {e}')
return False


async def register_control_file(
assignment_name: str, lis_outcome_service_url: str, lis_result_sourcedid: str, lms_user_id: str, course_id: str
) -> bool:
"""
Helps to register the control file to keep track of assignments and resource id's with the LMS to
send grades.
Args:
assignment_name: string representation of the assignment name from the LMS (normalized)
lis_outcome_service_url: url endpoint that is used to send grades to the LMS with LTI 1.1
lis_result_sourcedid: unique assignment or module identifier used with LTI 1.1
lms_user_id: unique (opaque) user id
course_id: the course id within the lms
Returns: True when a new the control file was created and saved, false otherwise
"""

client = AsyncHTTPClient()
try:
response = await client.fetch(
f'{SERVICE_BASE_URL}/control-file/{assignment_name}/{lis_outcome_service_url}/{lis_result_sourcedid}/{lms_user_id}/{course_id}',
headers=SERVICE_COMMON_HEADERS,
body='',
method='POST',
)
logger.debug(f'Grader-setup service response: {response.body}')
return True
except HTTPError as e:
# HTTPError is raised for non-200 responses
# the response can be found in e.response.
logger.error(f'Grader-setup service returned an error: {e}')
return False
41 changes: 31 additions & 10 deletions src/illumidesk/illumidesk/authenticators/authenticator.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from illumidesk.apis.jupyterhub_api import JupyterHubAPI
from illumidesk.apis.nbgrader_service import NbGraderServiceHelper
from illumidesk.apis.setup_course_service import register_new_service
from illumidesk.apis.setup_course_service import register_control_file
from illumidesk.apis.setup_course_service import create_assignment_source_dir

from illumidesk.authenticators.handlers import LTI11AuthenticateHandler
Expand All @@ -30,8 +31,6 @@
from illumidesk.authenticators.validator import LTI11LaunchValidator
from illumidesk.authenticators.validator import LTI13LaunchValidator

from illumidesk.grades.senders import LTIGradesSenderControlFile


logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
Expand Down Expand Up @@ -72,17 +71,39 @@ async def setup_course_hook(
username = lti_utils.normalize_string(authentication['name'])
lms_user_id = authentication['auth_state']['lms_user_id']
user_role = authentication['auth_state']['user_role']

# lti 1.1 specific items
lis_outcome_service_url = ''
lis_result_sourcedid = ''
assignment_name = ''
if 'lis_outcome_service_url' in authentication['auth_state']['lis_outcome_service_url']:
lis_outcome_service_url = authentication['auth_state']['lis_outcome_service_url']
if 'lis_result_sourcedid' in authentication['auth_state']['lis_outcome_service_url']:
lis_result_sourcedid = authentication['auth_state']['lis_outcome_service_url']
if 'assignment_name' in authentication['auth_state']['assignment_name']:
assignment_name = lti_utils.normalize_string(authentication['auth_state']['assignment_name'])

# register the user (it doesn't matter if it is a student or instructor) with her/his lms_user_id in nbgrader
nb_service.add_user_to_nbgrader_gradebook(username, lms_user_id)
# TODO: verify the logic to simplify groups creation and membership
if user_is_a_student(user_role):
# assign the user to 'nbgrader-<course_id>' group in jupyterhub and gradebook
await jupyterhub_api.add_student_to_jupyterhub_group(course_id, username)
# add or update the lti 1.1 grader control file
if lis_outcome_service_url and lis_result_sourcedid:
_ = await register_control_file(
lis_outcome_service_url=lis_outcome_service_url,
lis_result_sourcedid=lis_result_sourcedid,
assignment_name=assignment_name,
course_id=course_id,
lms_user_id=lms_user_id,
)
elif user_is_an_instructor(user_role):
# assign the user in 'formgrade-<course_id>' group
await jupyterhub_api.add_instructor_to_jupyterhub_group(course_id, username)
# launch the new (?) grader-notebook as a service
setup_response = await register_new_service(org_name=ORG_NAME, course_id=course_id)

# launch the new grader-notebook as a service
_ = await register_new_service(org_name=ORG_NAME, course_id=course_id)

return authentication

Expand Down Expand Up @@ -241,10 +262,7 @@ async def authenticate(self, handler: BaseHandler, data: Dict[str, str] = None)
lis_outcome_service_url = args['lis_outcome_service_url']
if 'lis_result_sourcedid' in args and args['lis_result_sourcedid']:
lis_result_sourcedid = args['lis_result_sourcedid']
# only if both values exist we can register them to submit grades later
if lis_outcome_service_url and lis_result_sourcedid:
control_file = LTIGradesSenderControlFile(f'/home/grader-{course_id}/{course_id}')
control_file.register_data(assignment_name, lis_outcome_service_url, lms_user_id, lis_result_sourcedid)

# Assignment creation
if assignment_name:
nbgrader_service = NbGraderServiceHelper(course_id, True)
Expand All @@ -259,9 +277,12 @@ async def authenticate(self, handler: BaseHandler, data: Dict[str, str] = None)
return {
'name': username_normalized,
'auth_state': {
'assignment_name': assignment_name,
'course_id': course_id,
'lms_user_id': lms_user_id,
'user_role': user_role,
'lis_outcome_service_url': lis_outcome_service_url,
'lis_result_sourcedid': lis_result_sourcedid,
}, # noqa: E231
}

Expand Down Expand Up @@ -379,7 +400,7 @@ async def authenticate( # noqa: C901
]
# if there is a resource link request then process additional steps
if not validator.is_deep_link_launch(jwt_decoded):
await process_resource_link(self.log, course_id, jwt_decoded)
await process_resource_link_lti_13(self.log, course_id, jwt_decoded)

lms_user_id = jwt_decoded['sub'] if 'sub' in jwt_decoded else username

Expand All @@ -398,7 +419,7 @@ async def authenticate( # noqa: C901
}


async def process_resource_link(
async def process_resource_link_lti_13(
logger: Any,
course_id: str,
jwt_body_decoded: Dict[str, Any],
Expand Down
2 changes: 1 addition & 1 deletion src/illumidesk/illumidesk/authenticators/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,6 @@
}

# the list of roles that recognize a user as a Student
DEFAULT_ROLE_NAMES_FOR_STUDENT = ['student', 'learner']
DEFAULT_ROLE_NAMES_FOR_STUDENT = ['student', 'learner', 'Student', 'Learner']
# the list of roles that recognize a user as an Instructor
DEFAULT_ROLE_NAMES_FOR_INSTRUCTOR = ['instructor', 'urn:lti:role:ims/lis/teachingassistant']
Loading

0 comments on commit 638a73e

Please sign in to comment.