def post(self): if not CaliperSensor.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 ['type', 'action', 'object']: if not raw_params.get(param): abort(400) params[param] = raw_params.get(param) # add optional params for param in [ 'eventTime', 'target', 'generated', 'referrer', 'extensions', 'profile' ]: if raw_params.get(param): params[param] = raw_params.get(param) event = CaliperEvent.generate_from_params(current_user, params, course=course) CaliperSensor.emit(event) return {'success': True}
def emit_lrs_caliper_event(self, caliper_log_id): from compair.learning_records import CaliperSensor caliper_log = CaliperLog.query \ .filter_by( id=caliper_log_id, transmitted=False ) \ .one_or_none() if caliper_log: try: CaliperSensor._emit_to_lrs(json.loads(caliper_log.event)) 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_caliper_event connection refused: " + socket.error.strerror) return raise error CaliperLog.query \ .filter_by(id=caliper_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 CaliperSensor.enabled(): # this should silently fail abort(404) params = caliper_event_parser.parse_args() course_uuid = params.pop('course_id') course = _get_valid_course(course_uuid) event = CaliperEvent.generate_from_params(current_user, params, course=course) CaliperSensor.emit(event) return { 'success': True }
def post(self): if not CaliperSensor.enabled(): # this should silently fail abort(404) params = caliper_event_parser.parse_args() course_uuid = params.pop('course_id') course = _get_valid_course(course_uuid) event = CaliperEvent.generate_from_params(current_user, params, course=course) CaliperSensor.emit(event) return {'success': True}
def emit_lrs_caliper_event(self, caliper_log_id): from compair.learning_records import CaliperSensor caliper_log = CaliperLog.query.filter_by(id=caliper_log_id).one_or_none() if caliper_log: try: CaliperSensor._emit_to_lrs(json.loads(caliper_log.event)) except socket.error as error: # don't raise connection refused error when in eager mode if error.errno != socket.errno.ECONNREFUSED: return raise error caliper_log.transmitted = True db.session.commit()
def test_send_remote_caliper_event(self, mocked_send_event): self.app.config['XAPI_ENABLED'] = False def send_event_override(event): self.sent_caliper_event = json.loads(event.as_json()) return {} mocked_send_event.side_effect = send_event_override expected_assignment = { 'name': self.assignment.name, 'type': 'Assessment', 'dateCreated': self.assignment.created.replace(tzinfo=pytz.utc).isoformat(), 'dateModified': self.assignment.modified.replace(tzinfo=pytz.utc).isoformat(), 'dateToStartOn': self.assignment.answer_start.replace(tzinfo=pytz.utc).isoformat(), 'description': self.assignment.description, 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid, 'isPartOf': { 'academicSession': self.course.term, 'dateCreated': self.course.created.replace(tzinfo=pytz.utc).isoformat(), 'dateModified': self.course.modified.replace(tzinfo=pytz.utc).isoformat(), 'id': "https://localhost:8888/app/course/" + self.course.uuid, 'name': self.course.name, 'type': 'CourseOffering', 'extensions': { 'ltiContexts': [{ 'context_id': self.lti_context.context_id, 'oauth_consumer_key': self.lti_data.lti_consumer.oauth_consumer_key, 'lis_course_offering_sourcedid': "sis_course_id", 'lis_course_section_sourcedid': "sis_section_id", }] } }, 'items': [{ 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/question", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/comparison/question/1", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/evaluation/question/1", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/evaluation/question/2", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/comparison/question/2", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/evaluation/question/3", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/evaluation/question/4", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/comparison/question/3", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/evaluation/question/5", 'type': 'AssessmentItem' }, { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/evaluation/question/6", 'type': 'AssessmentItem' }], } expected_assignment_question = { 'name': self.assignment.name, 'type': 'AssessmentItem', 'dateCreated': self.assignment.created.replace(tzinfo=pytz.utc).isoformat(), 'dateModified': self.assignment.modified.replace(tzinfo=pytz.utc).isoformat(), 'dateToStartOn': self.assignment.answer_start.replace(tzinfo=pytz.utc).isoformat(), 'dateToSubmit': self.assignment.answer_end.replace(tzinfo=pytz.utc).isoformat(), 'description': self.assignment.description, 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/question", 'isPartOf': expected_assignment, } expected_attempt = { 'assignable': expected_assignment_question, 'assignee': self.get_compair_caliper_actor(self.user), 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/question/attempt/" + self.answer.attempt_uuid, 'duration': "PT05M00S", 'startedAtTime': self.answer.attempt_started.replace(tzinfo=pytz.utc).isoformat(), 'endedAtTime': self.answer.attempt_ended.replace(tzinfo=pytz.utc).isoformat(), 'type': 'Attempt' } expected_answer = { 'attempt': expected_attempt, 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/answer/" + self.answer.uuid, 'type': 'Response', 'dateCreated': self.answer.created.replace(tzinfo=pytz.utc).isoformat(), 'dateModified': self.answer.modified.replace(tzinfo=pytz.utc).isoformat(), 'extensions': { 'characterCount': len(self.answer.content), 'content': self.answer.content, 'isDraft': False, 'wordCount': len(self.answer.content.split(" ")), 'scoreDetails': { 'algorithm': self.assignment.scoring_algorithm.value, 'loses': 0, 'opponents': 0, 'rounds': 0, 'score': 5, 'wins': 0, 'criteria': { "https://localhost:8888/app/criterion/" + self.criterion.uuid: { 'loses': 0, 'opponents': 0, 'rounds': 0, 'score': 5, 'wins': 0 }, } }, } } expected_answer_comment = { 'commenter': self.get_compair_caliper_actor(self.user), 'commented': expected_answer, 'value': self.answer_comment.content, 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/answer/" + self.answer.uuid + "/comment/" + self.answer_comment.uuid, 'type': 'Comment', 'dateCreated': self.answer_comment.created.replace(tzinfo=pytz.utc).isoformat(), 'dateModified': self.answer_comment.modified.replace(tzinfo=pytz.utc).isoformat(), 'extensions': { 'characterCount': len(self.answer_comment.content), 'isDraft': False, 'type': 'Public', 'wordCount': len(self.answer_comment.content.split(" ")), }, } expected_event = { 'action': 'Completed', 'actor': self.get_compair_caliper_actor(self.user), 'membership': self.get_caliper_membership(self.course, self.user, self.lti_context), 'generated': expected_answer, 'object': expected_assignment_question, 'session': self.get_caliper_session(self.get_compair_caliper_actor( self.user)), 'type': 'AssessmentItemEvent' } # test with answer normal content event = caliper.events.AssessmentItemEvent( action=caliper.constants. ASSESSMENT_ITEM_EVENT_ACTIONS["COMPLETED"], object=CaliperEntities.assignment_question(self.answer.assignment), generated=CaliperEntities.answer(self.answer), **CaliperEvent._defaults(self.user, self.course)) CaliperSensor._emit_to_lrs(json.loads(event.as_json())) self._validate_and_cleanup_caliper_event(self.sent_caliper_event) self.assertEqual(self.sent_caliper_event, expected_event) # 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_answer['extensions']['content'] = ( "c" * self.character_limit) + " [TEXT TRIMMED]..." expected_answer['extensions']['wordCount'] = 1 expected_answer['extensions']['characterCount'] = len(content) expected_answer['dateModified'] = self.answer.modified.replace( tzinfo=pytz.utc).isoformat() event = caliper.events.AssessmentItemEvent( action=caliper.constants. ASSESSMENT_ITEM_EVENT_ACTIONS["COMPLETED"], object=CaliperEntities.assignment_question(self.answer.assignment), generated=CaliperEntities.answer(self.answer), **CaliperEvent._defaults(self.user, self.course)) CaliperSensor._emit_to_lrs(json.loads(event.as_json())) self._validate_and_cleanup_caliper_event(self.sent_caliper_event) self.assertEqual(self.sent_caliper_event, expected_event) # test with answer comment normal content event = caliper.events.Event( action=caliper.constants.BASIC_EVENT_ACTIONS["MODIFIED"], object=CaliperEntities.answer_comment(self.answer_comment), **CaliperEvent._defaults(self.user, self.course)) CaliperSensor._emit_to_lrs(json.loads(event.as_json())) self._validate_and_cleanup_caliper_event(self.sent_caliper_event) expected_event = { 'action': 'Modified', 'actor': self.get_compair_caliper_actor(self.user), 'membership': self.get_caliper_membership(self.course, self.user, self.lti_context), 'object': expected_answer_comment, 'session': self.get_caliper_session(self.get_compair_caliper_actor( self.user)), 'type': 'Event' } self.assertEqual(self.sent_caliper_event, expected_event) # 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_answer_comment['value'] = ( "d" * self.character_limit) + " [TEXT TRIMMED]..." expected_answer_comment['extensions']['wordCount'] = 1 expected_answer_comment['extensions']['characterCount'] = len(content) expected_answer_comment[ 'dateModified'] = self.answer_comment.modified.replace( tzinfo=pytz.utc).isoformat() event = caliper.events.Event( action=caliper.constants.BASIC_EVENT_ACTIONS["MODIFIED"], object=CaliperEntities.answer_comment(self.answer_comment), **CaliperEvent._defaults(self.user, self.course)) CaliperSensor._emit_to_lrs(json.loads(event.as_json())) self._validate_and_cleanup_caliper_event(self.sent_caliper_event) self.assertEqual(self.sent_caliper_event, expected_event) # test with assignment normal content event = caliper.events.Event( action=caliper.constants.BASIC_EVENT_ACTIONS["MODIFIED"], object=CaliperEntities.assignment(self.assignment), **CaliperEvent._defaults(self.user, self.course)) CaliperSensor._emit_to_lrs(json.loads(event.as_json())) self._validate_and_cleanup_caliper_event(self.sent_caliper_event) expected_event = { 'action': 'Modified', 'actor': self.get_compair_caliper_actor(self.user), 'membership': self.get_caliper_membership(self.course, self.user, self.lti_context), 'object': expected_assignment, 'session': self.get_caliper_session(self.get_compair_caliper_actor( self.user)), 'type': 'Event' } self.assertEqual(self.sent_caliper_event, expected_event) # test with extremely long assignment 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['name'] = ( "a" * self.character_limit) + " [TEXT TRIMMED]..." expected_assignment['description'] = ( "b" * self.character_limit) + " [TEXT TRIMMED]..." expected_assignment['dateModified'] = self.assignment.modified.replace( tzinfo=pytz.utc).isoformat() event = caliper.events.Event( action=caliper.constants.BASIC_EVENT_ACTIONS["MODIFIED"], object=CaliperEntities.assignment(self.assignment), **CaliperEvent._defaults(self.user, self.course)) CaliperSensor._emit_to_lrs(json.loads(event.as_json())) self._validate_and_cleanup_caliper_event(self.sent_caliper_event) self.assertEqual(self.sent_caliper_event, expected_event)