def test_parse_role_lti_label(self): self.assertEqual([Role.STUDENT], Role.parse_role_lti("Student")) self.assertEqual([Role.STUDENT, Role.ALUMNI], Role.parse_role_lti("Student, Alumni")) self.assertEqual([Role.STUDENT, Role.ALUMNI], Role.parse_role_lti("Student ,Alumni")) self.assertEqual([Role.STUDENT, Role.ALUMNI], Role.parse_role_lti("Student,Alumni")) self.assertEqual([Role.STUDENT, Role.ALUMNI], Role.parse_role_lti(" Student , Alumni "))
def test_parse_role_unkown_multiple(self, logger): self.assertEqual( [Role.STUDENT, Role.ALUMNI], Role.parse_role_lti("Student, Unknown1, Unknown2, Alumni")) logger.warning.assert_has_calls([ mock.call("Received unknown LTI role: 'Unknown1'"), mock.call("Received unknown LTI role: 'Unknown2'"), ])
def test_parse_role_lti_label_URN(self): self.assertEqual([Role.STUDENT, Role.ALUMNI], Role.parse_role_lti( "urn:lti:instrole:ims/lis/Student, " "Alumni")) self.assertEqual([Role.STUDENT, Role.ALUMNI], Role.parse_role_lti( "urn:lti:instrole:ims/lis/Student ," "Alumni")) self.assertEqual([Role.STUDENT, Role.ALUMNI], Role.parse_role_lti( "urn:lti:instrole:ims/lis/Student," "Alumni")) self.assertEqual([Role.STUDENT, Role.ALUMNI], Role.parse_role_lti( " urn:lti:instrole:ims/lis/Student , " "Alumni "))
def get_or_create_user(wclass_db: WimsClass, wclass: wimsapi.Class, parameters: Dict[str, Any] ) -> Tuple[WimsUser, wimsapi.User]: """Get the WIMS' user database and wimsapi.User instances, create them if they does not exists. If at least one of the roles in the LTI request's parameters is in ROLES_ALLOWED_CREATE_WIMS_CLASS, the user will be connected as supervisor. Raises: - wimsapi.WimsAPIError if the WIMS' server denied a request. - requests.RequestException if the WIMS server could not be joined. Returns a tuple (user_db, user) where user_db is an instance of models.WimsUser and user an instance of wimsapi.User.""" try: role = Role.parse_role_lti(parameters["roles"]) if is_teacher(role): user_db = WimsUser.objects.get(lms_guid=None, wclass=wclass_db) else: user_db = WimsUser.objects.get(lms_guid=parameters['user_id'], wclass=wclass_db) user = wimsapi.User.get(wclass, user_db.quser) except WimsUser.DoesNotExist: user = create_user(parameters) i = 0 while True: try: wclass.additem(user) break except wimsapi.WimsAPIError as e: # Raised if an user with the same quser already exists, # in this case, keep trying by appending integer to quser (jdoe, jdoe1, # jdoe2, ...), stopping after 100 tries. # Can also be raised if an error occured while communicating with the # WIMS server, hence the following test. if "user already exists" not in str(e) or i >= 100: # pragma: no cover raise if i: user.quser = user.quser[:-len(str(i))] i += 1 user.quser += str(i) user_db = WimsUser.objects.create( lms_guid=parameters["user_id"], wclass=wclass_db, quser=user.quser ) logger.info("New user created (wims id: %s - lms id : %s) in class %d" % (user.quser, str(user_db.lms_guid), wclass_db.id)) return user_db, user
def wims_exam(request: HttpRequest, wims_pk: int, exam_pk: int) -> HttpResponse: """Redirect the client to the WIMS server corresponding to <wims_pk> and exam <exam_pk>. Will retrieve/create the right WIMS' class/user according to informations in request.POST. Raises: - Http404 if no LMS corresponding to request.POST["tool_consumer_instance_guid"] has been found in the database. - PermissionDenied if the class corresponding to request.POST['context_id'] does not exists and roles in request.POST['context_id'] are not one of settings.ROLES_ALLOWED_CREATE_WIMS_CLASS. Returns: - HttpResponseRedirect redirecting the user to WIMS, logged in his WIMS' class. - HttpResponse(status=502) if an error occured while communicating with the WIMS. - HttpResponse(status=504) if the WIMS server could not be joined.""" if request.method == "GET": uri = request.build_absolute_uri() return HttpResponseNotAllowed(["POST"], GET_ERROR_MSG % (uri[:-1], uri)) if request.method != "POST": return HttpResponseNotAllowed( ["POST"], "405 Method Not Allowed: '%s'" % request.method) try: parameters = parse_parameters(request.POST) logger.info("Request received from '%s'" % request.META.get('HTTP_REFERER', "Unknown")) check_parameters(parameters) check_custom_parameters(parameters) is_valid_request(request) except BadRequestException as e: logger.info(str(e)) return HttpResponseBadRequest(str(e)) # Retrieve the WIMS server try: wims_srv = WIMS.objects.get(pk=wims_pk) except WIMS.DoesNotExist: raise Http404("Unknown WIMS server of id '%d'" % wims_pk) # Retrieve the LMS try: lms = LMS.objects.get(guid=parameters["tool_consumer_instance_guid"]) except LMS.DoesNotExist: raise Http404("No LMS found with guid '%s'" % parameters["tool_consumer_instance_guid"]) wapi = wimsapi.WimsAPI(wims_srv.url, wims_srv.ident, wims_srv.passwd) try: # Check that the WIMS server is available bol, response = wapi.checkident(verbose=True) if not bol: raise wimsapi.WimsAPIError(response['message']) # Get the class wclass_db = WimsClass.objects.get(wims=wims_srv, lms=lms, lms_guid=parameters['context_id']) try: wclass = wimsapi.Class.get(wims_srv.url, wims_srv.ident, wims_srv.passwd, wclass_db.qclass, wims_srv.rclass, timeout=settings.WIMSAPI_TIMEOUT) except wimsapi.WimsAPIError as e: if "not existing" in str( e): # Class was deleted on the WIMS server qclass = wclass_db.qclass logger.info(( "Deleting class (id : %d - wims id : %s - lms id : %s) as it was" "deleted from the WIMS server") % (wclass_db.id, str( wclass_db.qclass), str(wclass_db.lms_guid))) wclass_db.delete() return HttpResponseNotFound(( "Class of ID %s could not be found on the WIMS server. Maybe it has been " "deleted from the WIMS server. Use this LTI link on your LMS to create a new " "WIMS class: %s") % (qclass, request.build_absolute_uri( reverse("lti:wims_class", args=[wims_pk])))) raise # Unknown error (pragma: no cover) # Check whether the user already exists, creating it otherwise user_db, user = get_or_create_user(wclass_db, wclass, parameters) # Check whether the exam already exists, creating it otherwise exam_db, exam = get_exam(wclass_db, wclass, exam_pk, parameters) if int(exam.exammode) not in [1, 2]: # not active or expired return HttpResponseForbidden( "This exam (%s) is currently unavailable (%s)" % (str(exam.qexam), MODE[int(exam.exammode)])) # Storing the URL and ID to send the grade back to the LMS try: gl = GradeLinkExam.objects.get(user=user_db, activity=exam_db) gl.sourcedid = parameters["lis_result_sourcedid"] gl.url = parameters["lis_outcome_service_url"] gl.save() except GradeLinkExam.DoesNotExist: GradeLinkExam.objects.create( user=user_db, activity=exam_db, lms=lms, sourcedid=parameters["lis_result_sourcedid"], url=parameters["lis_outcome_service_url"]) # If user is a teacher, send all grade back to the LMS role = Role.parse_role_lti(parameters["roles"]) if is_teacher(role): GradeLinkExam.send_back_all(exam_db) # Trying to authenticate the user on the WIMS server bol, response = wapi.authuser(wclass.qclass, wclass.rclass, user.quser) if not bol: # pragma: no cover raise wimsapi.WimsAPIError(response['message']) params = ( "&lang=%s&module=adm%%2Fclass%%2Fexam&+job=student&+exam=%s" % (wclass.lang, str(exam.qexam))) url = response["home_url"] + params except WimsClass.DoesNotExist as e: logger.info(str(e)) return HttpResponseNotFound("Could not find class of id '%s'" % parameters['context_id']) except wimsapi.WimsAPIError as e: # WIMS server responded with ERROR logger.info(str(e)) return HttpResponse(str(e), status=502) except requests.RequestException: logger.exception("Could not join the WIMS server '%s'" % wims_srv.url) return HttpResponse("Could not join the WIMS server '%s'" % wims_srv.url, status=504) return redirect(url)
def test_parse_role_unkown_one(self, logger): self.assertEqual([], Role.parse_role_lti("Unknown")) logger.warning.assert_called_with( "Received unknown LTI role: 'Unknown'")
def get_or_create_class(lms: LMS, wims_srv: WIMS, wapi: wimsapi.WimsAPI, parameters: Dict[str, Any]) -> Tuple[WimsClass, wimsapi.Class]: """Get the WIMS' class database and wimsapi.Class instances, create them if they does not exists. Raises: - exceptions.PermissionDenied if the class does not exists and none of the roles in the LTI request's parameters is in ROLES_ALLOWED_CREATE_WIMS_CLASS. - wimsapi.WimsAPIError if the WIMS' server denied a request. - requests.RequestException if the WIMS server could not be joined. Returns a tuple (wclass_db, wclass) where wclas_db is an instance of models.WimsClass and wclass an instance of wimsapi.Class.""" try: wclass_db = WimsClass.objects.get(wims=wims_srv, lms=lms, lms_guid=parameters['context_id']) try: wclass = wimsapi.Class.get(wapi.url, wapi.ident, wapi.passwd, wclass_db.qclass, wims_srv.rclass) except wimsapi.WimsAPIError as e: if "not existing" in str(e): # Class was deleted on the WIMS server logger.info(("Deleting class (id : %d - wims id : %s - lms id : %s) as it was" "deleted from the WIMS server.") % (wclass_db.id, str(wclass_db.qclass), str(wclass_db.lms_guid))) wclass_db.delete() raise WimsClass.DoesNotExist raise # Unknown error (pragma: no cover) except WimsClass.DoesNotExist: role = Role.parse_role_lti(parameters["roles"]) if not is_teacher(role): logger.warning(str(role)) msg = ("You must have at least one of these roles to create a Wims class: %s. Your " "roles: %s") msg %= (str([r.value for r in settings.ROLES_ALLOWED_CREATE_WIMS_CLASS]), str([r.value for r in role])) raise PermissionDenied(msg) wclass = create_class(wims_srv, parameters) wclass.save(wapi.url, wapi.ident, wapi.passwd, timeout=settings.WIMSAPI_TIMEOUT) wclass_db = WimsClass.objects.create( lms=lms, lms_guid=parameters["context_id"], wims=wims_srv, qclass=wclass.qclass, name=wclass.name ) logger.info("New class created (id : %d - wims id : %s - lms id : %s)" % (wclass_db.id, str(wclass.qclass), str(wclass_db.lms_guid))) WimsUser.objects.create(wclass=wclass_db, quser="******") logger.info("New user created (wims id : supervisor - lms id : None) in class %d" % wclass_db.id) try: title, body = generate_mail(wclass_db, wclass) send_mail( title, body, settings.SERVER_EMAIL, [wclass.supervisor.email], ) except Exception: logger.exception("An exception occurred while sending email:") return wclass_db, wclass