def test_parse_delete_result_xml(self): ''' Should parse deleteRequest XML. ''' request = OutcomeRequest() request.process_xml(DELETE_RESULT_XML) self.assertEqual(request.operation, 'deleteResult') self.assertEqual(request.lis_result_sourcedid, '261-154-728-17-784') self.assertEqual(request.message_identifier, '123456789') self.assertEqual(request.score, None)
def test_has_required_attributes(self): request = OutcomeRequest() self.assertFalse(request.has_required_attributes()) request.consumer_key = 'foo' request.consumer_secret = 'bar' self.assertFalse(request.has_required_attributes()) request.lis_outcome_service_url = 'http://example.edu/' request.lis_result_sourcedid = 1 request.operation = 'baz' self.assertTrue(request.has_required_attributes())
def test_post_outcome_request(self): request_headers = {"User-Agent": "unit-test"} request = OutcomeRequest(headers=request_headers) self.assertRaises(InvalidLTIConfigError, request.post_outcome_request) request.consumer_key = 'consumer' request.consumer_secret = 'secret' request.lis_outcome_service_url = 'http://example.edu/' request.lis_result_sourcedid = 'foo' request.operation = REPLACE_REQUEST with HTTMock(response_content): resp = request.post_outcome_request(nonce='my_nonce', timestamp='1234567890') self.assertIsInstance(resp, OutcomeResponse) request = resp.post_response.request self.assertTrue('authorization' in request.headers) self.assertEqual(request.headers.get('user-agent'), b"unit-test") self.assertEqual(request.headers.get('content-type'), b"application/xml") auth_header = unquote(request.headers['authorization'].decode('utf-8')) correct = ('OAuth ' 'oauth_nonce="my_nonce", oauth_timestamp="1234567890", ' 'oauth_version="1.0", oauth_signature_method="HMAC-SHA1", ' 'oauth_consumer_key="consumer", ' 'oauth_body_hash="glWvnsZZ8lMif1ATz8Tx64CTTaY=", ' 'oauth_signature="XR6A1CmUauXZdJZXa1pJpTQi6OQ="') self.assertEqual(auth_header, correct)
def test_from_post_request(self): factory = RequestFactory() post_request = factory.post('/', data=REPLACE_RESULT_XML, content_type='application/xml') request = OutcomeRequest.from_post_request(post_request) self.assertEqual(request.operation, 'replaceResult') self.assertEqual(request.lis_result_sourcedid, '261-154-728-17-784') self.assertEqual(request.message_identifier, '123456789') self.assertEqual(request.score, '5')
def test_from_post_request(self): factory = RequestFactory() post_request = factory.post('/', data=REPLACE_RESULT_XML, content_type='application/xml' ) request = OutcomeRequest.from_post_request(post_request) self.assertEqual(request.operation, 'replaceResult') self.assertEqual(request.lis_result_sourcedid, '261-154-728-17-784') self.assertEqual(request.message_identifier, '123456789') self.assertEqual(request.score, '5')
def post_grades(self, parameters): # Secret of the external Tool consumer_key = 'aef4193cf141d02a2adde6a9d5afaff028d9bf23139a4d1def1ee224f7682ca0' consumer_secret = 'f0b884c83c858fd8078bd187c0c54c281a32df2653401b0c415dec63580dc847' # Create POST-Request outcome_request = OutcomeRequest({ 'consumer_key': consumer_key, 'consumer_secret': consumer_secret, 'lis_outcome_service_url': parameters['lis_outcome_service_url'], 'lis_result_sourcedid': parameters['lis_result_sourcedid'] }) # Replace result in Moodle outcome_response = outcome_request.post_replace_result( parameters['score'])
def update_lms_grades(request=None, sequence=None): """Send grade update to LMS (LTI Tool).""" outcome_request = OutcomeRequest().from_post_request( request) if request else OutcomeRequest() outcome_service = sequence.outcome_service if outcome_service is None: log.info( f"Sequence: {sequence} doesn't contain an outcome service, grade is not sent." ) return consumer = outcome_service.lms_lti_connection outcome_request.consumer_key = consumer.consumer_key outcome_request.consumer_secret = consumer.consumer_secret outcome_request.lis_outcome_service_url = outcome_service.lis_outcome_service_url outcome_request.lis_result_sourcedid = sequence.lis_result_sourcedid log.debug( "Update LMS grades. Used sequence = {} is completed = {}, grading_policy = {}" .format(sequence, sequence.completed, sequence.collection_order.grading_policy)) score = sequence.collection_order.grading_policy.calculate_grade(sequence) outcome_request.post_replace_result(score) lms_response = outcome_request.outcome_response user_id = sequence.lti_user if lms_response.is_success(): log.info( "Successfully sent updated grade to LMS. Student:{}, grade:{}, comment: success" .format(user_id, score)) elif lms_response.is_processing(): log.info( "Grade update is being processed by LMS. Student:{}, grade:{}, comment: processing" .format(user_id, score)) elif lms_response.has_warning(): log.warning( "Grade update response has warnings. Student:{}, grade:{}, comment: warning" .format(user_id, score)) else: log.error( "Grade update request failed. Student:{}, grade:{}, comment:{}". format(user_id, score, lms_response.code_major))
def send_grade_update(consumer_key, consumer_secret, lis_outcome_service_url, lis_result_sourcedid, score): """ Send lms grade for an lti component :param consumer_key: lti client key :param consumer_secret: lti client secret :param lis_outcome_service_url: outcome service url to send outcome request to :param lis_result_sourcedid: context identifier for consumer to use :param score: score between 0.0 and 1.0 :return: """ outcome_request = OutcomeRequest() outcome_request.consumer_key = consumer_key outcome_request.consumer_secret = consumer_secret outcome_request.lis_outcome_service_url = lis_outcome_service_url outcome_request.lis_result_sourcedid = lis_result_sourcedid # construct info string for logging args = "score={}, lis_outcome_service_url={} lis_result_sourcedid={}, consumer_key={}, consumer_secret={}, ".format( score, lis_outcome_service_url, lis_result_sourcedid, consumer_key, consumer_secret) log.debug("Updating LMS grade, with parameters: {}".format(args)) # send request to update score outcome_request.post_replace_result(score) # check out the request response lms_response = outcome_request.outcome_response # logging if lms_response.is_success(): log.info("Successfully sent updated grade to LMS. {}".format(args)) elif lms_response.is_processing(): log.info( "Grade update is being processed by LMS. {}, comment: {}".format( args, 'processing')) elif lms_response.has_warning(): log.warning( "Grade update response has warnings. {}, comment={}".format( args, 'processing')) else: log.error("Grade update request failed. {}, comment={}".format( args, lms_response.code_major)) return lms_response
def test_from_post_request(self): factory = RequestFactory() post_request = factory.post('/', data=REPLACE_RESULT_XML, content_type='application/xml') request_headers = { "User-Agent": "post-request", "Content-Type": "text/xml" } request = OutcomeRequest.from_post_request(post_request, request_headers) self.assertEqual(request.operation, 'replaceResult') self.assertEqual(request.lis_result_sourcedid, '261-154-728-17-784') self.assertEqual(request.message_identifier, '123456789') self.assertEqual(request.score, '5') self.assertEqual(request.headers.get('User-Agent'), "post-request") self.assertEqual(request.headers.get('Content-Type'), "text/xml")
def test_post_outcome_request(self): request = OutcomeRequest() self.assertRaises(InvalidLTIConfigError, request.post_outcome_request) request.consumer_key = 'consumer' request.consumer_secret = 'secret' request.lis_outcome_service_url = 'http://example.edu/' request.lis_result_sourcedid = 'foo' request.operation = REPLACE_REQUEST with HTTMock(response_content): resp = request.post_outcome_request( nonce='my_nonce', timestamp='1234567890' ) self.assertIsInstance(resp, OutcomeResponse) request = resp.post_response.request self.assertTrue('authorization' in request.headers) auth_header = unquote(request.headers['authorization'].decode('utf-8')) correct = ('OAuth ' 'oauth_nonce="my_nonce", oauth_timestamp="1234567890", ' 'oauth_version="1.0", oauth_signature_method="HMAC-SHA1", ' 'oauth_consumer_key="consumer", ' 'oauth_body_hash="glWvnsZZ8lMif1ATz8Tx64CTTaY=", ' 'oauth_signature="XR6A1CmUauXZdJZXa1pJpTQi6OQ="') self.assertEqual(auth_header, correct)
def run(self): # Load old tasks from the database for todo in self._database.lis_outcome_queue.find({}): self._add_to_queue(todo) try: while not self._stopped: time.sleep(0.5) data = self._queue.get() mongo_id, username, courseid, taskid, consumer_key, service_url, result_id, nb_attempt = data try: course = self._course_factory.get_course(courseid) task = course.get_task(taskid) consumer_secret = course.lti_keys()[consumer_key] grade = self._user_manager.get_task_cache( username, task.get_course_id(), task.get_id())["grade"] grade = grade / 100.0 if grade > 1: grade = 1 if grade < 0: grade = 0 except Exception: self._logger.error( "An exception occurred while getting a course/LTI secret/grade in LTIOutcomeManager.", exc_info=True) continue try: outcome_response = OutcomeRequest({ "consumer_key": consumer_key, "consumer_secret": consumer_secret, "lis_outcome_service_url": service_url, "lis_result_sourcedid": result_id }).post_replace_result(grade) if outcome_response.code_major == "success": self._delete_in_db(mongo_id) self._logger.debug("Successfully sent grade to TC: %s", str(data)) continue except Exception: self._logger.error( "An exception occurred while sending a grade to the TC.", exc_info=True) if nb_attempt < 5: self._logger.debug( "An error occurred while sending a grade to the TC. Retrying..." ) self._increment_attempt(mongo_id) else: self._logger.error( "An error occurred while sending a grade to the TC. Maximum number of retries reached." ) self._delete_in_db(mongo_id) except KeyboardInterrupt: pass
def callback_sequence_item_grade(request): outcome_response = OutcomeResponse(message_identifier='unknown', code_major=CODE_MAJOR_CODES[2], severity=SEVERITY_CODES[0]) try: outcome_request = OutcomeRequest().from_post_request(request) score = float(outcome_request.score) if not 0.0 <= score <= 1.0: raise InvalidLTIConfigError( '[LTI] score value is outside the permitted range of 0.0-1.0') operation = outcome_request.operation if not operation == 'replaceResult': raise InvalidLTIConfigError( '[LTI] request operation {} cannot be proceed'.format( operation)) except (InvalidLTIConfigError, ValueError) as err: body = escape(request.body) if request.body else '' error_message = "Request body XML parsing error: {} {}".format( err.message, body) log.debug("Failure to archive grade from the source: %s" + error_message) outcome_response.description = escape(error_message) return HttpResponse(outcome_response.generate_response_xml(), content_type='application/xml') sequence_item_id, user_id, _activity, _suffix = outcome_request.lis_result_sourcedid.text.split( ':') outcome_response.code_major = CODE_MAJOR_CODES[0] outcome_response.description = 'Score for {sourced_id} is now {score}'.format( sourced_id=outcome_request.lis_result_sourcedid, score=score) outcome_response.message_identifier = outcome_request.message_identifier outcome_response.operation = operation xml = outcome_response.generate_response_xml() log.debug( "Received CallBack with the submitted answer for sequence item {}.". format(sequence_item_id)) try: sequence_item = SequenceItem.objects.get(id=sequence_item_id) except SequenceItem.DoesNotExist: error_message = "Sequence Item with the ID={} was not found".format( sequence_item_id) outcome_response.description = escape(error_message) log.debug("[LTI] {}".format(error_message)) return HttpResponseNotFound(outcome_response.generate_response_xml(), content_type='application/xml') sequence_item.score = score sequence_item.save() log.debug("[LTI] Sequence item {} grade is updated".format(sequence_item)) last_log_submit = Log.objects.filter(sequence_item=sequence_item, log_type='S').last() attempt = (last_log_submit.attempt if last_log_submit else 0) + 1 correct = bool(score) Log.objects.create( sequence_item=sequence_item, log_type=Log.SUBMITTED, answer=correct, attempt=attempt, ) log.debug( "New Log is created log_type: 'Submitted', attempt: {}, correct: {}, sequence is completed: {}" .format(attempt, correct, sequence_item.sequence.completed)) message_to_consumer = {"sequence_status": "updated"} sequence = sequence_item.sequence if sequence_item.sequence.collection_order.ui_option: ui_details = sequence_item.sequence.sequence_ui_details() message_to_consumer["ui_details"] = ui_details NextButtonConsumer.send_message_to_channel( f'{sequence_item.id}_{sequence_item.position}', message_to_consumer) if sequence.lis_result_sourcedid: policy = sequence.collection_order.grading_policy.policy_instance( sequence=sequence, request=request, user_id=user_id) policy.send_grade() return HttpResponse(xml, content_type="application/xml")