def post(self): if not XAPI.enabled(): # this should silently fail abort(404) raw_params = request.get_json(force=True) params = {} course_uuid = raw_params.get('course_id') course = _get_valid_course(course_uuid) # add required params for param in ['verb', 'object']: if not raw_params.get(param): abort(400) params[param] = raw_params.get(param) # add optional params for param in ['context', 'result', 'timestamp']: if raw_params.get(param): params[param] = raw_params.get(param) statement = XAPIStatement.generate_from_params(current_user, params, course=course) XAPI.emit(statement) return {'success': True}
def emit_lrs_xapi_statement(self, xapi_log_id): from compair.learning_records import XAPI xapi_log = XAPILog.query \ .filter_by( id=xapi_log_id, transmitted=False ) \ .one_or_none() if xapi_log: try: XAPI._emit_to_lrs(json.loads(xapi_log.statement)) except socket.error as error: # don't raise connection refused error when in eager mode if error.errno != socket.errno.ECONNREFUSED: current_app.logger.error( "emit_lrs_xapi_statement connection refused: " + socket.error.strerror) return raise error XAPILog.query \ .filter_by(id=xapi_log_id) \ .delete() db.session.commit()
def resend_learning_records(self): from compair.learning_records import XAPI, CaliperSensor # only re-send learning records that have last started over an hour ago # (this is to try and prevent sending duplicates if possible) one_hour_ago = datetime.datetime.utcnow() - datetime.timedelta(hours=1) if XAPI.enabled() and not XAPI.storing_locally(): xapi_logs = XAPILog.query \ .filter(and_( XAPILog.transmitted == False, XAPILog.modified <= one_hour_ago )) \ .all() for xapi_log in xapi_logs: emit_lrs_xapi_statement(xapi_log.id) if CaliperSensor.enabled() and not CaliperSensor.storing_locally(): caliper_logs = CaliperLog.query \ .filter(and_( CaliperLog.transmitted == False, CaliperLog.modified <= one_hour_ago )) \ .all() for caliper_log in caliper_logs: emit_lrs_caliper_event(caliper_log.id)
def resend_learning_records(self): from compair.learning_records import XAPI, CaliperSensor # only re-send learning records that have last started over an hour ago # (this is to try and prevent sending duplicates if possible) one_hour_ago = datetime.datetime.utcnow() - datetime.timedelta(hours=1) if XAPI.enabled() and not XAPI.storing_locally(): xapi_logs = XAPILog.query \ .filter(and_( XAPILog.transmitted == False, XAPILog.modified <= one_hour_ago )) \ .all() for xapi_log in xapi_logs: emit_lrs_xapi_statement.delay(xapi_log.id) if CaliperSensor.enabled() and not CaliperSensor.storing_locally(): caliper_logs = CaliperLog.query \ .filter(and_( CaliperLog.transmitted == False, CaliperLog.modified <= one_hour_ago )) \ .all() for caliper_log in caliper_logs: emit_lrs_caliper_event.delay(caliper_log.id)
def post(self): if not XAPI.enabled(): # this should silently fail abort(404) params = xapi_statement_parser.parse_args() course_uuid = params.pop('course_id') course = _get_valid_course(course_uuid) statement = XAPIStatement.generate_from_params(current_user, params, course=course) XAPI.emit(statement) return { 'success': True }
def post(self): if not XAPI.enabled(): # this should silently fail abort(404) params = xapi_statement_parser.parse_args() course_uuid = params.pop('course_id') course = _get_valid_course(course_uuid) statement = XAPIStatement.generate_from_params(current_user, params, course=course) XAPI.emit(statement) return {'success': True}
def emit_lrs_xapi_statement(self, xapi_log_id): from compair.learning_records import XAPI xapi_log = XAPILog.query.filter_by(id=xapi_log_id).one_or_none() if xapi_log: try: XAPI._emit_to_lrs(json.loads(xapi_log.statement)) except socket.error as error: # don't raise connection refused error when in eager mode if error.errno != socket.errno.ECONNREFUSED: return raise error xapi_log.transmitted = True db.session.commit()
def test_send_remote_xapi_statement(self, mocked_save_statement): self.app.config['CALIPER_ENABLED'] = False def save_statement_override(statement): self.sent_xapi_statement = json.loads( statement.to_json(XAPI._version)) return LRSResponse( success=True, request=None, response=None, data=json.dumps(["123"]), ) mocked_save_statement.side_effect = save_statement_override expected_course = { 'id': "https://localhost:8888/app/course/" + self.course.uuid, 'definition': { 'type': 'http://adlnet.gov/expapi/activities/course', 'name': { 'en-US': self.course.name } }, 'objectType': 'Activity' } expected_sis_course = { 'id': 'https://localhost:8888/course/' + self.lti_context.lis_course_offering_sourcedid, 'objectType': 'Activity' } expected_sis_section = { 'id': 'https://localhost:8888/course/' + self.lti_context.lis_course_offering_sourcedid + '/section/' + self.lti_context.lis_course_section_sourcedid, 'objectType': 'Activity' } expected_assignment = { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid, 'definition': { 'type': 'http://adlnet.gov/expapi/activities/assessment', 'name': { 'en-US': self.assignment.name }, 'description': { 'en-US': self.assignment.description }, }, 'objectType': 'Activity' } expected_assignment_question = { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/question", 'definition': { 'type': 'http://adlnet.gov/expapi/activities/question', 'name': { 'en-US': self.assignment.name }, 'description': { 'en-US': self.assignment.description }, }, 'objectType': 'Activity' } expected_attempt = { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/question/attempt/" + self.answer.attempt_uuid, 'definition': { 'type': 'http://adlnet.gov/expapi/activities/attempt', 'extensions': { 'http://id.tincanapi.com/extension/attempt': { 'duration': "PT05M00S", 'startedAtTime': self.answer.attempt_started.replace( tzinfo=pytz.utc).isoformat(), 'endedAtTime': self.answer.attempt_ended.replace( tzinfo=pytz.utc).isoformat(), } } }, 'objectType': 'Activity' } expected_answer = { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/answer/" + self.answer.uuid, 'definition': { 'type': 'http://id.tincanapi.com/activitytype/solution', 'extensions': { 'http://id.tincanapi.com/extension/isDraft': False } }, 'objectType': 'Activity' } expected_answer_comment = { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/answer/" + self.answer.uuid + "/comment/" + self.answer_comment.uuid, 'definition': { 'type': 'http://activitystrea.ms/schema/1.0/comment', 'name': { 'en-US': "Assignment answer comment" }, 'extensions': { 'http://id.tincanapi.com/extension/isDraft': False, 'http://id.tincanapi.com/extension/type': self.answer_comment.comment_type.value } }, 'objectType': 'Activity' } expected_verb = { 'id': 'http://activitystrea.ms/schema/1.0/submit', 'display': { 'en-US': 'submitted' } } expected_context = { 'contextActivities': { 'parent': [expected_assignment_question, expected_attempt], 'grouping': [ expected_assignment, expected_course, expected_sis_course, expected_sis_section ] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } } expected_result = { 'success': True, 'response': self.answer.content, 'extensions': { 'http://xapi.learninganalytics.ubc.ca/extension/character-count': len(self.answer.content), 'http://xapi.learninganalytics.ubc.ca/extension/word-count': len(self.answer.content.split(" ")) } } expected_statement = { "actor": self.get_compair_xapi_actor(self.user), "verb": expected_verb, "object": expected_answer, "context": expected_context, "result": expected_result } # test with answer normal content statement = XAPIStatement.generate( user=self.user, verb=XAPIVerb.generate('submitted'), object=XAPIObject.answer(self.answer), context=XAPIContext.answer(self.answer), result=XAPIResult.basic_content(self.answer.content, success=True)) XAPI._emit_to_lrs(json.loads(statement.to_json(XAPI._version))) self._validate_and_cleanup_xapi_statement(self.sent_xapi_statement) self.assertEqual(self.sent_xapi_statement, expected_statement) # test with extremely long answer content # content should be ~ LRS_USER_INPUT_FIELD_SIZE_LIMIT bytes long + 100 characters content = "c" * (self.character_limit + 100) self.answer.content = content db.session.commit() # expected_answer content should be <= LRS_USER_INPUT_FIELD_SIZE_LIMIT bytes long + " [TEXT TRIMMED]..." expected_result['response'] = ( "c" * self.character_limit) + " [TEXT TRIMMED]..." expected_result['extensions'][ 'http://xapi.learninganalytics.ubc.ca/extension/word-count'] = 1 expected_result['extensions'][ 'http://xapi.learninganalytics.ubc.ca/extension/character-count'] = len( content) statement = XAPIStatement.generate( user=self.user, verb=XAPIVerb.generate('submitted'), object=XAPIObject.answer(self.answer), context=XAPIContext.answer(self.answer), result=XAPIResult.basic_content(self.answer.content, success=True)) XAPI._emit_to_lrs(json.loads(statement.to_json(XAPI._version))) self._validate_and_cleanup_xapi_statement(self.sent_xapi_statement) self.assertEqual(self.sent_xapi_statement, expected_statement) expected_verb = { 'id': 'http://activitystrea.ms/schema/1.0/update', 'display': { 'en-US': 'updated' } } expected_result = { 'response': self.answer_comment.content, 'extensions': { 'http://xapi.learninganalytics.ubc.ca/extension/character-count': len(self.answer_comment.content), 'http://xapi.learninganalytics.ubc.ca/extension/word-count': len(self.answer_comment.content.split(" ")) } } expected_context = { 'contextActivities': { 'parent': [expected_answer], 'grouping': [ expected_assignment_question, expected_assignment, expected_course, expected_sis_course, expected_sis_section ] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } } expected_statement = { "actor": self.get_compair_xapi_actor(self.user), "verb": expected_verb, "object": expected_answer_comment, "context": expected_context, "result": expected_result } # test with answer comment normal content statement = XAPIStatement.generate( user=self.user, verb=XAPIVerb.generate('updated'), object=XAPIObject.answer_comment(self.answer_comment), context=XAPIContext.answer_comment(self.answer_comment), result=XAPIResult.basic_content(self.answer_comment.content)) XAPI._emit_to_lrs(json.loads(statement.to_json(XAPI._version))) self._validate_and_cleanup_xapi_statement(self.sent_xapi_statement) self.assertEqual(self.sent_xapi_statement, expected_statement) # test with extremely long answer comment content # content should be ~ LRS_USER_INPUT_FIELD_SIZE_LIMIT bytes long + 100 characters content = "d" * (self.character_limit + 100) self.answer_comment.content = content db.session.commit() # expected_assignment name and description should be <= LRS_USER_INPUT_FIELD_SIZE_LIMIT bytes long + " [TEXT TRIMMED]..." expected_result['response'] = ( "d" * self.character_limit) + " [TEXT TRIMMED]..." expected_result['extensions'][ 'http://xapi.learninganalytics.ubc.ca/extension/word-count'] = 1 expected_result['extensions'][ 'http://xapi.learninganalytics.ubc.ca/extension/character-count'] = len( content) statement = XAPIStatement.generate( user=self.user, verb=XAPIVerb.generate('updated'), object=XAPIObject.answer_comment(self.answer_comment), context=XAPIContext.answer_comment(self.answer_comment), result=XAPIResult.basic_content(self.answer_comment.content)) XAPI._emit_to_lrs(json.loads(statement.to_json(XAPI._version))) self._validate_and_cleanup_xapi_statement(self.sent_xapi_statement) self.assertEqual(self.sent_xapi_statement, expected_statement) expected_context = { 'contextActivities': { 'parent': [expected_course], 'grouping': [expected_sis_course, expected_sis_section] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } } expected_statement = { "actor": self.get_compair_xapi_actor(self.user), "verb": expected_verb, "object": expected_assignment, "context": expected_context } # test with assignment normal content statement = XAPIStatement.generate( user=self.user, verb=XAPIVerb.generate('updated'), object=XAPIObject.assignment(self.assignment), context=XAPIContext.assignment(self.assignment)) XAPI._emit_to_lrs(json.loads(statement.to_json(XAPI._version))) self._validate_and_cleanup_xapi_statement(self.sent_xapi_statement) self.assertEqual(self.sent_xapi_statement, expected_statement) # test with extremely long answer content # content should be ~ LRS_USER_INPUT_FIELD_SIZE_LIMIT bytes long + 100 characters name = "a" * (self.character_limit + 100) description = "b" * (self.character_limit + 100) self.assignment.name = name self.assignment.description = description db.session.commit() # expected_assignment name and description should be <= LRS_USER_INPUT_FIELD_SIZE_LIMIT bytes long + " [TEXT TRIMMED]..." expected_assignment['definition']['name']['en-US'] = ( "a" * self.character_limit) + " [TEXT TRIMMED]..." expected_assignment['definition']['description']['en-US'] = ( "b" * self.character_limit) + " [TEXT TRIMMED]..." statement = XAPIStatement.generate( user=self.user, verb=XAPIVerb.generate('updated'), object=XAPIObject.assignment(self.assignment), context=XAPIContext.assignment(self.assignment)) XAPI._emit_to_lrs(json.loads(statement.to_json(XAPI._version))) self._validate_and_cleanup_xapi_statement(self.sent_xapi_statement) self.assertEqual(self.sent_xapi_statement, expected_statement)