def setUp(self): super(ComPAIRLearningRecordTestCase, self).setUp() self.app.config[ 'LRS_XAPI_STATEMENT_ENDPOINT'] = 'http://example.com/xapi' self.app.config['LRS_XAPI_USERNAME'] = '******' self.app.config['LRS_XAPI_PASSWORD'] = '******' self.app.config['LRS_CALIPER_HOST'] = 'http://example.com/caliper' self.app.config['LRS_CALIPER_API_KEY'] = 'lrs_api_key' self.app.config['LRS_USER_INPUT_FIELD_SIZE_LIMIT'] = 200 # 200 bytes self.data = SimpleAnswersTestData() self.lti_data = LTITestData() self.user = self.data.authorized_student self.setup_session_data(self.user) self.course = self.data.main_course self.lti_context = self.lti_data.create_context( self.lti_data.lti_consumer, compair_course_id=self.course.id, lis_course_offering_sourcedid="sis_course_id", lis_course_section_sourcedid="sis_section_id", ) self.assignment = self.data.assignments[0] self.criterion = self.assignment.criteria[0] self.answer = self.data.create_answer(self.assignment, self.user) self.answer_comment = self.data.create_answer_comment( self.answer, self.user, AnswerCommentType.public) self.sent_xapi_statement = None self.sent_caliper_event = None self.character_limit = int( current_app.config.get('LRS_USER_INPUT_FIELD_SIZE_LIMIT') / len("c".encode('utf-8')))
class CoursesLTIAPITests(ComPAIRAPITestCase): def setUp(self): super(CoursesLTIAPITests, self).setUp() self.data = SimpleAssignmentTestData() self.lti_data = LTITestData() def test_delete_course(self): # test unlinking of lti contexts when course deleted course = self.data.get_course() url = '/api/courses/' + course.uuid lti_consumer = self.lti_data.get_consumer() lti_context1 = self.lti_data.create_context( lti_consumer, compair_course_id=course.id) lti_context2 = self.lti_data.create_context( lti_consumer, compair_course_id=course.id) lti_resource_link1 = self.lti_data.create_resource_link( lti_consumer, lti_context=lti_context2, compair_assignment=self.data.assignments[0]) lti_resource_link2 = self.lti_data.create_resource_link( lti_consumer, lti_context=lti_context2, compair_assignment=self.data.assignments[1]) with self.login(self.data.get_authorized_instructor().username): rv = self.client.delete(url) self.assert200(rv) self.assertEqual(course.uuid, rv.json['id']) self.assertIsNone(lti_context1.compair_course_id) self.assertIsNone(lti_context2.compair_course_id) self.assertIsNone(lti_resource_link1.compair_assignment_id) self.assertIsNone(lti_resource_link2.compair_assignment_id)
def setUp(self): super(AnswerCommentAPITests, self).setUp() self.data = AnswerCommentsTestData() self.course = self.data.get_course() self.assignments = self.data.get_assignments() self.answers = self.data.get_answers_by_assignment() self.assignment = self.assignments[0] self.assignment.enable_self_evaluation = True db.session.commit() self.assignment.calculate_grades() self.lti_data = LTITestData()
def setUp(self): super(ComPAIRLearningRecordTestCase, self).setUp() self.data = SimpleAnswersTestData() self.auth_data = ThirdPartyAuthTestData() self.lti_data = LTITestData() self.course = self.data.main_course self.assignment = self.data.assignments[0] self.user = self.data.create_user(SystemRole.instructor) self.data.enrol_user(self.user, self.data.get_course(), CourseRole.instructor) self.global_unique_identifier = 'mock_puid_è_global_unique_identifier'
def setUp(self): super(ComparisonAPITests, self).setUp() self.data = ComparisonTestData() self.course = self.data.get_course() self.assignment = self.data.get_assignments()[0] self.base_url = self._build_url(self.course.uuid, self.assignment.uuid) self.lti_data = LTITestData() secondary_criterion = self.data.create_criterion( self.data.authorized_instructor) AssignmentCriterionFactory(criterion=secondary_criterion, assignment=self.assignment) db.session.commit()
def test_create_user_lti_and_CAS(self): url = '/api/users' lti_data = LTITestData() auth_data = ThirdPartyAuthTestData() with self.client.session_transaction() as sess: sess['CAS_CREATE'] = True sess['CAS_UNIQUE_IDENTIFIER'] = "some_unique_identifier" self.assertIsNone(sess.get('LTI')) # test login required when LTI and oauth_create_user_link are not present (even when CAS params are present) expected = UserFactory.stub(system_role=SystemRole.student.value) rv = self.client.post(url, data=json.dumps(expected.__dict__), content_type='application/json') self.assert401(rv) # test create student via lti session with self.lti_launch(lti_data.get_consumer(), lti_data.generate_resource_link_id(), user_id=lti_data.generate_user_id(), context_id=lti_data.generate_context_id(), roles="Student") as lti_response: self.assert200(lti_response) with self.client.session_transaction() as sess: sess['CAS_CREATE'] = True sess['CAS_UNIQUE_IDENTIFIER'] = "some_unique_identifier" self.assertTrue(sess.get('LTI')) expected = UserFactory.stub(system_role=None) rv = self.client.post(url, data=json.dumps(expected.__dict__), content_type="application/json") self.assert200(rv) self.assertEqual(expected.displayname, rv.json['displayname']) user = User.query.filter_by(uuid=rv.json['id']).one() self.assertEqual(SystemRole.student, user.system_role) self.assertIsNone(user.password) self.assertIsNone(user.username) third_party_user = ThirdPartyUser.query \ .filter_by( third_party_type=ThirdPartyType.cas, unique_identifier="some_unique_identifier", user_id=user.id ) \ .one_or_none() self.assertIsNotNone(third_party_user) with self.client.session_transaction() as sess: self.assertTrue(sess.get('CAS_LOGIN')) self.assertIsNone(sess.get('CAS_CREATE')) self.assertIsNone(sess.get('CAS_UNIQUE_IDENTIFIER')) self.assertIsNone(sess.get('oauth_create_user_link'))
def setUp(self): super(ComparisonAPITests, self).setUp() self.data = ComparisonTestData() self.course = self.data.get_course() self.assignment = self.data.get_assignments()[0] self.base_url = self._build_url(self.course.uuid, self.assignment.uuid) self.lti_data = LTITestData() secondary_criterion = self.data.create_criterion(self.data.authorized_instructor) AssignmentCriterionFactory(criterion=secondary_criterion, assignment=self.assignment) db.session.commit()
def setUp(self): super(TestLTIOutcome, self).setUp() self.fixtures = TestFixture().add_course(num_students=30, num_groups=2, with_draft_student=True) self.lti_data = LTITestData() self.lti_consumer = self.lti_data.lti_consumer self.lis_outcome_service_url = "TestUrl.com" self.lis_result_sourcedid = "SomeUniqueSourcedId" self.grade = 0.8
def setUp(self): super(ComPAIRLearningRecordTestCase, self).setUp() self.data = BasicTestData() self.lti_data = LTITestData() self.user = self.data.authorized_student self.setup_session_data(self.user) self.course = self.data.main_course self.lti_context = self.lti_data.create_context( self.lti_data.lti_consumer, compair_course_id=self.course.id, lis_course_offering_sourcedid="sis_course_id", lis_course_section_sourcedid="sis_section_id", ) self.expected_caliper_course = { 'academicSession': self.course.term, 'dateCreated': self.course.created.replace(tzinfo=pytz.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z', 'dateModified': self.course.modified.replace(tzinfo=pytz.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z', 'id': "https://localhost:8888/app/course/"+self.course.uuid, 'name': self.course.name, 'type': 'CourseOffering', 'otherIdentifiers': [{ 'identifier': self.lti_context.context_id, 'identifierType': 'LtiContextId', 'type': 'SystemIdentifier', 'extensions': { 'lis_course_offering_sourcedid': 'sis_course_id', 'lis_course_section_sourcedid': 'sis_section_id', 'oauth_consumer_key': self.lti_data.lti_consumer.oauth_consumer_key, }, }] } self.expected_xapi_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' }
class CoursesLTIAPITests(ComPAIRAPITestCase): def setUp(self): super(CoursesLTIAPITests, self).setUp() self.data = SimpleAssignmentTestData() self.lti_data = LTITestData() def test_delete_course(self): # test unlinking of lti contexts when course deleted course = self.data.get_course() url = '/api/courses/' + course.uuid lti_consumer = self.lti_data.get_consumer() lti_context1 = self.lti_data.create_context( lti_consumer, compair_course_id=course.id ) lti_context2 = self.lti_data.create_context( lti_consumer, compair_course_id=course.id ) lti_resource_link1 = self.lti_data.create_resource_link( lti_consumer, lti_context=lti_context2, compair_assignment=self.data.assignments[0] ) lti_resource_link2 = self.lti_data.create_resource_link( lti_consumer, lti_context=lti_context2, compair_assignment=self.data.assignments[1] ) with self.login(self.data.get_authorized_instructor().username): rv = self.client.delete(url) self.assert200(rv) self.assertEqual(course.uuid, rv.json['id']) self.assertIsNone(lti_context1.compair_course_id) self.assertIsNone(lti_context2.compair_course_id) self.assertIsNone(lti_resource_link1.compair_assignment_id) self.assertIsNone(lti_resource_link2.compair_assignment_id)
class CourseLearningRecordTests(ComPAIRLearningRecordTestCase): def setUp(self): super(ComPAIRLearningRecordTestCase, self).setUp() self.data = BasicTestData() self.lti_data = LTITestData() self.user = self.data.authorized_student self.setup_session_data(self.user) self.course = self.data.main_course self.lti_context = self.lti_data.create_context( self.lti_data.lti_consumer, compair_course_id=self.course.id, lis_course_offering_sourcedid="sis_course_id", lis_course_section_sourcedid="sis_section_id", ) self.expected_caliper_course = { '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", }] } } self.expected_xapi_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' } self.expected_xapi_sis_course = { 'id': 'https://localhost:8888/course/' + self.lti_context.lis_course_offering_sourcedid, 'objectType': 'Activity' } self.expected_xapi_sis_section = { 'id': 'https://localhost:8888/course/' + self.lti_context.lis_course_offering_sourcedid + '/section/' + self.lti_context.lis_course_section_sourcedid, 'objectType': 'Activity' } def test_on_course_create(self): on_course_create.send(current_app._get_current_object(), event_name=on_course_create.name, user=self.user, course=self.course) events = self.get_and_clear_caliper_event_log() expected_caliper_event = { 'action': 'Created', 'actor': self.get_compair_caliper_actor(self.user), 'membership': self.get_caliper_membership(self.course, self.user, self.lti_context), 'object': self.expected_caliper_course, 'session': self.get_caliper_session(self.get_compair_caliper_actor( self.user)), 'type': 'Event' } self.assertEqual(len(events), 1) self.assertEqual(events[0], expected_caliper_event) statements = self.get_and_clear_xapi_statement_log() expected_xapi_statement = { "actor": self.get_compair_xapi_actor(self.user), "verb": { 'id': 'http://activitystrea.ms/schema/1.0/author', 'display': { 'en-US': 'authored' } }, "object": self.expected_xapi_course, "context": { 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } } } self.assertEqual(len(statements), 1) self.assertEqual(statements[0], expected_xapi_statement) def test_on_course_modified(self): on_course_modified.send(current_app._get_current_object(), event_name=on_course_modified.name, user=self.user, course=self.course) events = self.get_and_clear_caliper_event_log() expected_caliper_event = { 'action': 'Modified', 'actor': self.get_compair_caliper_actor(self.user), 'membership': self.get_caliper_membership(self.course, self.user, self.lti_context), 'object': self.expected_caliper_course, 'session': self.get_caliper_session(self.get_compair_caliper_actor( self.user)), 'type': 'Event' } self.assertEqual(len(events), 1) self.assertEqual(events[0], expected_caliper_event) statements = self.get_and_clear_xapi_statement_log() expected_xapi_statement = { "actor": self.get_compair_xapi_actor(self.user), "verb": { 'id': 'http://activitystrea.ms/schema/1.0/update', 'display': { 'en-US': 'updated' } }, "object": self.expected_xapi_course, "context": { 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } } } self.assertEqual(len(statements), 1) self.assertEqual(statements[0], expected_xapi_statement) def test_on_course_delete(self): on_course_delete.send(current_app._get_current_object(), event_name=on_course_delete.name, user=self.user, course=self.course) events = self.get_and_clear_caliper_event_log() expected_caliper_event = { 'action': 'Deleted', 'actor': self.get_compair_caliper_actor(self.user), 'membership': self.get_caliper_membership(self.course, self.user, self.lti_context), 'object': self.expected_caliper_course, 'session': self.get_caliper_session(self.get_compair_caliper_actor( self.user)), 'type': 'Event' } self.assertEqual(len(events), 1) self.assertEqual(events[0], expected_caliper_event) statements = self.get_and_clear_xapi_statement_log() expected_xapi_statement = { "actor": self.get_compair_xapi_actor(self.user), "verb": { 'id': 'http://activitystrea.ms/schema/1.0/delete', 'display': { 'en-US': 'deleted' } }, "object": self.expected_xapi_course, "context": { 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } } } self.assertEqual(len(statements), 1) self.assertEqual(statements[0], expected_xapi_statement)
def setUp(self): super(ComPAIRLearningRecordTestCase, self).setUp() self.data = BasicTestData() self.lti_data = LTITestData() self.user = self.data.authorized_student self.setup_session_data(self.user) self.course = self.data.main_course self.lti_context = self.lti_data.create_context( self.lti_data.lti_consumer, compair_course_id=self.course.id, lis_course_offering_sourcedid="sis_course_id", lis_course_section_sourcedid="sis_section_id", ) self.expected_caliper_course = { '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", }] } } self.expected_xapi_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' } self.expected_xapi_sis_course = { 'id': 'https://localhost:8888/course/' + self.lti_context.lis_course_offering_sourcedid, 'objectType': 'Activity' } self.expected_xapi_sis_section = { 'id': 'https://localhost:8888/course/' + self.lti_context.lis_course_offering_sourcedid + '/section/' + self.lti_context.lis_course_section_sourcedid, 'objectType': 'Activity' }
class AssignmentLearningRecordTests(ComPAIRLearningRecordTestCase): def setUp(self): super(ComPAIRLearningRecordTestCase, self).setUp() self.data = SimpleAssignmentTestData() self.lti_data = LTITestData() self.user = self.data.authorized_student self.setup_session_data(self.user) self.course = self.data.main_course self.lti_context = self.lti_data.create_context( self.lti_data.lti_consumer, compair_course_id=self.course.id, lis_course_offering_sourcedid="sis_course_id", lis_course_section_sourcedid="sis_section_id", ) self.assignment = self.data.assignments[0] self.expected_caliper_course = { '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", }] } } self.expected_caliper_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': self.expected_caliper_course, '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' }], } self.expected_xapi_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' } self.expected_xapi_sis_course = { 'id': 'https://localhost:8888/course/'+self.lti_context.lis_course_offering_sourcedid, 'objectType': 'Activity' } self.expected_xapi_sis_section = { 'id': 'https://localhost:8888/course/'+self.lti_context.lis_course_offering_sourcedid+'/section/'+self.lti_context.lis_course_section_sourcedid, 'objectType': 'Activity' } self.expected_xapi_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' } def test_on_assignment_create(self): on_assignment_create.send( current_app._get_current_object(), event_name=on_assignment_create.name, user=self.user, assignment=self.assignment ) events = self.get_and_clear_caliper_event_log() expected_caliper_event = { 'action': 'Created', 'actor': self.get_compair_caliper_actor(self.user), 'membership': self.get_caliper_membership(self.course, self.user, self.lti_context), 'object': self.expected_caliper_assignment, 'session': self.get_caliper_session(self.get_compair_caliper_actor(self.user)), 'type': 'Event' } self.assertEqual(len(events), 1) self.assertEqual(events[0], expected_caliper_event) statements = self.get_and_clear_xapi_statement_log() expected_xapi_statement = { "actor": self.get_compair_xapi_actor(self.user), "verb": { 'id': 'http://activitystrea.ms/schema/1.0/author', 'display': {'en-US': 'authored'} }, "object": self.expected_xapi_assignment, "context": { 'contextActivities': { 'parent': [self.expected_xapi_course], 'grouping': [self.expected_xapi_sis_course, self.expected_xapi_sis_section] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } } } self.assertEqual(len(statements), 1) self.assertEqual(statements[0], expected_xapi_statement) def test_on_assignment_modified(self): on_assignment_modified.send( current_app._get_current_object(), event_name=on_assignment_modified.name, user=self.user, assignment=self.assignment ) events = self.get_and_clear_caliper_event_log() expected_caliper_event = { 'action': 'Modified', 'actor': self.get_compair_caliper_actor(self.user), 'membership': self.get_caliper_membership(self.course, self.user, self.lti_context), 'object': self.expected_caliper_assignment, 'session': self.get_caliper_session(self.get_compair_caliper_actor(self.user)), 'type': 'Event' } self.assertEqual(len(events), 1) self.assertEqual(events[0], expected_caliper_event) statements = self.get_and_clear_xapi_statement_log() expected_xapi_statement = { "actor": self.get_compair_xapi_actor(self.user), "verb": { 'id': 'http://activitystrea.ms/schema/1.0/update', 'display': {'en-US': 'updated'} }, "object": self.expected_xapi_assignment, "context": { 'contextActivities': { 'parent': [self.expected_xapi_course], 'grouping': [self.expected_xapi_sis_course, self.expected_xapi_sis_section] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } } } self.assertEqual(len(statements), 1) self.assertEqual(statements[0], expected_xapi_statement) def test_on_assignment_delete(self): on_assignment_delete.send( current_app._get_current_object(), event_name=on_assignment_delete.name, user=self.user, assignment=self.assignment ) events = self.get_and_clear_caliper_event_log() expected_caliper_event = { 'action': 'Deleted', 'actor': self.get_compair_caliper_actor(self.user), 'membership': self.get_caliper_membership(self.course, self.user, self.lti_context), 'object': self.expected_caliper_assignment, 'session': self.get_caliper_session(self.get_compair_caliper_actor(self.user)), 'type': 'Event' } self.assertEqual(len(events), 1) self.assertEqual(events[0], expected_caliper_event) statements = self.get_and_clear_xapi_statement_log() expected_xapi_statement = { "actor": self.get_compair_xapi_actor(self.user), "verb": { 'id': 'http://activitystrea.ms/schema/1.0/delete', 'display': {'en-US': 'deleted'} }, "object": self.expected_xapi_assignment, "context": { 'contextActivities': { 'parent': [self.expected_xapi_course], 'grouping': [self.expected_xapi_sis_course, self.expected_xapi_sis_section] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } } } self.assertEqual(len(statements), 1) self.assertEqual(statements[0], expected_xapi_statement)
class AnswerCommentListAPITests(ComPAIRAPITestCase): """ Tests for answer comment list API """ resource = AnswerCommentListAPI api = api def setUp(self): super(AnswerCommentListAPITests, self).setUp() self.data = AnswerCommentsTestData() self.course = self.data.get_course() self.assignments = self.data.get_assignments() self.answers = self.data.get_answers_by_assignment() self.assignment = self.assignments[0] self.assignment.enable_self_evaluation = True db.session.commit() self.assignment.calculate_grades() self.lti_data = LTITestData() def test_get_all_answer_comments(self): url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid) # test login required rv = self.client.get(url) self.assert401(rv) # test unauthorized user with self.login(self.data.get_unauthorized_instructor().username): rv = self.client.get(url) self.assert403(rv) with self.login(self.data.get_authorized_instructor().username): # test invalid answer id invalid_url = self.get_url(course_uuid=self.course.id, assignment_uuid=self.assignment.uuid, answer_uuid="999") rv = self.client.get(invalid_url) self.assert404(rv) # test authorized user rv = self.client.get(url) self.assert200(rv) self.assertEqual(1, len(rv.json)) self.assertEqual( self.data.get_non_draft_answer_comments_by_assignment( self.assignment)[1].content, rv.json[0]['content']) self.assertIn( self.data.get_non_draft_answer_comments_by_assignment( self.assignment)[1].user_fullname, rv.json[0]['user']['fullname']) # test non-owner student of answer access comments student = self.data.get_authorized_student() for user_context in [ self.login(student.username), self.impersonate(self.data.get_authorized_instructor(), student) ]: with user_context: rv = self.client.get(url) self.assert200(rv) self.assertEqual(0, len(rv.json)) # test owner student of answer access comments student = self.data.get_extra_student(0) for user_context in [ \ self.login(student.username), \ self.impersonate(self.data.get_authorized_instructor(), student)]: with user_context: rv = self.client.get(url) self.assert200(rv) self.assertEqual(1, len(rv.json)) self.assertNotIn('fullname', rv.json[0]['user']) def test_get_list_query_params(self): comment = AnswerCommentsTestData.create_answer_comment( self.data.get_extra_student(0), self.answers[self.assignment.id][0], comment_type=AnswerCommentType.self_evaluation) draft_comment = AnswerCommentsTestData.create_answer_comment( self.data.get_extra_student(0), self.answers[self.assignment.id][0], comment_type=AnswerCommentType.evaluation, draft=True) base_params = { 'course_uuid': self.course.uuid, 'assignment_uuid': self.assignment.uuid, } with self.login(self.data.get_authorized_instructor().username): # no answer ids rv = self.client.get(self.get_url(**base_params)) self.assert404(rv) params = dict(base_params, answer_ids=self.answers[self.assignment.id][0].uuid) extra_student2_answer_comment_uuid = self.data.get_answer_comments_by_assignment( self.assignment)[1].uuid rv = self.client.get(self.get_url(**params)) self.assert200(rv) self.assertEqual(2, len(rv.json)) rv = self.client.get( self.get_url(self_evaluation='false', **params)) self.assert200(rv) self.assertEqual(1, len(rv.json)) self.assertEqual(extra_student2_answer_comment_uuid, rv.json[0]['id']) rv = self.client.get(self.get_url(self_evaluation='only', **params)) self.assert200(rv) self.assertEqual(1, len(rv.json)) self.assertEqual(comment.uuid, rv.json[0]['id']) ids = [extra_student2_answer_comment_uuid, comment.uuid] rv = self.client.get(self.get_url(ids=','.join(ids), **base_params)) self.assert200(rv) self.assertEqual(2, len(rv.json)) six.assertCountEqual(self, ids, [c['id'] for c in rv.json]) answer_ids = [ answer.uuid for answer in self.answers[self.assignment.id] ] params = dict(base_params, answer_ids=','.join(answer_ids)) rv = self.client.get(self.get_url(**params)) self.assert200(rv) self.assertEqual(3, len(rv.json)) rv = self.client.get( self.get_url(self_evaluation='false', **params)) self.assert200(rv) self.assertEqual(2, len(rv.json)) self.assertNotIn(comment.uuid, (c['id'] for c in rv.json)) rv = self.client.get(self.get_url(self_evaluation='only', **params)) self.assert200(rv) self.assertEqual(1, len(rv.json)) self.assertEqual(comment.uuid, rv.json[0]['id']) answer_ids = [ answer.uuid for answer in self.answers[self.assignment.id] ] params = dict(base_params, answer_ids=','.join(answer_ids), user_ids=self.data.get_extra_student(1).uuid) rv = self.client.get(self.get_url(**params)) self.assert200(rv) self.assertEqual(1, len(rv.json)) # test user_ids filter user_ids = ','.join([self.data.get_extra_student(0).uuid]) rv = self.client.get(self.get_url(user_ids=user_ids, **base_params)) self.assert200(rv) self.assertEqual(2, len(rv.json)) six.assertCountEqual(self, [ comment.uuid, self.data.answer_comments_by_assignment[ self.assignment.id][0].uuid ], [c['id'] for c in rv.json]) student = self.data.get_extra_student(1) for user_context in [ \ self.login(student.username), \ self.impersonate(self.data.get_authorized_instructor(), student)]: with user_context: answer_ids = [ answer.uuid for answer in self.answers[self.assignment.id] ] params = dict(base_params, answer_ids=','.join(answer_ids), user_ids=self.data.get_extra_student(1).uuid) rv = self.client.get(self.get_url(**params)) self.assert200(rv) self.assertEqual(1, len(rv.json)) # answer is not from the student but comment is answer_ids = [self.answers[self.assignment.id][1].uuid] params = dict(base_params, answer_ids=','.join(answer_ids), user_ids=self.data.get_extra_student(0).uuid) rv = self.client.get(self.get_url(**params)) self.assert200(rv) self.assertEqual(1, len(rv.json)) self.assertEqual( self.data.get_extra_student(0).uuid, rv.json[0]['user_id']) # test drafts student = self.data.get_extra_student(0) for user_context in [ self.login(student.username), self.impersonate(self.data.get_authorized_instructor(), student) ]: with user_context: params = dict(base_params, user_ids=self.data.get_extra_student(0).uuid) rv = self.client.get(self.get_url(draft='only', **params)) self.assert200(rv) self.assertEqual(1, len(rv.json)) self.assertEqual(draft_comment.uuid, rv.json[0]['id']) rv = self.client.get(self.get_url(draft='false', **params)) self.assert200(rv) self.assertEqual(2, len(rv.json)) rv = self.client.get(self.get_url(draft='true', **params)) self.assert200(rv) self.assertEqual(3, len(rv.json)) self.assertEqual(draft_comment.uuid, rv.json[0]['id']) @mock.patch('compair.tasks.lti_outcomes.update_lti_course_grades.run') @mock.patch('compair.tasks.lti_outcomes.update_lti_assignment_grades.run') def test_create_answer_comment(self, mocked_update_assignment_grades_run, mocked_update_course_grades_run): url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid) content = { 'comment_type': AnswerCommentType.private.value, 'content': 'great answer' } # test login required rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert401(rv) # test unauthorized user with self.login(self.data.get_unauthorized_instructor().username): rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert403(rv) # test invalid course id with self.login(self.data.get_authorized_instructor().username): invalid_url = self.get_url( course_uuid="999", assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid) rv = self.client.post(invalid_url, data=json.dumps(content), content_type='application/json') self.assert404(rv) # test invalid assignment id invalid_url = self.get_url( course_uuid=self.course.uuid, assignment_uuid="999", answer_uuid=self.answers[self.assignment.id][0].uuid) rv = self.client.post(invalid_url, data=json.dumps(content), content_type='application/json') self.assert404(rv) # test invalid answer id invalid_url = self.get_url(course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid="999") rv = self.client.post(invalid_url, data=json.dumps(content), content_type='application/json') self.assert404(rv) # test empty content empty = content.copy() empty['content'] = '' rv = self.client.post(url, data=json.dumps(empty), content_type='application/json') self.assert400(rv) # test empty comment type empty = content.copy() empty['comment_type'] = '' rv = self.client.post(url, data=json.dumps(empty), content_type='application/json') self.assert400(rv) # test authorized user with mail.record_messages() as outbox: rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert200(rv) self.assertEqual(content['content'], rv.json['content']) self.assertFalse(rv.json['draft']) self.assertIn('fullname', rv.json['user']) self.assertEqual(len(outbox), 1) self.assertEqual( outbox[0].subject, "New Answer Feedback in " + self.data.get_course().name) self.assertEqual( outbox[0].recipients, [self.answers[self.assignment.id][0].user.email]) # test authorized user draft with mail.record_messages() as outbox: draft_content = content.copy() draft_content['draft'] = True rv = self.client.post(url, data=json.dumps(draft_content), content_type='application/json') self.assert200(rv) self.assertEqual(content['content'], rv.json['content']) self.assertTrue(rv.json['draft']) self.assertEqual(len(outbox), 0) # test authorized user draft - empty content with mail.record_messages() as outbox: empty = draft_content.copy() empty['content'] = None rv = self.client.post(url, data=json.dumps(empty), content_type='application/json') self.assert200(rv) self.assertEqual(empty['content'], rv.json['content']) self.assertTrue(rv.json['draft']) self.assertEqual(len(outbox), 0) with self.login('root'): with mail.record_messages() as outbox: rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert200(rv) self.assertEqual(len(outbox), 1) self.assertIn('fullname', rv.json['user']) with self.login(self.data.get_authorized_student().username): lti_consumer = self.lti_data.lti_consumer (lti_user_resource_link1, lti_user_resource_link2 ) = self.lti_data.setup_student_user_resource_links( self.data.get_authorized_student(), self.course, self.assignment) course_grade = CourseGrade.get_user_course_grade( self.course, self.data.get_authorized_student()).grade assignment_grade = AssignmentGrade.get_user_assignment_grade( self.assignment, self.data.get_authorized_student()).grade content = { 'comment_type': AnswerCommentType.self_evaluation.value, 'content': 'great answer' } # test student can not submit self-eval after self-eval grace period orig_answer_end = self.assignment.answer_end self.assignment.answer_end = datetime.datetime.utcnow( ) - datetime.timedelta(hours=12) self.assignment.self_eval_start = datetime.datetime.utcnow( ) - datetime.timedelta(hours=1) self.assignment.self_eval_end = datetime.datetime.utcnow( ) - datetime.timedelta(minutes=10) db.session.add(self.assignment) db.session.commit() rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert403(rv) self.assertEqual("Self-Evaluation Not Saved", rv.json['title']) self.assertEqual( "Sorry, the self-evaluation deadline has passed and therefore cannot be submitted.", rv.json['message']) self.assignment.answer_end = orig_answer_end self.assignment.self_eval_start = None self.assignment.self_eval_end = None with mail.record_messages() as outbox: orig_answer_end = self.assignment.answer_end self.assignment.answer_end = datetime.datetime.utcnow( ) - datetime.timedelta(hours=12) rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert200(rv) self.assertEqual(len(outbox), 0) self.assertNotIn('fullname', rv.json['user']) # grades should increase new_course_grade = CourseGrade.get_user_course_grade( self.course, self.data.get_authorized_student()) new_assignment_grade = AssignmentGrade.get_user_assignment_grade( self.assignment, self.data.get_authorized_student()) self.assertGreater(new_course_grade.grade, course_grade) self.assertGreater(new_assignment_grade.grade, assignment_grade) mocked_update_assignment_grades_run.assert_called_once_with( lti_consumer.id, [[ lti_user_resource_link2.lis_result_sourcedid, new_assignment_grade.id ]]) mocked_update_assignment_grades_run.reset_mock() mocked_update_course_grades_run.assert_called_once_with( lti_consumer.id, [[ lti_user_resource_link1.lis_result_sourcedid, new_course_grade.id ]]) mocked_update_assignment_grades_run.reset_mock() self.assignment.answer_end = orig_answer_end # test with impersonation student = self.data.get_extra_student(0) with self.impersonate(self.data.get_authorized_instructor(), student): lti_consumer = self.lti_data.lti_consumer (lti_user_resource_link1, lti_user_resource_link2 ) = self.lti_data.setup_student_user_resource_links( self.data.get_authorized_student(), self.course, self.assignment) course_grade = CourseGrade.get_user_course_grade( self.course, self.data.get_authorized_student()).grade assignment_grade = AssignmentGrade.get_user_assignment_grade( self.assignment, self.data.get_authorized_student()).grade content = { 'comment_type': AnswerCommentType.self_evaluation.value, 'content': 'great answer' } with mail.record_messages() as outbox: rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert403(rv) self.assertTrue(rv.json['disabled_by_impersonation'])
class RemoteLearningRecordTests(ComPAIRLearningRecordTestCase): def setUp(self): super(ComPAIRLearningRecordTestCase, self).setUp() self.app.config[ 'LRS_XAPI_STATEMENT_ENDPOINT'] = 'http://example.com/xapi' self.app.config['LRS_XAPI_USERNAME'] = '******' self.app.config['LRS_XAPI_PASSWORD'] = '******' self.app.config['LRS_CALIPER_HOST'] = 'http://example.com/caliper' self.app.config['LRS_CALIPER_API_KEY'] = 'lrs_api_key' self.app.config['LRS_USER_INPUT_FIELD_SIZE_LIMIT'] = 200 # 200 bytes self.data = SimpleAnswersTestData() self.lti_data = LTITestData() self.user = self.data.authorized_student self.setup_session_data(self.user) self.course = self.data.main_course self.lti_context = self.lti_data.create_context( self.lti_data.lti_consumer, compair_course_id=self.course.id, lis_course_offering_sourcedid="sis_course_id", lis_course_section_sourcedid="sis_section_id", ) self.assignment = self.data.assignments[0] self.criterion = self.assignment.criteria[0] self.answer = self.data.create_answer(self.assignment, self.user) self.answer_comment = self.data.create_answer_comment( self.answer, self.user, AnswerCommentType.public) self.sent_xapi_statement = None self.sent_caliper_event = None self.character_limit = int( current_app.config.get('LRS_USER_INPUT_FIELD_SIZE_LIMIT') / len("c".encode('utf-8'))) @mock.patch('caliper.sensor.Sensor.send') 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://*****:*****@mock.patch('tincan.RemoteLRS.save_statement') 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)
class LTIConsumersAPITests(ComPAIRAPITestCase): def setUp(self): super(LTIConsumersAPITests, self).setUp() self.data = BasicTestData() self.lti_data = LTITestData() def _build_consumer_url(self, consumer_uuid=None): return '/api/lti/consumers' + ('/' + consumer_uuid if consumer_uuid else '') def test_create_lti_consumer(self): url = self._build_consumer_url() consumer_expected = { 'oauth_consumer_key': 'new_consumer_key', 'oauth_consumer_secret': 'new_consumer_secret', 'global_unique_identifier_param': 'new_global_unique_identifier_param', 'student_number_param': 'new_student_number_param' } # Test login required rv = self.client.post(url, data=json.dumps(consumer_expected), content_type='application/json') self.assert401(rv) # Test unauthorized access with self.login(self.data.get_authorized_instructor().username): rv = self.client.post(url, data=json.dumps(consumer_expected), content_type='application/json') self.assert403(rv) with self.login(self.data.get_authorized_ta().username): rv = self.client.post(url, data=json.dumps(consumer_expected), content_type='application/json') self.assert403(rv) with self.login(self.data.get_authorized_student().username): rv = self.client.post(url, data=json.dumps(consumer_expected), content_type='application/json') self.assert403(rv) # Test authorized access with self.login('root'): rv = self.client.post(url, data=json.dumps(consumer_expected), content_type='application/json') self.assert200(rv) self.assertEqual(consumer_expected['oauth_consumer_key'], rv.json['oauth_consumer_key']) self.assertEqual(consumer_expected['oauth_consumer_secret'], rv.json['oauth_consumer_secret']) self.assertEqual( consumer_expected['global_unique_identifier_param'], rv.json['global_unique_identifier_param']) self.assertEqual(consumer_expected['student_number_param'], rv.json['student_number_param']) self.assertTrue(rv.json['active']) # test unique oauth_consumer_key by submitting again rv = self.client.post(url, data=json.dumps(consumer_expected), content_type='application/json') self.assertStatus(rv, 409) self.assertEqual(rv.json['title'], "Consumer Not Saved") self.assertEqual( rv.json['message'], "An LTI consumer with the same consumer key already exists. Please double-check the consumer key and try saving again." ) def test_list_lti_consumers(self): url = self._build_consumer_url() # Test login required rv = self.client.get(url) self.assert401(rv) # Test unauthorized access with self.login(self.data.get_authorized_instructor().username): rv = self.client.get(url) self.assert403(rv) with self.login(self.data.get_authorized_ta().username): rv = self.client.get(url) self.assert403(rv) with self.login(self.data.get_authorized_student().username): rv = self.client.get(url) self.assert403(rv) # Test authorized access with self.login('root'): lti_consumers = self.lti_data.lti_consumers rv = self.client.get(url) self.assert200(rv) self.assertEqual(len(rv.json['objects']), 1) self.assertEqual(rv.json['total'], 1) for index, lti_consumer in enumerate(lti_consumers): self.assertEqual( lti_consumer.oauth_consumer_key, rv.json['objects'][index]['oauth_consumer_key']) self.assertEqual( lti_consumer.global_unique_identifier_param, rv.json['objects'][index] ['global_unique_identifier_param']) self.assertEqual( lti_consumer.student_number_param, rv.json['objects'][index]['student_number_param']) self.assertEqual(lti_consumer.active, rv.json['objects'][index]['active']) self.assertNotIn('oauth_consumer_secret', rv.json['objects'][index]) # test paging for i in range(1, 30): # add 29 more consumers if i % 2 == 0: self.lti_data.create_consumer( oauth_consumer_key='lti_consumer_key_' + str(i)) else: self.lti_data.create_consumer( oauth_consumer_key='lti_consumer_key_' + str(i), global_unique_identifier_param= 'global_unique_identifier_param_' + str(i)) lti_consumers = self.lti_data.lti_consumers rv = self.client.get(url) self.assert200(rv) self.assertEqual(len(rv.json['objects']), 20) self.assertEqual(rv.json['total'], 30) for index, lti_consumer in enumerate(lti_consumers[:20]): self.assertEqual( lti_consumer.oauth_consumer_key, rv.json['objects'][index]['oauth_consumer_key']) self.assertEqual( lti_consumer.global_unique_identifier_param, rv.json['objects'][index] ['global_unique_identifier_param']) self.assertEqual( lti_consumer.student_number_param, rv.json['objects'][index]['student_number_param']) self.assertEqual(lti_consumer.active, rv.json['objects'][index]['active']) self.assertNotIn('oauth_consumer_secret', rv.json['objects'][index]) rv = self.client.get(url + "?page=2") self.assert200(rv) self.assertEqual(len(rv.json['objects']), 10) self.assertEqual(rv.json['total'], 30) for index, lti_consumer in enumerate(lti_consumers[20:]): self.assertEqual( lti_consumer.oauth_consumer_key, rv.json['objects'][index]['oauth_consumer_key']) self.assertEqual( lti_consumer.global_unique_identifier_param, rv.json['objects'][index] ['global_unique_identifier_param']) self.assertEqual( lti_consumer.student_number_param, rv.json['objects'][index]['student_number_param']) self.assertEqual(lti_consumer.active, rv.json['objects'][index]['active']) self.assertNotIn('oauth_consumer_secret', rv.json['objects'][index]) # test order by rv = self.client.get(url + "?orderBy=oauth_consumer_key") self.assert200(rv) self.assertEqual(len(rv.json['objects']), 20) self.assertEqual(rv.json['total'], 30) sorted_lti_consumers = sorted( [consumer for consumer in lti_consumers], key=lambda consumer: consumer.oauth_consumer_key)[:20] for index, lti_consumer in enumerate(sorted_lti_consumers): self.assertEqual( lti_consumer.oauth_consumer_key, rv.json['objects'][index]['oauth_consumer_key']) self.assertEqual( lti_consumer.global_unique_identifier_param, rv.json['objects'][index] ['global_unique_identifier_param']) self.assertEqual( lti_consumer.student_number_param, rv.json['objects'][index]['student_number_param']) self.assertEqual(lti_consumer.active, rv.json['objects'][index]['active']) self.assertNotIn('oauth_consumer_secret', rv.json['objects'][index]) rv = self.client.get(url + "?orderBy=oauth_consumer_key&reverse=true") self.assert200(rv) self.assertEqual(len(rv.json['objects']), 20) self.assertEqual(rv.json['total'], 30) sorted_lti_consumers = sorted( [consumer for consumer in lti_consumers], key=lambda consumer: consumer.oauth_consumer_key, reverse=True)[:20] for index, lti_consumer in enumerate(sorted_lti_consumers): self.assertEqual( lti_consumer.oauth_consumer_key, rv.json['objects'][index]['oauth_consumer_key']) self.assertEqual( lti_consumer.global_unique_identifier_param, rv.json['objects'][index] ['global_unique_identifier_param']) self.assertEqual( lti_consumer.student_number_param, rv.json['objects'][index]['student_number_param']) self.assertEqual(lti_consumer.active, rv.json['objects'][index]['active']) self.assertNotIn('oauth_consumer_secret', rv.json['objects'][index]) def test_get_lti_consumer(self): lti_consumer = self.lti_data.lti_consumer url = self._build_consumer_url(lti_consumer.uuid) # Test login required rv = self.client.get(url) self.assert401(rv) # Test unauthorized access with self.login(self.data.get_authorized_instructor().username): rv = self.client.get(url) self.assert403(rv) with self.login(self.data.get_authorized_ta().username): rv = self.client.get(url) self.assert403(rv) with self.login(self.data.get_authorized_student().username): rv = self.client.get(url) self.assert403(rv) # Test authorized access with self.login('root'): # invalid id rv = self.client.get(self._build_consumer_url("999")) self.assert404(rv) # valid url rv = self.client.get(url) self.assert200(rv) self.assertEqual(lti_consumer.oauth_consumer_key, rv.json['oauth_consumer_key']) self.assertEqual(lti_consumer.oauth_consumer_secret, rv.json['oauth_consumer_secret']) self.assertEqual(lti_consumer.global_unique_identifier_param, rv.json['global_unique_identifier_param']) self.assertEqual(lti_consumer.student_number_param, rv.json['student_number_param']) self.assertTrue(lti_consumer.active, rv.json['active']) def test_edit_lti_consumer(self): lti_consumer = self.lti_data.lti_consumer url = self._build_consumer_url(lti_consumer.uuid) consumer_expected = { 'id': lti_consumer.uuid, 'oauth_consumer_key': 'edit_consumer_key', 'oauth_consumer_secret': 'edit_consumer_secret', 'global_unique_identifier_param': 'edit_consumer_global_unique_identifier_param', 'student_number_param': 'edit_student_number_param', 'active': False } # Test login required rv = self.client.post(url, data=json.dumps(consumer_expected), content_type='application/json') self.assert401(rv) # Test unauthorized access with self.login(self.data.get_authorized_instructor().username): rv = self.client.post(url, data=json.dumps(consumer_expected), content_type='application/json') self.assert403(rv) with self.login(self.data.get_authorized_ta().username): rv = self.client.post(url, data=json.dumps(consumer_expected), content_type='application/json') self.assert403(rv) with self.login(self.data.get_authorized_student().username): rv = self.client.post(url, data=json.dumps(consumer_expected), content_type='application/json') self.assert403(rv) # Test authorized access with self.login('root'): # invalid id rv = self.client.post(self._build_consumer_url("999"), data=json.dumps(consumer_expected), content_type='application/json') self.assert404(rv) # invalid id invalid_expected = consumer_expected.copy() invalid_expected['id'] = "999" rv = self.client.post(url, data=json.dumps(invalid_expected), content_type='application/json') self.assert400(rv) # valid url rv = self.client.post(url, data=json.dumps(consumer_expected), content_type='application/json') self.assert200(rv) self.assertEqual(consumer_expected['oauth_consumer_key'], rv.json['oauth_consumer_key']) self.assertEqual(consumer_expected['oauth_consumer_secret'], rv.json['oauth_consumer_secret']) self.assertEqual( consumer_expected['global_unique_identifier_param'], rv.json['global_unique_identifier_param']) self.assertEqual(consumer_expected['student_number_param'], rv.json['student_number_param']) self.assertEqual(consumer_expected['active'], rv.json['active']) # valid url (empty global_unique_identifier_param) consumer_expected_no_override = consumer_expected.copy() consumer_expected_no_override[ 'global_unique_identifier_param'] = "" rv = self.client.post( url, data=json.dumps(consumer_expected_no_override), content_type='application/json') self.assert200(rv) self.assertEqual(consumer_expected['oauth_consumer_key'], rv.json['oauth_consumer_key']) self.assertEqual(consumer_expected['oauth_consumer_secret'], rv.json['oauth_consumer_secret']) self.assertIsNone(rv.json['global_unique_identifier_param']) self.assertEqual(consumer_expected['student_number_param'], rv.json['student_number_param']) self.assertEqual(consumer_expected['active'], rv.json['active']) # valid url (empty student_number_param) consumer_expected_no_override = consumer_expected.copy() consumer_expected_no_override['student_number_param'] = "" rv = self.client.post( url, data=json.dumps(consumer_expected_no_override), content_type='application/json') self.assert200(rv) self.assertEqual(consumer_expected['oauth_consumer_key'], rv.json['oauth_consumer_key']) self.assertEqual(consumer_expected['oauth_consumer_secret'], rv.json['oauth_consumer_secret']) self.assertEqual( consumer_expected['global_unique_identifier_param'], rv.json['global_unique_identifier_param']) self.assertIsNone(rv.json['student_number_param']) self.assertEqual(consumer_expected['active'], rv.json['active']) # test edit duplicate consumer key lti_consumer2 = self.lti_data.create_consumer() url = self._build_consumer_url(lti_consumer2.uuid) consumer2_expected = consumer_expected.copy() consumer2_expected['id'] = lti_consumer2.uuid rv = self.client.post(url, data=json.dumps(consumer2_expected), content_type='application/json') self.assertStatus(rv, 409) self.assertEqual(rv.json['title'], "Consumer Not Saved") self.assertEqual( rv.json['message'], "An LTI consumer with the same consumer key already exists. Please double-check the consumer key and try saving again." )
def setUp(self): super(ComPAIRLearningRecordTestCase, self).setUp() self.data = SimpleAssignmentTestData() self.lti_data = LTITestData() self.user = self.data.authorized_student self.setup_session_data(self.user) self.course = self.data.main_course self.lti_context = self.lti_data.create_context( self.lti_data.lti_consumer, compair_course_id=self.course.id, lis_course_offering_sourcedid="sis_course_id", lis_course_section_sourcedid="sis_section_id", ) self.assignment = self.data.assignments[0] self.answer = AnswerFactory(assignment=self.assignment, user=self.user) db.session.commit() self.expected_caliper_course = { 'academicSession': self.course.term, 'dateCreated': self.course.created.replace( tzinfo=pytz.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z', 'dateModified': self.course.modified.replace( tzinfo=pytz.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z', 'id': "https://localhost:8888/app/course/" + self.course.uuid, 'name': self.course.name, 'type': 'CourseOffering', 'otherIdentifiers': [{ 'identifier': self.lti_context.context_id, 'identifierType': 'LtiContextId', 'type': 'SystemIdentifier', 'extensions': { 'lis_course_offering_sourcedid': 'sis_course_id', 'lis_course_section_sourcedid': 'sis_section_id', 'oauth_consumer_key': self.lti_data.lti_consumer.oauth_consumer_key, }, }] } self.expected_caliper_assignment = { 'name': self.assignment.name, 'type': 'Assessment', 'dateCreated': self.assignment.created.replace( tzinfo=pytz.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z', 'dateModified': self.assignment.modified.replace( tzinfo=pytz.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z', 'dateToStartOn': self.assignment.answer_start.replace( tzinfo=pytz.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z', 'description': self.assignment.description, 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid, 'isPartOf': self.expected_caliper_course, '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' }], } self.expected_caliper_assignment_question = { 'name': self.assignment.name, 'type': 'AssessmentItem', 'dateCreated': self.assignment.created.replace( tzinfo=pytz.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z', 'dateModified': self.assignment.modified.replace( tzinfo=pytz.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z', 'dateToStartOn': self.assignment.answer_start.replace( tzinfo=pytz.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z', 'dateToSubmit': self.assignment.answer_end.replace( tzinfo=pytz.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z', 'description': self.assignment.description, 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/question", 'isPartOf': self.expected_caliper_assignment, } self.expected_caliper_answer_attempt = { 'assignable': self.expected_caliper_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).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z', 'endedAtTime': self.answer.attempt_ended.replace( tzinfo=pytz.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z', 'type': 'Attempt' } self.expected_caliper_answer = { 'attempt': self.expected_caliper_answer_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).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z', 'dateModified': self.answer.modified.replace( tzinfo=pytz.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z', 'extensions': { 'characterCount': len(self.answer.content), 'content': self.answer.content, 'isDraft': False, 'wordCount': 8, } } self.expected_xapi_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' } self.expected_xapi_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' } self.expected_xapi_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' } self.expected_xapi_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' }
class AnswerCommentAPITests(ComPAIRAPITestCase): """ Tests for answer comment API """ resource = AnswerCommentAPI api = api def setUp(self): super(AnswerCommentAPITests, self).setUp() self.data = AnswerCommentsTestData() self.course = self.data.get_course() self.assignments = self.data.get_assignments() self.answers = self.data.get_answers_by_assignment() self.assignment = self.assignments[0] self.assignment.enable_self_evaluation = True db.session.commit() self.assignment.calculate_grades() self.lti_data = LTITestData() def test_get_single_answer_comment(self): comment = self.data.get_answer_comments_by_assignment(self.assignment)[0] url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid=comment.uuid) draft_comment = self.data.get_answer_comments_by_assignment(self.assignment)[2] draft_url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid=draft_comment.uuid) # test login required rv = self.client.get(url) self.assert401(rv) # test unauthorized user with self.login(self.data.get_unauthorized_instructor().username): rv = self.client.get(url) self.assert403(rv) # test unauthorized user student fetching draft of another student with self.login(self.data.get_extra_student(0).username): rv = self.client.get(draft_url) self.assert403(rv) # test invalid course id with self.login(self.data.get_authorized_instructor().username): invalid_url = self.get_url( course_uuid="999", assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid=comment.uuid) rv = self.client.get(invalid_url) self.assert404(rv) # test invalid answer id invalid_url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid="999", answer_comment_uuid=comment.uuid) rv = self.client.get(invalid_url) self.assert404(rv) # test invalid comment id invalid_url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid="999") rv = self.client.get(invalid_url) self.assert404(rv) # test authorized instructor rv = self.client.get(url) self.assert200(rv) self.assertEqual(comment.content, rv.json['content']) # test draft rv = self.client.get(draft_url) self.assert200(rv) self.assertEqual(draft_comment.content, rv.json['content']) self.assertTrue(rv.json['draft']) # test author with self.login(self.data.get_extra_student(0).username): rv = self.client.get(url) self.assert200(rv) self.assertEqual(comment.content, rv.json['content']) # test draft author with self.login(self.data.get_extra_student(1).username): rv = self.client.get(draft_url) self.assert200(rv) self.assertEqual(draft_comment.content, rv.json['content']) self.assertTrue(rv.json['draft']) @mock.patch('compair.tasks.lti_outcomes.update_lti_course_grades.run') @mock.patch('compair.tasks.lti_outcomes.update_lti_assignment_grades.run') def test_edit_answer_comment(self, mocked_update_assignment_grades_run, mocked_update_course_grades_run): comment = self.data.get_answer_comments_by_assignment(self.assignment)[0] url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid=comment.uuid ) content = { 'id': comment.uuid, 'content': 'insightful.', 'comment_type': AnswerCommentType.private.value } draft_comment = self.data.get_answer_comments_by_assignment(self.assignment)[2] draft_url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid=draft_comment.uuid) draft_content = { 'id': draft_comment.uuid, 'content': 'insightful.', 'comment_type': AnswerCommentType.private.value, 'draft': True } # test login required rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert401(rv) # test unauthorized user with self.login(self.data.get_unauthorized_instructor().username): rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert403(rv) # test invalid course id with self.login(self.data.get_authorized_instructor().username): invalid_url = self.get_url( course_uuid="999", assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid=comment.uuid) rv = self.client.post(invalid_url, data=json.dumps(content), content_type='application/json') self.assert404(rv) # test invalid answer id invalid_url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid="999", answer_comment_uuid=comment.uuid) rv = self.client.post(invalid_url, data=json.dumps(content), content_type='application/json') self.assert404(rv) # test invalid comment id invalid_url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid="999") rv = self.client.post(invalid_url, data=json.dumps(content), content_type='application/json') self.assert404(rv) # test unmatched comment ids invalid = content.copy() invalid['id'] = self.data.get_answer_comments_by_assignment(self.assignment)[1].uuid rv = self.client.post(url, data=json.dumps(invalid), content_type='application/json') self.assert400(rv) self.assertEqual("Comment id does not match URL.", rv.json['error']) # test empty content empty = content.copy() empty['content'] = '' rv = self.client.post(url, data=json.dumps(empty), content_type='application/json') self.assert400(rv) self.assertEqual("The comment content is empty!", rv.json['error']) # test empty comment_type empty = content.copy() empty['comment_type'] = '' rv = self.client.post(url, data=json.dumps(empty), content_type='application/json') self.assert400(rv) # test authorized instructor with self.login(self.data.get_authorized_instructor().username): rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert200(rv) self.assertEqual(content['content'], rv.json['content']) # test author with self.login(self.data.get_extra_student(0).username): content['content'] = 'I am the author' rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert200(rv) self.assertEqual(content['content'], rv.json['content']) self.assertFalse(rv.json['draft']) # ignored setting draft to True when draft is already False content['draft'] = True rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert200(rv) self.assertEqual(content['content'], rv.json['content']) self.assertFalse(rv.json['draft']) # test draft author with self.login(self.data.get_extra_student(1).username): draft_content['content'] = 'I am the author' rv = self.client.post(draft_url, data=json.dumps(draft_content), content_type='application/json') self.assert200(rv) self.assertEqual(draft_content['content'], rv.json['content']) self.assertTrue(rv.json['draft']) # can change draft to False when draft is True draft_content['draft'] = False rv = self.client.post(draft_url, data=json.dumps(draft_content), content_type='application/json') self.assert200(rv) self.assertFalse(rv.json['draft']) answer = self.answers[self.assignment.id][0] self_evaluation = self.data.create_answer_comment( answer.user, answer, comment_type=AnswerCommentType.self_evaluation, draft=True) self_evaluation_url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=answer.uuid, answer_comment_uuid=self_evaluation.uuid) with self.login(answer.user.username): lti_consumer = self.lti_data.lti_consumer (lti_user_resource_link1, lti_user_resource_link2) = self.lti_data.setup_student_user_resource_links( answer.user, self.course, self.assignment) course_grade = CourseGrade.get_user_course_grade(self.course, answer.user).grade assignment_grade = AssignmentGrade.get_user_assignment_grade(self.assignment, answer.user).grade content = { 'id': self_evaluation.uuid, 'content': 'insightful.', 'comment_type': AnswerCommentType.self_evaluation.value, 'draft': True } rv = self.client.post(self_evaluation_url, data=json.dumps(content), content_type='application/json') self.assert200(rv) self.assertEqual(content['content'], rv.json['content']) self.assertTrue(rv.json['draft']) # grades should not change new_course_grade = CourseGrade.get_user_course_grade(self.course, answer.user).grade new_assignment_grade = AssignmentGrade.get_user_assignment_grade(self.assignment, answer.user).grade self.assertEqual(new_course_grade, course_grade) self.assertEqual(new_assignment_grade, assignment_grade) # can change draft to False when draft is True content['draft'] = False rv = self.client.post(self_evaluation_url, data=json.dumps(content), content_type='application/json') self.assert200(rv) self.assertFalse(rv.json['draft']) # grades should increase new_course_grade = CourseGrade.get_user_course_grade(self.course, answer.user) new_assignment_grade = AssignmentGrade.get_user_assignment_grade(self.assignment, answer.user) self.assertGreater(new_course_grade.grade, course_grade) self.assertGreater(new_assignment_grade.grade, assignment_grade) mocked_update_assignment_grades_run.assert_called_once_with( lti_consumer.id, [(lti_user_resource_link2.lis_result_sourcedid, new_assignment_grade.id)] ) mocked_update_assignment_grades_run.reset_mock() mocked_update_course_grades_run.assert_called_once_with( lti_consumer.id, [(lti_user_resource_link1.lis_result_sourcedid, new_course_grade.id)] ) mocked_update_course_grades_run.reset_mock() @mock.patch('compair.tasks.lti_outcomes.update_lti_course_grades.run') @mock.patch('compair.tasks.lti_outcomes.update_lti_assignment_grades.run') def test_delete_answer_comment(self, mocked_update_assignment_grades_run, mocked_update_course_grades_run): comment = self.data.get_answer_comments_by_assignment(self.assignment)[0] url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid=comment.uuid) # test login required rv = self.client.delete(url) self.assert401(rv) # test unauthorized user with self.login(self.data.get_unauthorized_instructor().username): rv = self.client.delete(url) self.assert403(rv) # test invalid comment id with self.login(self.data.get_authorized_instructor().username): invalid_url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid="999") rv = self.client.delete(invalid_url) self.assert404(rv) # test authorized instructor rv = self.client.delete(url) self.assert200(rv) self.assertEqual(comment.uuid, rv.json['id']) # test author with self.login(self.data.get_extra_student(1).username): comment = self.data.get_answer_comments_by_assignment(self.assignment)[1] url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid=comment.uuid) rv = self.client.delete(url) self.assert200(rv) self.assertEqual(comment.uuid, rv.json['id']) # test delete self-evaulation answer = self.answers[self.assignment.id][0] self_evaluation = self.data.create_answer_comment(answer.user, answer, comment_type=AnswerCommentType.self_evaluation) self.assignment.calculate_grade(answer.user) self.course.calculate_grade(answer.user) lti_consumer = self.lti_data.lti_consumer (lti_user_resource_link1, lti_user_resource_link2) = self.lti_data.setup_student_user_resource_links( answer.user, self.course, self.assignment) with self.login(self.data.get_authorized_instructor().username): course_grade = CourseGrade.get_user_course_grade(self.course, answer.user).grade assignment_grade = AssignmentGrade.get_user_assignment_grade(self.assignment, answer.user).grade url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=answer.uuid, answer_comment_uuid=self_evaluation.uuid) rv = self.client.delete(url) self.assert200(rv) self.assertEqual(self_evaluation.uuid, rv.json['id']) # grades should decrease new_course_grade = CourseGrade.get_user_course_grade(self.course, answer.user) new_assignment_grade = AssignmentGrade.get_user_assignment_grade(self.assignment, answer.user) self.assertLess(new_course_grade.grade, course_grade) self.assertLess(new_assignment_grade.grade, assignment_grade) mocked_update_assignment_grades_run.assert_called_once_with( lti_consumer.id, [(lti_user_resource_link2.lis_result_sourcedid, new_assignment_grade.id)] ) mocked_update_assignment_grades_run.reset_mock() mocked_update_course_grades_run.assert_called_once_with( lti_consumer.id, [(lti_user_resource_link1.lis_result_sourcedid, new_course_grade.id)] ) mocked_update_course_grades_run.reset_mock()
def setUp(self): super(ComPAIRLearningRecordTestCase, self).setUp() self.data = SimpleAssignmentTestData() self.lti_data = LTITestData() self.user = self.data.authorized_student self.setup_session_data(self.user) self.course = self.data.main_course self.lti_context = self.lti_data.create_context( self.lti_data.lti_consumer, compair_course_id=self.course.id, lis_course_offering_sourcedid="sis_course_id", lis_course_section_sourcedid="sis_section_id", ) self.assignment = self.data.assignments[0] self.expected_caliper_course = { 'academicSession': self.course.term, 'dateCreated': self.course.created.replace( tzinfo=pytz.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z', 'dateModified': self.course.modified.replace( tzinfo=pytz.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z', 'id': "https://localhost:8888/app/course/" + self.course.uuid, 'name': self.course.name, 'type': 'CourseOffering', 'otherIdentifiers': [{ 'identifier': self.lti_context.context_id, 'identifierType': 'LtiContextId', 'type': 'SystemIdentifier', 'extensions': { 'lis_course_offering_sourcedid': 'sis_course_id', 'lis_course_section_sourcedid': 'sis_section_id', 'oauth_consumer_key': self.lti_data.lti_consumer.oauth_consumer_key, }, }] } self.expected_caliper_assignment = { 'name': self.assignment.name, 'type': 'Assessment', 'dateCreated': self.assignment.created.replace( tzinfo=pytz.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z', 'dateModified': self.assignment.modified.replace( tzinfo=pytz.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z', 'dateToStartOn': self.assignment.answer_start.replace( tzinfo=pytz.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z', 'description': self.assignment.description, 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid, 'isPartOf': self.expected_caliper_course, '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' }], } self.expected_xapi_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' } self.expected_xapi_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' }
class ComparisonLearningRecordTests(ComPAIRLearningRecordTestCase): def setUp(self): super(ComPAIRLearningRecordTestCase, self).setUp() self.data = ComparisonTestData() self.lti_data = LTITestData() self.user = self.data.authorized_student self.setup_session_data(self.user) self.course = self.data.main_course self.lti_context = self.lti_data.create_context( self.lti_data.lti_consumer, compair_course_id=self.course.id, lis_course_offering_sourcedid="sis_course_id", lis_course_section_sourcedid="sis_section_id", ) self.assignment = self.data.assignments[0] self.criterion = self.assignment.criteria[0] self.answer1 = self.data.answers[0] self.answer2 = self.data.answers[1] self.example_comparison = ComparisonFactory(assignment=self.assignment, user=self.user, answer1_id=self.answer1.id, answer2_id=self.answer2.id, winner=None, completed=False) self.example_comparison_criterion = ComparisonCriterionFactory( comparison=self.example_comparison, criterion=self.criterion, winner=WinningAnswer.answer1, ) self.comparison = ComparisonFactory(assignment=self.assignment, user=self.user, answer1_id=self.answer1.id, answer2_id=self.answer2.id, winner=None, completed=False) self.comparison_criterion = ComparisonCriterionFactory( comparison=self.comparison, criterion=self.criterion, winner=WinningAnswer.answer1, ) db.session.commit() self.expected_caliper_course = { '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", }] } } self.expected_caliper_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': self.expected_caliper_course, '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' }], } self.expected_caliper_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': self.expected_caliper_assignment, } self.expected_caliper_answer1_attempt = { 'assignable': self.expected_caliper_assignment_question, 'assignee': self.get_compair_caliper_actor(self.answer1.user), 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/question/attempt/" + self.answer1.attempt_uuid, 'duration': "PT05M00S", 'startedAtTime': self.answer1.attempt_started.replace(tzinfo=pytz.utc).isoformat(), 'endedAtTime': self.answer1.attempt_ended.replace(tzinfo=pytz.utc).isoformat(), 'type': 'Attempt' } self.expected_caliper_answer1 = { 'attempt': self.expected_caliper_answer1_attempt, 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/answer/" + self.answer1.uuid, 'type': 'Response', 'dateCreated': self.answer1.created.replace(tzinfo=pytz.utc).isoformat(), 'dateModified': self.answer1.modified.replace(tzinfo=pytz.utc).isoformat(), 'extensions': { 'characterCount': len(self.answer1.content), 'content': self.answer1.content, 'isDraft': False, 'wordCount': 8, '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 }, } }, } } self.expected_caliper_answer2_attempt = { 'assignable': self.expected_caliper_assignment_question, 'assignee': self.get_compair_caliper_actor(self.answer2.user), 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/question/attempt/" + self.answer2.attempt_uuid, 'duration': "PT05M00S", 'startedAtTime': self.answer2.attempt_started.replace(tzinfo=pytz.utc).isoformat(), 'endedAtTime': self.answer2.attempt_ended.replace(tzinfo=pytz.utc).isoformat(), 'type': 'Attempt' } self.expected_caliper_answer2 = { 'attempt': self.expected_caliper_answer2_attempt, 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/answer/" + self.answer2.uuid, 'type': 'Response', 'dateCreated': self.answer2.created.replace(tzinfo=pytz.utc).isoformat(), 'dateModified': self.answer2.modified.replace(tzinfo=pytz.utc).isoformat(), 'extensions': { 'characterCount': len(self.answer2.content), 'content': self.answer2.content, 'isDraft': False, 'wordCount': 8, '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 }, } }, } } self.expected_caliper_comparison_question = { 'name': "Assignment comparison #1", 'type': 'AssessmentItem', 'dateCreated': self.assignment.created.replace(tzinfo=pytz.utc).isoformat(), 'dateModified': self.assignment.modified.replace(tzinfo=pytz.utc).isoformat(), 'dateToStartOn': self.assignment.answer_end.replace(tzinfo=pytz.utc).isoformat(), 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/comparison/question/1", 'isPartOf': self.expected_caliper_assignment, } self.expected_xapi_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' } self.expected_xapi_sis_course = { 'id': 'https://localhost:8888/course/' + self.lti_context.lis_course_offering_sourcedid, 'objectType': 'Activity' } self.expected_xapi_sis_section = { 'id': 'https://localhost:8888/course/' + self.lti_context.lis_course_offering_sourcedid + '/section/' + self.lti_context.lis_course_section_sourcedid, 'objectType': 'Activity' } self.expected_xapi_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' } self.expected_xapi_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' } self.expected_xapi_answer1_attempt = { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/question/attempt/" + self.answer1.attempt_uuid, 'definition': { 'type': 'http://adlnet.gov/expapi/activities/attempt', 'extensions': { 'http://id.tincanapi.com/extension/attempt': { 'duration': "PT05M00S", 'startedAtTime': self.answer1.attempt_started.replace( tzinfo=pytz.utc).isoformat(), 'endedAtTime': self.answer1.attempt_ended.replace( tzinfo=pytz.utc).isoformat(), } } }, 'objectType': 'Activity' } self.expected_xapi_answer1 = { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/answer/" + self.answer1.uuid, 'definition': { 'type': 'http://id.tincanapi.com/activitytype/solution', 'extensions': { 'http://id.tincanapi.com/extension/isDraft': False } }, 'objectType': 'Activity' } self.expected_xapi_answer2_attempt = { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/question/attempt/" + self.answer2.attempt_uuid, 'definition': { 'type': 'http://adlnet.gov/expapi/activities/attempt', 'extensions': { 'http://id.tincanapi.com/extension/attempt': { 'duration': "PT05M00S", 'startedAtTime': self.answer2.attempt_started.replace( tzinfo=pytz.utc).isoformat(), 'endedAtTime': self.answer2.attempt_ended.replace( tzinfo=pytz.utc).isoformat(), } } }, 'objectType': 'Activity' } self.expected_xapi_answer2 = { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/answer/" + self.answer2.uuid, 'definition': { 'type': 'http://id.tincanapi.com/activitytype/solution', 'extensions': { 'http://id.tincanapi.com/extension/isDraft': False } }, 'objectType': 'Activity' } self.expected_xapi_comparison_question = { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/comparison/question/1", 'definition': { 'type': 'http://adlnet.gov/expapi/activities/question', 'name': { 'en-US': "Assignment comparison #1" } }, 'objectType': 'Activity' } def test_on_comparison_update(self): completed_count = 0 for (is_comparison_example, comparison) in [(True, self.example_comparison), (False, self.comparison)]: for completed in [False, True]: comparison.completed = completed comparison.winner = WinningAnswer.answer1 if completed else None db.session.commit() on_comparison_update.send( current_app._get_current_object(), event_name=on_comparison_update.name, user=self.user, assignment=self.assignment, comparison=comparison, is_comparison_example=is_comparison_example) if completed: completed_count += 1 current_comparison = completed_count if completed else completed_count + 1 self.expected_caliper_comparison_question[ 'name'] = "Assignment comparison #" + str( current_comparison) self.expected_caliper_comparison_question[ 'id'] = "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/comparison/question/" + str( current_comparison) self.expected_xapi_comparison_question['definition']['name'][ 'en-US'] = "Assignment comparison #{}".format( current_comparison) self.expected_xapi_comparison_question[ 'id'] = "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/comparison/question/" + str( current_comparison) expected_caliper_comparison_attempt = { 'assignable': self.expected_caliper_comparison_question, 'assignee': self.get_compair_caliper_actor(self.user), 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/comparison/question/" + str(current_comparison) + "/attempt/" + comparison.attempt_uuid, 'duration': "PT05M00S", 'startedAtTime': comparison.attempt_started.replace( tzinfo=pytz.utc).isoformat(), 'endedAtTime': comparison.attempt_ended.replace( tzinfo=pytz.utc).isoformat(), 'type': 'Attempt' } expected_caliper_comparison = { 'attempt': expected_caliper_comparison_attempt, 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/comparison/" + comparison.uuid, 'type': 'Response', 'dateCreated': comparison.created.replace(tzinfo=pytz.utc).isoformat(), 'dateModified': comparison.modified.replace(tzinfo=pytz.utc).isoformat(), 'extensions': { 'pairingAlgorithm': self.comparison.pairing_algorithm.value, 'winner': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/answer/" + self.answer1.uuid if completed else "Undecided", 'criteria': { "https://localhost:8888/app/criterion/" + self.criterion.uuid: "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/answer/" + self.answer1.uuid, }, "answers": [ "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/answer/" + self.answer1.uuid, "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/answer/" + self.answer2.uuid, ], "completed": completed } } events = self.get_and_clear_caliper_event_log() expected_caliper_events = [{ 'action': 'Completed', 'actor': self.get_compair_caliper_actor(self.user), 'membership': self.get_caliper_membership(self.course, self.user, self.lti_context), 'object': self.expected_caliper_comparison_question, 'generated': expected_caliper_comparison, 'session': self.get_caliper_session( self.get_compair_caliper_actor(self.user)), 'type': 'AssessmentItemEvent' }, { 'action': 'Submitted', 'actor': self.get_compair_caliper_actor(self.user), 'membership': self.get_caliper_membership(self.course, self.user, self.lti_context), 'object': self.expected_caliper_assignment, 'generated': { 'assignable': self.expected_caliper_assignment, 'assignee': self.get_compair_caliper_actor(self.user), 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/attempt/" + comparison.attempt_uuid, 'duration': "PT05M00S", 'startedAtTime': comparison.attempt_started.replace( tzinfo=pytz.utc).isoformat(), 'endedAtTime': comparison.attempt_ended.replace( tzinfo=pytz.utc).isoformat(), 'type': 'Attempt' }, 'session': self.get_caliper_session( self.get_compair_caliper_actor(self.user)), 'type': 'AssessmentEvent' }] if not is_comparison_example and completed: expected_caliper_events.append({ 'action': 'Ranked', 'actor': self.get_compair_caliper_actor(self.user), 'membership': self.get_caliper_membership(self.course, self.user, self.lti_context), 'object': self.expected_caliper_answer1, 'session': self.get_caliper_session( self.get_compair_caliper_actor(self.user)), 'type': 'Event' }) expected_caliper_events.append({ 'action': 'Ranked', 'actor': self.get_compair_caliper_actor(self.user), 'membership': self.get_caliper_membership(self.course, self.user, self.lti_context), 'object': self.expected_caliper_answer2, 'session': self.get_caliper_session( self.get_compair_caliper_actor(self.user)), 'type': 'Event' }) self.assertEqual(len(events), len(expected_caliper_events)) for index, expected_event in enumerate( expected_caliper_events): self.assertEqual(events[index], expected_event) expected_xapi_comparison_attempt = { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/comparison/question/" + str(current_comparison) + "/attempt/" + comparison.attempt_uuid, 'definition': { 'type': 'http://adlnet.gov/expapi/activities/attempt', 'extensions': { 'http://id.tincanapi.com/extension/attempt': { 'duration': "PT05M00S", 'startedAtTime': comparison.attempt_started.replace( tzinfo=pytz.utc).isoformat(), 'endedAtTime': comparison.attempt_ended.replace( tzinfo=pytz.utc).isoformat(), } } }, 'objectType': 'Activity' } expected_xapi_comparison = { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/comparison/" + comparison.uuid, 'definition': { 'type': 'http://id.tincanapi.com/activitytype/solution', 'name': { 'en-US': "Assignment comparison" }, 'extensions': { 'http://id.tincanapi.com/extension/completed': completed } }, 'objectType': 'Activity' } statements = self.get_and_clear_xapi_statement_log() expected_xapi_statements = [{ "actor": self.get_compair_xapi_actor(self.user), "verb": { 'id': 'http://adlnet.gov/expapi/verbs/completed', 'display': { 'en-US': 'completed' } }, "object": expected_xapi_comparison, "context": { 'registration': comparison.attempt_uuid, 'contextActivities': { 'parent': [ self.expected_xapi_comparison_question, self.expected_xapi_answer1, self.expected_xapi_answer2, expected_xapi_comparison_attempt ], 'grouping': [ self.expected_xapi_assignment, self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section ] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } }, "result": { 'success': True, 'duration': "PT05M00S", 'completion': completed, 'response': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/answer/" + self.answer1.uuid if completed else "Undecided", 'extensions': { 'http://xapi.learninganalytics.ubc.ca/extension/criteria': { "https://localhost:8888/app/criterion/" + self.criterion.uuid: "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/answer/" + self.answer1.uuid, } } } }, { "actor": self.get_compair_xapi_actor(self.user), "verb": { 'id': 'http://activitystrea.ms/schema/1.0/submit', 'display': { 'en-US': 'submitted' } }, "object": { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/attempt/" + comparison.attempt_uuid, 'definition': { 'type': 'http://adlnet.gov/expapi/activities/attempt', 'extensions': { 'http://id.tincanapi.com/extension/attempt': { 'duration': "PT05M00S", 'startedAtTime': comparison.attempt_started.replace( tzinfo=pytz.utc).isoformat(), 'endedAtTime': comparison.attempt_ended.replace( tzinfo=pytz.utc).isoformat(), } } }, 'objectType': 'Activity' }, "context": { 'registration': comparison.attempt_uuid, 'contextActivities': { 'parent': [self.expected_xapi_assignment], 'grouping': [ self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section ] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } }, "result": { 'success': True, 'completion': completed } }] if not is_comparison_example and completed: expected_xapi_statements.append({ "actor": self.get_compair_xapi_actor(self.user), "verb": { 'id': 'https://w3id.org/xapi/dod-isd/verbs/ranked', 'display': { 'en-US': 'ranked' } }, "object": self.expected_xapi_answer1, "context": { 'registration': comparison.attempt_uuid, 'contextActivities': { 'parent': [ self.expected_xapi_assignment_question, self.expected_xapi_answer1_attempt ], 'grouping': [ self.expected_xapi_assignment, self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section ] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } }, "result": { 'score': { 'raw': 5.0 }, 'extensions': { 'http://xapi.learninganalytics.ubc.ca/extension/score-details': { '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_xapi_statements.append({ "actor": self.get_compair_xapi_actor(self.user), "verb": { 'id': 'https://w3id.org/xapi/dod-isd/verbs/ranked', 'display': { 'en-US': 'ranked' } }, "object": self.expected_xapi_answer2, "context": { 'registration': comparison.attempt_uuid, 'contextActivities': { 'parent': [ self.expected_xapi_assignment_question, self.expected_xapi_answer2_attempt ], 'grouping': [ self.expected_xapi_assignment, self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section ] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } }, "result": { 'score': { 'raw': 5.0 }, 'extensions': { 'http://xapi.learninganalytics.ubc.ca/extension/score-details': { '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 }, } } } } }) self.assertEqual(len(statements), len(expected_xapi_statements)) for index, expected_statement in enumerate( expected_xapi_statements): self.assertEqual(statements[index], expected_statement)
class AssignmentLearningRecordTests(ComPAIRLearningRecordTestCase): def setUp(self): super(ComPAIRLearningRecordTestCase, self).setUp() self.data = SimpleAssignmentTestData() self.lti_data = LTITestData() self.user = self.data.authorized_student self.setup_session_data(self.user) self.course = self.data.main_course self.lti_context = self.lti_data.create_context( self.lti_data.lti_consumer, compair_course_id=self.course.id, lis_course_offering_sourcedid="sis_course_id", lis_course_section_sourcedid="sis_section_id", ) self.assignment = self.data.assignments[0] self.expected_caliper_course = { 'academicSession': self.course.term, 'dateCreated': self.course.created.replace( tzinfo=pytz.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z', 'dateModified': self.course.modified.replace( tzinfo=pytz.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z', 'id': "https://localhost:8888/app/course/" + self.course.uuid, 'name': self.course.name, 'type': 'CourseOffering', 'otherIdentifiers': [{ 'identifier': self.lti_context.context_id, 'identifierType': 'LtiContextId', 'type': 'SystemIdentifier', 'extensions': { 'lis_course_offering_sourcedid': 'sis_course_id', 'lis_course_section_sourcedid': 'sis_section_id', 'oauth_consumer_key': self.lti_data.lti_consumer.oauth_consumer_key, }, }] } self.expected_caliper_assignment = { 'name': self.assignment.name, 'type': 'Assessment', 'dateCreated': self.assignment.created.replace( tzinfo=pytz.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z', 'dateModified': self.assignment.modified.replace( tzinfo=pytz.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z', 'dateToStartOn': self.assignment.answer_start.replace( tzinfo=pytz.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z', 'description': self.assignment.description, 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid, 'isPartOf': self.expected_caliper_course, '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' }], } self.expected_xapi_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' } self.expected_xapi_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' } def test_on_assignment_create(self): on_assignment_create.send(current_app._get_current_object(), event_name=on_assignment_create.name, user=self.user, assignment=self.assignment) events = self.get_and_clear_caliper_event_log() expected_caliper_event = { 'action': 'Created', 'profile': 'ResourceManagementProfile', 'actor': self.get_compair_caliper_actor(self.user), 'membership': self.get_caliper_membership(self.course, self.user, self.lti_context), 'object': self.expected_caliper_assignment, 'session': self.get_caliper_session(self.get_compair_caliper_actor( self.user)), 'type': 'ResourceManagementEvent' } self.assertEqual(len(events), 1) self.assertEqual(events[0], expected_caliper_event) statements = self.get_and_clear_xapi_statement_log() expected_xapi_statement = { "actor": self.get_compair_xapi_actor(self.user), "verb": { 'id': 'http://activitystrea.ms/schema/1.0/author', 'display': { 'en-US': 'authored' } }, "object": self.expected_xapi_assignment, "context": { 'contextActivities': { 'parent': [self.expected_xapi_course], 'grouping': [] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info(), 'sis_courses': [{ 'id': 'sis_course_id', 'section_ids': ['sis_section_id'] }] } } } self.assertEqual(len(statements), 1) self.assertEqual(statements[0], expected_xapi_statement) def test_on_assignment_modified(self): on_assignment_modified.send(current_app._get_current_object(), event_name=on_assignment_modified.name, user=self.user, assignment=self.assignment) events = self.get_and_clear_caliper_event_log() expected_caliper_event = { 'action': 'Modified', 'profile': 'ResourceManagementProfile', 'actor': self.get_compair_caliper_actor(self.user), 'membership': self.get_caliper_membership(self.course, self.user, self.lti_context), 'object': self.expected_caliper_assignment, 'session': self.get_caliper_session(self.get_compair_caliper_actor( self.user)), 'type': 'ResourceManagementEvent' } self.assertEqual(len(events), 1) self.assertEqual(events[0], expected_caliper_event) statements = self.get_and_clear_xapi_statement_log() expected_xapi_statement = { "actor": self.get_compair_xapi_actor(self.user), "verb": { 'id': 'http://activitystrea.ms/schema/1.0/update', 'display': { 'en-US': 'updated' } }, "object": self.expected_xapi_assignment, "context": { 'contextActivities': { 'parent': [self.expected_xapi_course], 'grouping': [] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info(), 'sis_courses': [{ 'id': 'sis_course_id', 'section_ids': ['sis_section_id'] }] } } } self.assertEqual(len(statements), 1) self.assertEqual(statements[0], expected_xapi_statement) def test_on_assignment_delete(self): on_assignment_delete.send(current_app._get_current_object(), event_name=on_assignment_delete.name, user=self.user, assignment=self.assignment) events = self.get_and_clear_caliper_event_log() expected_caliper_event = { 'action': 'Archived', 'profile': 'ResourceManagementProfile', 'actor': self.get_compair_caliper_actor(self.user), 'membership': self.get_caliper_membership(self.course, self.user, self.lti_context), 'object': self.expected_caliper_assignment, 'session': self.get_caliper_session(self.get_compair_caliper_actor( self.user)), 'type': 'ResourceManagementEvent' } self.assertEqual(len(events), 1) self.assertEqual(events[0], expected_caliper_event) statements = self.get_and_clear_xapi_statement_log() expected_xapi_statement = { "actor": self.get_compair_xapi_actor(self.user), "verb": { 'id': 'https://w3id.org/xapi/dod-isd/verbs/archived', 'display': { 'en-US': 'archived' } }, "object": self.expected_xapi_assignment, "context": { 'contextActivities': { 'parent': [self.expected_xapi_course], 'grouping': [] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info(), 'sis_courses': [{ 'id': 'sis_course_id', 'section_ids': ['sis_section_id'] }] } } } self.assertEqual(len(statements), 1) self.assertEqual(statements[0], expected_xapi_statement)
class AnswerCommentAPITests(ComPAIRAPITestCase): """ Tests for answer comment API """ resource = AnswerCommentAPI api = api def setUp(self): super(AnswerCommentAPITests, self).setUp() self.data = AnswerCommentsTestData() self.course = self.data.get_course() self.assignments = self.data.get_assignments() self.answers = self.data.get_answers_by_assignment() self.assignment = self.assignments[0] self.assignment.enable_self_evaluation = True db.session.commit() self.assignment.calculate_grades() self.lti_data = LTITestData() def test_get_single_answer_comment(self): comment = self.data.get_answer_comments_by_assignment( self.assignment)[0] url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid=comment.uuid) draft_comment = self.data.get_answer_comments_by_assignment( self.assignment)[2] draft_url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid=draft_comment.uuid) # test login required rv = self.client.get(url) self.assert401(rv) # test unauthorized user with self.login(self.data.get_unauthorized_instructor().username): rv = self.client.get(url) self.assert403(rv) # test unauthorized user student fetching draft of another student with self.login(self.data.get_extra_student(0).username): rv = self.client.get(draft_url) self.assert403(rv) # test invalid course id with self.login(self.data.get_authorized_instructor().username): invalid_url = self.get_url( course_uuid="999", assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid=comment.uuid) rv = self.client.get(invalid_url) self.assert404(rv) # test invalid answer id invalid_url = self.get_url(course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid="999", answer_comment_uuid=comment.uuid) rv = self.client.get(invalid_url) self.assert404(rv) # test invalid comment id invalid_url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid="999") rv = self.client.get(invalid_url) self.assert404(rv) # test authorized instructor rv = self.client.get(url) self.assert200(rv) self.assertEqual(comment.content, rv.json['content']) # test draft rv = self.client.get(draft_url) self.assert200(rv) self.assertEqual(draft_comment.content, rv.json['content']) self.assertTrue(rv.json['draft']) # test author with self.login(self.data.get_extra_student(0).username): rv = self.client.get(url) self.assert200(rv) self.assertEqual(comment.content, rv.json['content']) # test draft author with self.login(self.data.get_extra_student(1).username): rv = self.client.get(draft_url) self.assert200(rv) self.assertEqual(draft_comment.content, rv.json['content']) self.assertTrue(rv.json['draft']) @mock.patch('compair.tasks.lti_outcomes.update_lti_course_grades.run') @mock.patch('compair.tasks.lti_outcomes.update_lti_assignment_grades.run') def test_edit_answer_comment(self, mocked_update_assignment_grades_run, mocked_update_course_grades_run): comment = self.data.get_answer_comments_by_assignment( self.assignment)[0] url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid=comment.uuid) content = { 'id': comment.uuid, 'content': 'insightful.', 'comment_type': AnswerCommentType.private.value } draft_comment = self.data.get_answer_comments_by_assignment( self.assignment)[2] draft_url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid=draft_comment.uuid) draft_content = { 'id': draft_comment.uuid, 'content': 'insightful.', 'comment_type': AnswerCommentType.private.value, 'draft': True } # test login required rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert401(rv) # test unauthorized user with self.login(self.data.get_unauthorized_instructor().username): rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert403(rv) # test invalid course id with self.login(self.data.get_authorized_instructor().username): invalid_url = self.get_url( course_uuid="999", assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid=comment.uuid) rv = self.client.post(invalid_url, data=json.dumps(content), content_type='application/json') self.assert404(rv) # test invalid answer id invalid_url = self.get_url(course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid="999", answer_comment_uuid=comment.uuid) rv = self.client.post(invalid_url, data=json.dumps(content), content_type='application/json') self.assert404(rv) # test invalid comment id invalid_url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid="999") rv = self.client.post(invalid_url, data=json.dumps(content), content_type='application/json') self.assert404(rv) # test unmatched comment ids invalid = content.copy() invalid['id'] = self.data.get_answer_comments_by_assignment( self.assignment)[1].uuid rv = self.client.post(url, data=json.dumps(invalid), content_type='application/json') self.assert400(rv) self.assertEqual("Reply Not Saved", rv.json['title']) self.assertEqual( "The reply's ID does not match the URL, which is required in order to save the reply.", rv.json['message']) # test empty content empty = content.copy() empty['content'] = '' rv = self.client.post(url, data=json.dumps(empty), content_type='application/json') self.assert400(rv) self.assertEqual("Reply Not Saved", rv.json['title']) self.assertEqual( "Please provide content in the text editor to reply and try saving again.", rv.json['message']) # test empty comment_type empty = content.copy() empty['comment_type'] = '' rv = self.client.post(url, data=json.dumps(empty), content_type='application/json') self.assert400(rv) # test authorized instructor with self.login(self.data.get_authorized_instructor().username): with mail.record_messages() as outbox: rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert200(rv) self.assertEqual(content['content'], rv.json['content']) self.assertEqual(len(outbox), 0) # test author with self.login(self.data.get_extra_student(0).username): with mail.record_messages() as outbox: content['content'] = 'I am the author' rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert200(rv) self.assertEqual(content['content'], rv.json['content']) self.assertFalse(rv.json['draft']) self.assertEqual(len(outbox), 0) # ignored setting draft to True when draft is already False with mail.record_messages() as outbox: content['draft'] = True rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert200(rv) self.assertEqual(content['content'], rv.json['content']) self.assertFalse(rv.json['draft']) self.assertEqual(len(outbox), 0) # test draft author with self.login(self.data.get_extra_student(1).username): with mail.record_messages() as outbox: draft_content['content'] = 'I am the author' rv = self.client.post(draft_url, data=json.dumps(draft_content), content_type='application/json') self.assert200(rv) self.assertEqual(draft_content['content'], rv.json['content']) self.assertTrue(rv.json['draft']) self.assertEqual(len(outbox), 0) # can change draft to False when draft is True with mail.record_messages() as outbox: draft_content['draft'] = False rv = self.client.post(draft_url, data=json.dumps(draft_content), content_type='application/json') self.assert200(rv) self.assertFalse(rv.json['draft']) self.assertEqual(len(outbox), 1) self.assertEqual( outbox[0].subject, "New Answer Feedback in " + self.data.get_course().name) self.assertEqual( outbox[0].recipients, [self.answers[self.assignment.id][0].user.email]) answer = self.answers[self.assignment.id][0] self_evaluation = self.data.create_answer_comment( answer.user, answer, comment_type=AnswerCommentType.self_evaluation, draft=True) self_evaluation_url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=answer.uuid, answer_comment_uuid=self_evaluation.uuid) with self.login(answer.user.username): lti_consumer = self.lti_data.lti_consumer (lti_user_resource_link1, lti_user_resource_link2 ) = self.lti_data.setup_student_user_resource_links( answer.user, self.course, self.assignment) course_grade = CourseGrade.get_user_course_grade( self.course, answer.user).grade assignment_grade = AssignmentGrade.get_user_assignment_grade( self.assignment, answer.user).grade content = { 'id': self_evaluation.uuid, 'content': 'insightful.', 'comment_type': AnswerCommentType.self_evaluation.value, 'draft': True } with mail.record_messages() as outbox: rv = self.client.post(self_evaluation_url, data=json.dumps(content), content_type='application/json') self.assert200(rv) self.assertEqual(content['content'], rv.json['content']) self.assertTrue(rv.json['draft']) self.assertEqual(len(outbox), 0) # grades should not change new_course_grade = CourseGrade.get_user_course_grade( self.course, answer.user).grade new_assignment_grade = AssignmentGrade.get_user_assignment_grade( self.assignment, answer.user).grade self.assertEqual(new_course_grade, course_grade) self.assertEqual(new_assignment_grade, assignment_grade) # can change draft to False when draft is True with mail.record_messages() as outbox: content['draft'] = False rv = self.client.post(self_evaluation_url, data=json.dumps(content), content_type='application/json') self.assert200(rv) self.assertFalse(rv.json['draft']) self.assertEqual(len(outbox), 0) # grades should increase new_course_grade = CourseGrade.get_user_course_grade( self.course, answer.user) new_assignment_grade = AssignmentGrade.get_user_assignment_grade( self.assignment, answer.user) self.assertGreater(new_course_grade.grade, course_grade) self.assertGreater(new_assignment_grade.grade, assignment_grade) mocked_update_assignment_grades_run.assert_called_once_with( lti_consumer.id, [(lti_user_resource_link2.lis_result_sourcedid, new_assignment_grade.id)]) mocked_update_assignment_grades_run.reset_mock() mocked_update_course_grades_run.assert_called_once_with( lti_consumer.id, [(lti_user_resource_link1.lis_result_sourcedid, new_course_grade.id)]) mocked_update_course_grades_run.reset_mock() @mock.patch('compair.tasks.lti_outcomes.update_lti_course_grades.run') @mock.patch('compair.tasks.lti_outcomes.update_lti_assignment_grades.run') def test_delete_answer_comment(self, mocked_update_assignment_grades_run, mocked_update_course_grades_run): comment = self.data.get_answer_comments_by_assignment( self.assignment)[0] url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid=comment.uuid) # test login required rv = self.client.delete(url) self.assert401(rv) # test unauthorized user with self.login(self.data.get_unauthorized_instructor().username): rv = self.client.delete(url) self.assert403(rv) # test invalid comment id with self.login(self.data.get_authorized_instructor().username): invalid_url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid="999") rv = self.client.delete(invalid_url) self.assert404(rv) # test authorized instructor rv = self.client.delete(url) self.assert200(rv) self.assertEqual(comment.uuid, rv.json['id']) # test author with self.login(self.data.get_extra_student(1).username): comment = self.data.get_answer_comments_by_assignment( self.assignment)[1] url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid=comment.uuid) rv = self.client.delete(url) self.assert200(rv) self.assertEqual(comment.uuid, rv.json['id']) # test delete self-evaulation answer = self.answers[self.assignment.id][0] self_evaluation = self.data.create_answer_comment( answer.user, answer, comment_type=AnswerCommentType.self_evaluation) self.assignment.calculate_grade(answer.user) self.course.calculate_grade(answer.user) lti_consumer = self.lti_data.lti_consumer (lti_user_resource_link1, lti_user_resource_link2 ) = self.lti_data.setup_student_user_resource_links( answer.user, self.course, self.assignment) with self.login(self.data.get_authorized_instructor().username): course_grade = CourseGrade.get_user_course_grade( self.course, answer.user).grade assignment_grade = AssignmentGrade.get_user_assignment_grade( self.assignment, answer.user).grade url = self.get_url(course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=answer.uuid, answer_comment_uuid=self_evaluation.uuid) rv = self.client.delete(url) self.assert200(rv) self.assertEqual(self_evaluation.uuid, rv.json['id']) # grades should decrease new_course_grade = CourseGrade.get_user_course_grade( self.course, answer.user) new_assignment_grade = AssignmentGrade.get_user_assignment_grade( self.assignment, answer.user) self.assertLess(new_course_grade.grade, course_grade) self.assertLess(new_assignment_grade.grade, assignment_grade) mocked_update_assignment_grades_run.assert_called_once_with( lti_consumer.id, [(lti_user_resource_link2.lis_result_sourcedid, new_assignment_grade.id)]) mocked_update_assignment_grades_run.reset_mock() mocked_update_course_grades_run.assert_called_once_with( lti_consumer.id, [(lti_user_resource_link1.lis_result_sourcedid, new_course_grade.id)]) mocked_update_course_grades_run.reset_mock()
class AnswerCommentAPITests(ComPAIRAPITestCase): """ Tests for answer comment API """ resource = AnswerCommentAPI api = api def setUp(self): super(AnswerCommentAPITests, self).setUp() self.data = AnswerCommentsTestData() self.course = self.data.get_course() self.assignments = self.data.get_assignments() self.answers = self.data.get_answers_by_assignment() self.assignment = self.assignments[0] self.assignment.enable_self_evaluation = True db.session.commit() self.assignment.calculate_grades() self.lti_data = LTITestData() def test_get_single_answer_comment(self): comment = self.data.get_answer_comments_by_assignment( self.assignment)[0] url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid=comment.uuid) draft_comment = self.data.get_answer_comments_by_assignment( self.assignment)[2] draft_url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid=draft_comment.uuid) # test login required rv = self.client.get(url) self.assert401(rv) # test unauthorized user with self.login(self.data.get_unauthorized_instructor().username): rv = self.client.get(url) self.assert403(rv) # test unauthorized user student fetching draft of another student student = self.data.get_extra_student(0) for user_context in [ \ self.login(student.username), \ self.impersonate(self.data.get_authorized_instructor(), student)]: with user_context: rv = self.client.get(draft_url) self.assert403(rv) # test invalid course id with self.login(self.data.get_authorized_instructor().username): invalid_url = self.get_url( course_uuid="999", assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid=comment.uuid) rv = self.client.get(invalid_url) self.assert404(rv) # test invalid answer id invalid_url = self.get_url(course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid="999", answer_comment_uuid=comment.uuid) rv = self.client.get(invalid_url) self.assert404(rv) # test invalid comment id invalid_url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid="999") rv = self.client.get(invalid_url) self.assert404(rv) # test authorized instructor rv = self.client.get(url) self.assert200(rv) self.assertEqual(comment.content, rv.json['content']) self.assertIn('fullname', rv.json['user']) # test draft rv = self.client.get(draft_url) self.assert200(rv) self.assertEqual(draft_comment.content, rv.json['content']) self.assertTrue(rv.json['draft']) self.assertIn('fullname', rv.json['user']) # test author student = self.data.get_extra_student(0) for user_context in [ self.login(student.username), self.impersonate(self.data.get_authorized_instructor(), student) ]: with user_context: rv = self.client.get(url) self.assert200(rv) self.assertEqual(comment.content, rv.json['content']) self.assertNotIn('fullname', rv.json['user']) # test draft author student = self.data.get_extra_student(1) for user_context in [ self.login(student.username), self.impersonate(self.data.get_authorized_instructor(), student) ]: with user_context: rv = self.client.get(draft_url) self.assert200(rv) self.assertEqual(draft_comment.content, rv.json['content']) self.assertTrue(rv.json['draft']) self.assertNotIn('fullname', rv.json['user']) @mock.patch('compair.tasks.lti_outcomes.update_lti_course_grades.run') @mock.patch('compair.tasks.lti_outcomes.update_lti_assignment_grades.run') def test_edit_answer_comment(self, mocked_update_assignment_grades_run, mocked_update_course_grades_run): comment = self.data.get_answer_comments_by_assignment( self.assignment)[0] url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid=comment.uuid) content = { 'id': comment.uuid, 'content': 'insightful.', 'comment_type': AnswerCommentType.private.value } draft_comment = self.data.get_answer_comments_by_assignment( self.assignment)[2] draft_url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid=draft_comment.uuid) draft_content = { 'id': draft_comment.uuid, 'content': 'insightful.', 'comment_type': AnswerCommentType.private.value, 'draft': True } # test login required rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert401(rv) # test unauthorized user with self.login(self.data.get_unauthorized_instructor().username): rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert403(rv) # test invalid course id with self.login(self.data.get_authorized_instructor().username): invalid_url = self.get_url( course_uuid="999", assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid=comment.uuid) rv = self.client.post(invalid_url, data=json.dumps(content), content_type='application/json') self.assert404(rv) # test invalid answer id invalid_url = self.get_url(course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid="999", answer_comment_uuid=comment.uuid) rv = self.client.post(invalid_url, data=json.dumps(content), content_type='application/json') self.assert404(rv) # test invalid comment id invalid_url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid="999") rv = self.client.post(invalid_url, data=json.dumps(content), content_type='application/json') self.assert404(rv) # test unmatched comment ids invalid = content.copy() invalid['id'] = self.data.get_answer_comments_by_assignment( self.assignment)[1].uuid rv = self.client.post(url, data=json.dumps(invalid), content_type='application/json') self.assert400(rv) self.assertEqual("Feedback Not Saved", rv.json['title']) self.assertEqual( "The feedback's ID does not match the URL, which is required in order to save the feedback.", rv.json['message']) # test empty content empty = content.copy() empty['content'] = '' rv = self.client.post(url, data=json.dumps(empty), content_type='application/json') self.assert400(rv) self.assertEqual("Feedback Not Saved", rv.json['title']) self.assertEqual( "Please provide content in the text editor and try saving again.", rv.json['message']) # test empty comment_type empty = content.copy() empty['comment_type'] = '' rv = self.client.post(url, data=json.dumps(empty), content_type='application/json') self.assert400(rv) # test authorized instructor with self.login(self.data.get_authorized_instructor().username): with mail.record_messages() as outbox: rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert200(rv) self.assertEqual(content['content'], rv.json['content']) self.assertIn('fullname', rv.json['user']) self.assertEqual(len(outbox), 0) # test author with self.login(self.data.get_extra_student(0).username): # test student can not change comment to self-eval / eval invalid = content.copy() invalid['comment_type'] = AnswerCommentType.self_evaluation.value rv = self.client.post(url, data=json.dumps(invalid), content_type='application/json') self.assert400(rv) self.assertEqual("Feedback Not Saved", rv.json['title']) self.assertEqual( "Feedback type cannot be changed. Please contact support for assistance.", rv.json['message']) invalid = content.copy() invalid['comment_type'] = AnswerCommentType.evaluation.value rv = self.client.post(url, data=json.dumps(invalid), content_type='application/json') self.assert400(rv) self.assertEqual("Feedback Not Saved", rv.json['title']) self.assertEqual( "Feedback type cannot be changed. Please contact support for assistance.", rv.json['message']) with mail.record_messages() as outbox: content['content'] = 'I am the author' rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert200(rv) self.assertEqual(content['content'], rv.json['content']) self.assertFalse(rv.json['draft']) self.assertNotIn('fullname', rv.json['user']) self.assertEqual(len(outbox), 0) # ignored setting draft to True when draft is already False with mail.record_messages() as outbox: content['draft'] = True rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert200(rv) self.assertEqual(content['content'], rv.json['content']) self.assertFalse(rv.json['draft']) self.assertNotIn('fullname', rv.json['user']) self.assertEqual(len(outbox), 0) # test author with impersonation student = self.data.get_extra_student(0) with self.impersonate(self.data.get_authorized_instructor(), student): content['content'] = 'I am the author' rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert403(rv) self.assertTrue(rv.json['disabled_by_impersonation']) content['draft'] = True rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert403(rv) self.assertTrue(rv.json['disabled_by_impersonation']) # test draft author with self.login(self.data.get_extra_student(1).username): with mail.record_messages() as outbox: draft_content['content'] = 'I am the author' rv = self.client.post(draft_url, data=json.dumps(draft_content), content_type='application/json') self.assert200(rv) self.assertEqual(draft_content['content'], rv.json['content']) self.assertTrue(rv.json['draft']) self.assertNotIn('fullname', rv.json['user']) self.assertEqual(len(outbox), 0) # can change draft to False when draft is True with mail.record_messages() as outbox: draft_content['draft'] = False rv = self.client.post(draft_url, data=json.dumps(draft_content), content_type='application/json') self.assert200(rv) self.assertFalse(rv.json['draft']) self.assertNotIn('fullname', rv.json['user']) self.assertEqual(len(outbox), 1) self.assertEqual( outbox[0].subject, "New Answer Feedback in " + self.data.get_course().name) self.assertEqual( outbox[0].recipients, [self.answers[self.assignment.id][0].user.email]) # test draft author with impersonation student = self.data.get_extra_student(1) with self.impersonate(self.data.get_authorized_instructor(), student): draft_content['content'] = 'I am the author' rv = self.client.post(draft_url, data=json.dumps(draft_content), content_type='application/json') self.assert403(rv) self.assertTrue(rv.json['disabled_by_impersonation']) # cant change draft to False draft_content['draft'] = False rv = self.client.post(draft_url, data=json.dumps(draft_content), content_type='application/json') self.assert403(rv) self.assertTrue(rv.json['disabled_by_impersonation']) answer = self.answers[self.assignment.id][0] self_evaluation = self.data.create_answer_comment( answer.user, answer, comment_type=AnswerCommentType.self_evaluation, draft=True) self_evaluation_url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=answer.uuid, answer_comment_uuid=self_evaluation.uuid) with self.login(answer.user.username): lti_consumer = self.lti_data.lti_consumer (lti_user_resource_link1, lti_user_resource_link2 ) = self.lti_data.setup_student_user_resource_links( answer.user, self.course, self.assignment) course_grade = CourseGrade.get_user_course_grade( self.course, answer.user).grade assignment_grade = AssignmentGrade.get_user_assignment_grade( self.assignment, answer.user).grade content = { 'id': self_evaluation.uuid, 'content': 'insightful.', 'comment_type': AnswerCommentType.self_evaluation.value, 'draft': True } # test student can not submit self-eval after self-eval grace period orig_answer_end = self.assignment.answer_end self.assignment.answer_end = datetime.datetime.utcnow( ) - datetime.timedelta(hours=12) self.assignment.self_eval_start = datetime.datetime.utcnow( ) - datetime.timedelta(hours=1) self.assignment.self_eval_end = datetime.datetime.utcnow( ) - datetime.timedelta(minutes=10) db.session.add(self.assignment) db.session.commit() rv = self.client.post(self_evaluation_url, data=json.dumps(content), content_type='application/json') self.assert403(rv) self.assertEqual("Self-Evaluation Not Saved", rv.json['title']) self.assertEqual( "Sorry, the self-evaluation deadline has passed and therefore cannot be submitted.", rv.json['message']) self.assignment.answer_end = orig_answer_end self.assignment.self_eval_start = None self.assignment.self_eval_end = None with mail.record_messages() as outbox: self.assignment.answer_end = datetime.datetime.utcnow( ) - datetime.timedelta(hours=12) rv = self.client.post(self_evaluation_url, data=json.dumps(content), content_type='application/json') self.assert200(rv) self.assertEqual(content['content'], rv.json['content']) self.assertTrue(rv.json['draft']) self.assertNotIn('fullname', rv.json['user']) self.assertEqual(len(outbox), 0) # grades should not change new_course_grade = CourseGrade.get_user_course_grade( self.course, answer.user).grade new_assignment_grade = AssignmentGrade.get_user_assignment_grade( self.assignment, answer.user).grade self.assertEqual(new_course_grade, course_grade) self.assertEqual(new_assignment_grade, assignment_grade) # can change draft to False when draft is True with mail.record_messages() as outbox: content['draft'] = False rv = self.client.post(self_evaluation_url, data=json.dumps(content), content_type='application/json') self.assert200(rv) self.assertFalse(rv.json['draft']) self.assertNotIn('fullname', rv.json['user']) self.assertEqual(len(outbox), 0) # grades should increase new_course_grade = CourseGrade.get_user_course_grade( self.course, answer.user) new_assignment_grade = AssignmentGrade.get_user_assignment_grade( self.assignment, answer.user) self.assertGreater(new_course_grade.grade, course_grade) self.assertGreater(new_assignment_grade.grade, assignment_grade) mocked_update_assignment_grades_run.assert_called_once_with( lti_consumer.id, [[ lti_user_resource_link2.lis_result_sourcedid, new_assignment_grade.id ]]) mocked_update_assignment_grades_run.reset_mock() mocked_update_course_grades_run.assert_called_once_with( lti_consumer.id, [[ lti_user_resource_link1.lis_result_sourcedid, new_course_grade.id ]]) mocked_update_course_grades_run.reset_mock() # test self-evaluation with impersonation answers = self.answers[self.assignment.id] for answer in [ a for a in answers if a.user.system_role == SystemRole.student ]: self_evaluation = self.data.create_answer_comment( answer.user, answer, comment_type=AnswerCommentType.self_evaluation, draft=True) self_evaluation_url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=answer.uuid, answer_comment_uuid=self_evaluation.uuid) with self.impersonate(self.data.get_authorized_instructor(), answer.user): content = { 'id': self_evaluation.uuid, 'content': 'insightful.', 'comment_type': AnswerCommentType.self_evaluation.value, 'draft': True } rv = self.client.post(self_evaluation_url, data=json.dumps(content), content_type='application/json') self.assert403(rv) self.assertTrue(rv.json['disabled_by_impersonation']) # attempt to change draft to False content['draft'] = False rv = self.client.post(self_evaluation_url, data=json.dumps(content), content_type='application/json') self.assert403(rv) self.assertTrue(rv.json['disabled_by_impersonation']) @mock.patch('compair.tasks.lti_outcomes.update_lti_course_grades.run') @mock.patch('compair.tasks.lti_outcomes.update_lti_assignment_grades.run') def test_delete_answer_comment(self, mocked_update_assignment_grades_run, mocked_update_course_grades_run): comment = self.data.get_answer_comments_by_assignment( self.assignment)[0] url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid=comment.uuid) # test login required rv = self.client.delete(url) self.assert401(rv) # test unauthorized user with self.login(self.data.get_unauthorized_instructor().username): rv = self.client.delete(url) self.assert403(rv) # test invalid comment id with self.login(self.data.get_authorized_instructor().username): invalid_url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid="999") rv = self.client.delete(invalid_url) self.assert404(rv) # test authorized instructor rv = self.client.delete(url) self.assert200(rv) self.assertEqual(comment.uuid, rv.json['id']) # test author with impersonation student = self.data.get_extra_student(1) with self.impersonate(self.data.get_authorized_instructor(), student): url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid=comment.uuid) rv = self.client.delete(url) self.assert403(rv) self.assertTrue(rv.json['disabled_by_impersonation']) # test author with self.login(self.data.get_extra_student(1).username): comment = self.data.get_answer_comments_by_assignment( self.assignment)[1] url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid=comment.uuid) rv = self.client.delete(url) self.assert200(rv) self.assertEqual(comment.uuid, rv.json['id']) # test delete self-evaulation with impersonation answers = self.answers[self.assignment.id] for answer in [ a for a in answers if a.user.system_role == SystemRole.student ]: self_evaluation = self.data.create_answer_comment( answer.user, answer, comment_type=AnswerCommentType.self_evaluation, draft=True) with self.impersonate(self.data.get_authorized_instructor(), answer.user): url = self.get_url(course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=answer.uuid, answer_comment_uuid=self_evaluation.uuid) rv = self.client.delete(url) self.assert403(rv) self.assertTrue(rv.json['disabled_by_impersonation']) # test delete self-evaulation answer = self.answers[self.assignment.id][0] self_evaluation = self.data.create_answer_comment( answer.user, answer, comment_type=AnswerCommentType.self_evaluation) self.assignment.calculate_grade(answer.user) self.course.calculate_grade(answer.user) lti_consumer = self.lti_data.lti_consumer (lti_user_resource_link1, lti_user_resource_link2 ) = self.lti_data.setup_student_user_resource_links( answer.user, self.course, self.assignment) with self.login(self.data.get_authorized_instructor().username): course_grade = CourseGrade.get_user_course_grade( self.course, answer.user).grade assignment_grade = AssignmentGrade.get_user_assignment_grade( self.assignment, answer.user).grade url = self.get_url(course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=answer.uuid, answer_comment_uuid=self_evaluation.uuid) rv = self.client.delete(url) self.assert200(rv) self.assertEqual(self_evaluation.uuid, rv.json['id']) # grades should decrease new_course_grade = CourseGrade.get_user_course_grade( self.course, answer.user) new_assignment_grade = AssignmentGrade.get_user_assignment_grade( self.assignment, answer.user) self.assertLess(new_course_grade.grade, course_grade) self.assertLess(new_assignment_grade.grade, assignment_grade) mocked_update_assignment_grades_run.assert_called_once_with( lti_consumer.id, [[ lti_user_resource_link2.lis_result_sourcedid, new_assignment_grade.id ]]) mocked_update_assignment_grades_run.reset_mock() mocked_update_course_grades_run.assert_called_once_with( lti_consumer.id, [[ lti_user_resource_link1.lis_result_sourcedid, new_course_grade.id ]]) mocked_update_course_grades_run.reset_mock()
class FileLearningRecordTests(ComPAIRLearningRecordTestCase): def setUp(self): super(ComPAIRLearningRecordTestCase, self).setUp() self.data = SimpleAssignmentTestData() self.lti_data = LTITestData() self.user = self.data.authorized_student self.setup_session_data(self.user) self.course = self.data.main_course self.lti_context = self.lti_data.create_context( self.lti_data.lti_consumer, compair_course_id=self.course.id, lis_course_offering_sourcedid="sis_course_id", lis_course_section_sourcedid="sis_section_id", ) self.assignment = self.data.assignments[0] self.answer = AnswerFactory( assignment=self.assignment, user=self.user ) db.session.commit() self.expected_caliper_course = { '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", }] } } self.expected_caliper_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': self.expected_caliper_course, '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' }], } self.expected_caliper_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': self.expected_caliper_assignment, } self.expected_caliper_answer_attempt = { 'assignable': self.expected_caliper_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' } self.expected_caliper_answer = { 'attempt': self.expected_caliper_answer_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': 8, } } self.expected_xapi_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' } self.expected_xapi_sis_course = { 'id': 'https://localhost:8888/course/'+self.lti_context.lis_course_offering_sourcedid, 'objectType': 'Activity' } self.expected_xapi_sis_section = { 'id': 'https://localhost:8888/course/'+self.lti_context.lis_course_offering_sourcedid+'/section/'+self.lti_context.lis_course_section_sourcedid, 'objectType': 'Activity' } self.expected_xapi_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' } self.expected_xapi_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' } self.expected_xapi_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' } def test_on_get_file(self): # not report or attachment on_get_file.send( current_app._get_current_object(), event_name=on_get_file.name, user=self.user, file_type="none", file_name="some_file" ) events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 0) statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 0) # test report on_get_file.send( current_app._get_current_object(), event_name=on_get_file.name, user=self.user, file_type="report", file_name="some_report.csv" ) expected_caliper_object = { "id": 'https://localhost:8888/app/report/some_report.csv', "type": "Document", "name": "some_report.csv", "mediaType": "text/csv" } expected_caliper_event = { 'action': 'Viewed', 'actor': self.get_compair_caliper_actor(self.user), 'object': expected_caliper_object, 'session': self.get_caliper_session(self.get_compair_caliper_actor(self.user)), 'type': 'ViewEvent' } events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 1) self.assertEqual(events[0], expected_caliper_event) expected_xapi_object = { 'id': 'https://localhost:8888/app/report/some_report.csv', 'definition': { 'type': 'http://activitystrea.ms/schema/1.0/file', 'name': {'en-US': 'some_report.csv'}, 'extensions': { 'http://id.tincanapi.com/extension/mime-type': "text/csv" } }, 'objectType': 'Activity' } expected_xapi_verb = { 'id': 'http://id.tincanapi.com/verb/downloaded', 'display': {'en-US': 'downloaded'} } expected_xapi_context = { 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } } expected_xapi_statement = { "actor": self.get_compair_xapi_actor(self.user), "verb": expected_xapi_verb, "object": expected_xapi_object, "context": expected_xapi_context } statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 1) self.assertEqual(statements[0], expected_xapi_statement) # test attachment without file record on_get_file.send( current_app._get_current_object(), event_name=on_get_file.name, user=self.user, file_type="attachment", file_name="some_file" ) events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 0) statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 0) # test attachment file record (not linked to assignments or answers) file_record = self.data.create_file(self.user) on_get_file.send( current_app._get_current_object(), event_name=on_get_file.name, user=self.user, file_type="attachment", file_name=file_record.name ) events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 0) statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 0) # test attachment file record (assignment) self.assignment.file_id = file_record.id db.session.commit() on_get_file.send( current_app._get_current_object(), event_name=on_get_file.name, user=self.user, file_type="attachment", file_name=file_record.name ) expected_caliper_object = { "id": 'https://localhost:8888/app/attachment/'+file_record.name, "type": "Document", "name": file_record.alias, "mediaType": 'application/pdf', "isPartOf": self.expected_caliper_assignment, "dateCreated": file_record.created.replace(tzinfo=pytz.utc).isoformat(), "dateModified": file_record.modified.replace(tzinfo=pytz.utc).isoformat() } self.expected_caliper_assignment['dateModified'] = self.assignment.modified.replace(tzinfo=pytz.utc).isoformat() expected_caliper_event['object'] = expected_caliper_object expected_caliper_event['membership'] = self.get_caliper_membership(self.course, self.user, self.lti_context) events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 1) self.assertEqual(events[0], expected_caliper_event) expected_xapi_object = { 'id': 'https://localhost:8888/app/attachment/'+file_record.name, 'definition': { 'type': 'http://activitystrea.ms/schema/1.0/file', 'name': {'en-US': file_record.alias}, 'extensions': { 'http://id.tincanapi.com/extension/mime-type': 'application/pdf' } }, 'objectType': 'Activity' } expected_xapi_context = { 'contextActivities': { 'parent': [self.expected_xapi_assignment], 'grouping': [self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } } expected_xapi_statement['object'] = expected_xapi_object expected_xapi_statement['context'] = expected_xapi_context statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 1) self.assertEqual(statements[0], expected_xapi_statement) # test attachment file record (answer) self.assignment.file_id = None self.answer.file_id = file_record.id db.session.commit() on_get_file.send( current_app._get_current_object(), event_name=on_get_file.name, user=self.user, file_type="attachment", file_name=file_record.name ) self.expected_caliper_assignment_question['dateModified'] = self.assignment.modified.replace(tzinfo=pytz.utc).isoformat() self.expected_caliper_assignment['dateModified'] = self.assignment.modified.replace(tzinfo=pytz.utc).isoformat() self.expected_caliper_answer['dateModified'] = self.answer.modified.replace(tzinfo=pytz.utc).isoformat() expected_caliper_object["isPartOf"] = self.expected_caliper_answer events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 1) self.assertEqual(events[0], expected_caliper_event) expected_xapi_context = { 'contextActivities': { 'parent': [self.expected_xapi_answer], 'grouping': [self.expected_xapi_assignment_question, self.expected_xapi_assignment, self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } } expected_xapi_statement['context'] = expected_xapi_context statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 1) self.assertEqual(statements[0], expected_xapi_statement) def test_on_attach_file(self): file_record = self.data.create_file(self.user) self.assignment.file_id = file_record.id db.session.commit() # attache to assignment on_attach_file.send( current_app._get_current_object(), event_name=on_attach_file.name, user=self.user, file=file_record, ) expected_caliper_object = { "id": 'https://localhost:8888/app/attachment/'+file_record.name, "type": "Document", "name": file_record.alias, "mediaType": 'application/pdf', "isPartOf": self.expected_caliper_assignment, "dateCreated": file_record.created.replace(tzinfo=pytz.utc).isoformat(), "dateModified": file_record.modified.replace(tzinfo=pytz.utc).isoformat() } self.expected_caliper_assignment['dateModified'] = self.assignment.modified.replace(tzinfo=pytz.utc).isoformat() expected_caliper_event = { 'action': 'Attached', 'actor': self.get_compair_caliper_actor(self.user), 'membership': self.get_caliper_membership(self.course, self.user, self.lti_context), 'object': expected_caliper_object, 'session': self.get_caliper_session(self.get_compair_caliper_actor(self.user)), 'type': 'Event' } events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 1) self.assertEqual(events[0], expected_caliper_event) expected_xapi_verb = { 'id': 'http://activitystrea.ms/schema/1.0/attach', 'display': {'en-US': 'attached'} } expected_xapi_object = { 'id': 'https://localhost:8888/app/attachment/'+file_record.name, 'definition': { 'type': 'http://activitystrea.ms/schema/1.0/file', 'name': {'en-US': file_record.alias}, 'extensions': { 'http://id.tincanapi.com/extension/mime-type': 'application/pdf' } }, 'objectType': 'Activity' } expected_xapi_context = { 'contextActivities': { 'parent': [self.expected_xapi_assignment], 'grouping': [self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } } expected_xapi_statement = { "actor": self.get_compair_xapi_actor(self.user), "verb": expected_xapi_verb, "object": expected_xapi_object, "context": expected_xapi_context } statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 1) self.assertEqual(statements[0], expected_xapi_statement) # attach to answer self.assignment.file_id = None self.answer.file_id = file_record.id db.session.commit() on_attach_file.send( current_app._get_current_object(), event_name=on_attach_file.name, user=self.user, file=file_record, ) self.expected_caliper_assignment_question['dateModified'] = self.assignment.modified.replace(tzinfo=pytz.utc).isoformat() self.expected_caliper_assignment['dateModified'] = self.assignment.modified.replace(tzinfo=pytz.utc).isoformat() self.expected_caliper_answer['dateModified'] = self.answer.modified.replace(tzinfo=pytz.utc).isoformat() expected_caliper_object["isPartOf"] = self.expected_caliper_answer events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 1) self.assertEqual(events[0], expected_caliper_event) expected_xapi_context = { 'contextActivities': { 'parent': [self.expected_xapi_answer], 'grouping': [self.expected_xapi_assignment_question, self.expected_xapi_assignment, self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } } expected_xapi_statement['context'] = expected_xapi_context statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 1) self.assertEqual(statements[0], expected_xapi_statement) def test_on_detach_file(self): file_record = self.data.create_file(self.user) db.session.commit() # attache to assignment on_detach_file.send( current_app._get_current_object(), event_name=on_detach_file.name, user=self.user, file=file_record, assignment=self.assignment ) expected_caliper_object = { "id": 'https://localhost:8888/app/attachment/'+file_record.name, "type": "Document", "name": file_record.alias, "mediaType": 'application/pdf', "isPartOf": self.expected_caliper_assignment, "dateCreated": file_record.created.replace(tzinfo=pytz.utc).isoformat(), "dateModified": file_record.modified.replace(tzinfo=pytz.utc).isoformat() } expected_caliper_event = { 'action': 'Removed', 'actor': self.get_compair_caliper_actor(self.user), 'membership': self.get_caliper_membership(self.course, self.user, self.lti_context), 'object': expected_caliper_object, 'session': self.get_caliper_session(self.get_compair_caliper_actor(self.user)), 'type': 'Event' } events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 1) self.assertEqual(events[0], expected_caliper_event) expected_xapi_verb = { 'id': 'http://activitystrea.ms/schema/1.0/delete', 'display': {'en-US': 'deleted'} } expected_xapi_object = { 'id': 'https://localhost:8888/app/attachment/'+file_record.name, 'definition': { 'type': 'http://activitystrea.ms/schema/1.0/file', 'name': {'en-US': file_record.alias}, 'extensions': { 'http://id.tincanapi.com/extension/mime-type': 'application/pdf' } }, 'objectType': 'Activity' } expected_xapi_context = { 'contextActivities': { 'parent': [self.expected_xapi_assignment], 'grouping': [self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } } expected_xapi_statement = { "actor": self.get_compair_xapi_actor(self.user), "verb": expected_xapi_verb, "object": expected_xapi_object, "context": expected_xapi_context } statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 1) self.assertEqual(statements[0], expected_xapi_statement) # attach to answer on_detach_file.send( current_app._get_current_object(), event_name=on_detach_file.name, user=self.user, file=file_record, answer=self.answer ) expected_caliper_object["isPartOf"] = self.expected_caliper_answer events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 1) self.assertEqual(events[0], expected_caliper_event) expected_xapi_context = { 'contextActivities': { 'parent': [self.expected_xapi_answer], 'grouping': [self.expected_xapi_assignment_question, self.expected_xapi_assignment, self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } } expected_xapi_statement['context'] = expected_xapi_context statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 1) self.assertEqual(statements[0], expected_xapi_statement)
def test_create_user_lti(self): url = '/api/users' lti_data = LTITestData() # test login required when LTI and oauth_create_user_link are not present expected = UserFactory.stub(system_role=SystemRole.student.value) rv = self.client.post(url, data=json.dumps(expected.__dict__), content_type='application/json') self.assert401(rv) # Instructor - no context with self.lti_launch(lti_data.get_consumer(), lti_data.generate_resource_link_id(), user_id=lti_data.generate_user_id(), context_id=None, roles="Instructor") as lti_response: self.assert200(lti_response) # test create instructor via lti session expected = UserFactory.stub(system_role=None) rv = self.client.post(url, data=json.dumps(expected.__dict__), content_type="application/json") self.assert200(rv) self.assertEqual(expected.displayname, rv.json['displayname']) user = User.query.filter_by(uuid=rv.json['id']).one() self.assertEqual(SystemRole.instructor, user.system_role) self.assertIsNotNone(user.password) self.assertEqual(expected.username, user.username) # verify not enrolled in any course self.assertEqual(len(user.user_courses), 0) # Instructor - with context not linked with self.lti_launch(lti_data.get_consumer(), lti_data.generate_resource_link_id(), user_id=lti_data.generate_user_id(), context_id=lti_data.generate_context_id(), roles="Instructor") as lti_response: self.assert200(lti_response) # test create instructor via lti session expected = UserFactory.stub(system_role=None) rv = self.client.post(url, data=json.dumps(expected.__dict__), content_type="application/json") self.assert200(rv) self.assertEqual(expected.displayname, rv.json['displayname']) user = User.query.filter_by(uuid=rv.json['id']).one() self.assertEqual(SystemRole.instructor, user.system_role) self.assertIsNotNone(user.password) self.assertEqual(expected.username, user.username) # verify not enrolled in any course self.assertEqual(len(user.user_courses), 0) # Instructor - with context linked with self.lti_launch(lti_data.get_consumer(), lti_data.generate_resource_link_id(), user_id=lti_data.generate_user_id(), context_id=lti_data.generate_context_id(), roles="Instructor") as lti_response: self.assert200(lti_response) lti_context = LTIContext.query.all()[-1] course = self.data.create_course() lti_context.compair_course_id = course.id db.session.commit() # test create instructor via lti session expected = UserFactory.stub(system_role=None) rv = self.client.post(url, data=json.dumps(expected.__dict__), content_type="application/json") self.assert200(rv) self.assertEqual(expected.displayname, rv.json['displayname']) user = User.query.filter_by(uuid=rv.json['id']).one() self.assertEqual(SystemRole.instructor, user.system_role) self.assertIsNotNone(user.password) self.assertEqual(expected.username, user.username) # verify enrolled in course self.assertEqual(len(user.user_courses), 1) self.assertEqual(user.user_courses[0].course_id, course.id) # test create student via lti session with self.lti_launch(lti_data.get_consumer(), lti_data.generate_resource_link_id(), user_id=lti_data.generate_user_id(), context_id=lti_data.generate_context_id(), roles="Student") as lti_response: self.assert200(lti_response) expected = UserFactory.stub(system_role=None) rv = self.client.post(url, data=json.dumps(expected.__dict__), content_type="application/json") self.assert200(rv) self.assertEqual(expected.displayname, rv.json['displayname']) user = User.query.filter_by(uuid=rv.json['id']).one() self.assertEqual(SystemRole.student, user.system_role) self.assertIsNotNone(user.password) self.assertEqual(expected.username, user.username) # test create teaching assistant (student role) via lti session with self.lti_launch(lti_data.get_consumer(), lti_data.generate_resource_link_id(), user_id=lti_data.generate_user_id(), context_id=lti_data.generate_context_id(), roles="TeachingAssistant") as lti_response: self.assert200(lti_response) expected = UserFactory.stub(system_role=None) rv = self.client.post(url, data=json.dumps(expected.__dict__), content_type="application/json") self.assert200(rv) self.assertEqual(expected.displayname, rv.json['displayname']) user = User.query.filter_by(uuid=rv.json['id']).one() self.assertEqual(SystemRole.student, user.system_role) self.assertIsNotNone(user.password) self.assertEqual(expected.username, user.username)
class AnswerCommentListAPITests(ComPAIRAPITestCase): """ Tests for answer comment list API """ resource = AnswerCommentListAPI api = api def setUp(self): super(AnswerCommentListAPITests, self).setUp() self.data = AnswerCommentsTestData() self.course = self.data.get_course() self.assignments = self.data.get_assignments() self.answers = self.data.get_answers_by_assignment() self.assignment = self.assignments[0] self.assignment.enable_self_evaluation = True db.session.commit() self.assignment.calculate_grades() self.lti_data = LTITestData() def test_get_all_answer_comments(self): url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid) # test login required rv = self.client.get(url) self.assert401(rv) # test unauthorized user with self.login(self.data.get_unauthorized_instructor().username): rv = self.client.get(url) self.assert403(rv) with self.login(self.data.get_authorized_instructor().username): # test invalid answer id invalid_url = self.get_url( course_uuid=self.course.id, assignment_uuid=self.assignment.uuid, answer_uuid="999") rv = self.client.get(invalid_url) self.assert404(rv) # test authorized user rv = self.client.get(url) self.assert200(rv) self.assertEqual(1, len(rv.json)) self.assertEqual( self.data.get_non_draft_answer_comments_by_assignment(self.assignment)[1].content, rv.json[0]['content']) self.assertIn( self.data.get_non_draft_answer_comments_by_assignment(self.assignment)[1].user_fullname, rv.json[0]['user']['fullname']) # test non-owner student of answer access comments with self.login(self.data.get_authorized_student().username): rv = self.client.get(url) self.assert200(rv) self.assertEqual(0, len(rv.json)) # test owner student of answer access comments with self.login(self.data.get_extra_student(0).username): rv = self.client.get(url) self.assert200(rv) self.assertEqual(1, len(rv.json)) self.assertNotIn('user_fullname', rv.json[0]) def test_get_list_query_params(self): comment = AnswerCommentsTestData.create_answer_comment( self.data.get_extra_student(0), self.answers[self.assignment.id][0], comment_type=AnswerCommentType.self_evaluation ) draft_comment = AnswerCommentsTestData.create_answer_comment( self.data.get_extra_student(0), self.answers[self.assignment.id][0], comment_type=AnswerCommentType.evaluation, draft=True ) base_params = { 'course_uuid': self.course.uuid, 'assignment_uuid': self.assignment.uuid, } with self.login(self.data.get_authorized_instructor().username): # no answer ids rv = self.client.get(self.get_url(**base_params)) self.assert404(rv) params = dict(base_params, answer_ids=self.answers[self.assignment.id][0].uuid) extra_student2_answer_comment_uuid = self.data.get_answer_comments_by_assignment(self.assignment)[1].uuid rv = self.client.get(self.get_url(**params)) self.assert200(rv) self.assertEqual(2, len(rv.json)) rv = self.client.get(self.get_url(self_evaluation='false', **params)) self.assert200(rv) self.assertEqual(1, len(rv.json)) self.assertEqual(extra_student2_answer_comment_uuid, rv.json[0]['id']) rv = self.client.get(self.get_url(self_evaluation='only', **params)) self.assert200(rv) # self.assertEqual(1, rv.json['total']) self.assertEqual(1, len(rv.json)) self.assertEqual(comment.uuid, rv.json[0]['id']) ids = [extra_student2_answer_comment_uuid, comment.uuid] rv = self.client.get(self.get_url(ids=','.join(str(x) for x in ids), **base_params)) self.assert200(rv) # self.assertEqual(2, rv.json['total']) self.assertEqual(2, len(rv.json)) six.assertCountEqual(self, ids, [c['id'] for c in rv.json]) answer_ids = [answer.uuid for answer in self.answers[self.assignment.id]] params = dict(base_params, answer_ids=','.join(answer_ids)) rv = self.client.get(self.get_url(**params)) self.assert200(rv) self.assertEqual(3, len(rv.json)) rv = self.client.get(self.get_url(self_evaluation='false', **params)) self.assert200(rv) self.assertEqual(2, len(rv.json)) self.assertNotIn(comment.uuid, (c['id'] for c in rv.json)) rv = self.client.get(self.get_url(self_evaluation='only', **params)) self.assert200(rv) self.assertEqual(1, len(rv.json)) self.assertEqual(comment.uuid, rv.json[0]['id']) answer_ids = [answer.uuid for answer in self.answers[self.assignment.id]] params = dict(base_params, answer_ids=','.join(answer_ids), user_ids=self.data.get_extra_student(1).uuid) rv = self.client.get(self.get_url(**params)) self.assert200(rv) self.assertEqual(1, len(rv.json)) # test assignment_id filter rv = self.client.get(self.get_url(**base_params) + '?assignment_id=' + self.assignment.uuid) self.assert200(rv) self.assertEqual(3, len(rv.json)) six.assertCountEqual( self, [comment.uuid] + [c.uuid for c in self.data.get_non_draft_answer_comments_by_assignment(self.assignment)], [c['id'] for c in rv.json]) rv = self.client.get(self.get_url(**base_params) + '?assignment_id=' + self.assignments[1].uuid) self.assert200(rv) self.assertEqual(2, len(rv.json)) six.assertCountEqual( self, [c.uuid for c in self.data.get_non_draft_answer_comments_by_assignment(self.assignments[1])], [c['id'] for c in rv.json]) # test user_ids filter user_ids = ','.join([self.data.get_extra_student(0).uuid]) rv = self.client.get( self.get_url(user_ids=user_ids, **base_params) + '&assignment_id=' + self.assignment.uuid) self.assert200(rv) self.assertEqual(2, len(rv.json)) six.assertCountEqual( self, [comment.uuid, self.data.answer_comments_by_assignment[self.assignment.id][0].uuid], [c['id'] for c in rv.json]) with self.login(self.data.get_extra_student(1).username): answer_ids = [answer.uuid for answer in self.answers[self.assignment.id]] params = dict(base_params, answer_ids=','.join(answer_ids), user_ids=self.data.get_extra_student(1).uuid) rv = self.client.get(self.get_url(**params)) self.assert200(rv) self.assertEqual(1, len(rv.json)) # answer is not from the student but comment is answer_ids = [self.answers[self.assignment.id][1].uuid] params = dict(base_params, answer_ids=','.join(answer_ids), user_ids=self.data.get_extra_student(0).uuid) rv = self.client.get(self.get_url(**params)) self.assert200(rv) self.assertEqual(1, len(rv.json)) self.assertEqual(self.data.get_extra_student(0).uuid, rv.json[0]['user_id']) # test drafts with self.login(self.data.get_extra_student(0).username): params = dict(base_params, user_ids=self.data.get_extra_student(0).uuid) rv = self.client.get(self.get_url(draft='only', **params)) self.assert200(rv) self.assertEqual(1, len(rv.json)) self.assertEqual(draft_comment.uuid, rv.json[0]['id']) rv = self.client.get(self.get_url(draft='false', **params)) self.assert200(rv) self.assertEqual(3, len(rv.json)) rv = self.client.get(self.get_url(draft='true', **params)) self.assert200(rv) self.assertEqual(4, len(rv.json)) self.assertEqual(draft_comment.uuid, rv.json[0]['id']) @mock.patch('compair.tasks.lti_outcomes.update_lti_course_grades.run') @mock.patch('compair.tasks.lti_outcomes.update_lti_assignment_grades.run') def test_create_answer_comment(self, mocked_update_assignment_grades_run, mocked_update_course_grades_run): url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid) content = { 'comment_type': AnswerCommentType.private.value, 'content': 'great answer' } # test login required rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert401(rv) # test unauthorized user with self.login(self.data.get_unauthorized_instructor().username): rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert403(rv) # test invalid course id with self.login(self.data.get_authorized_instructor().username): invalid_url = self.get_url( course_uuid="999", assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid) rv = self.client.post(invalid_url, data=json.dumps(content), content_type='application/json') self.assert404(rv) # test invalid assignment id invalid_url = self.get_url( course_uuid=self.course.uuid, assignment_uuid="999", answer_uuid=self.answers[self.assignment.id][0].uuid) rv = self.client.post(invalid_url, data=json.dumps(content), content_type='application/json') self.assert404(rv) # test invalid answer id invalid_url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid="999") rv = self.client.post(invalid_url, data=json.dumps(content), content_type='application/json') self.assert404(rv) # test empty content empty = content.copy() empty['content'] = '' rv = self.client.post(url, data=json.dumps(empty), content_type='application/json') self.assert400(rv) # test empty comment type empty = content.copy() empty['comment_type'] = '' rv = self.client.post(url, data=json.dumps(empty), content_type='application/json') self.assert400(rv) # test authorized user rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert200(rv) self.assertEqual(content['content'], rv.json['content']) self.assertFalse(rv.json['draft']) # test authorized user draft draft_content = content.copy() draft_content['draft'] = True rv = self.client.post(url, data=json.dumps(draft_content), content_type='application/json') self.assert200(rv) self.assertEqual(content['content'], rv.json['content']) self.assertTrue(rv.json['draft']) # test authorized user draft - empty content empty = draft_content.copy() empty['content'] = None rv = self.client.post(url, data=json.dumps(empty), content_type='application/json') self.assert200(rv) self.assertEqual(empty['content'], rv.json['content']) self.assertTrue(rv.json['draft']) with self.login(self.data.get_authorized_student().username): lti_consumer = self.lti_data.lti_consumer (lti_user_resource_link1, lti_user_resource_link2) = self.lti_data.setup_student_user_resource_links( self.data.get_authorized_student(), self.course, self.assignment) course_grade = CourseGrade.get_user_course_grade(self.course, self.data.get_authorized_student()).grade assignment_grade = AssignmentGrade.get_user_assignment_grade(self.assignment, self.data.get_authorized_student()).grade content = { 'comment_type': AnswerCommentType.self_evaluation.value, 'content': 'great answer' } rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert200(rv) # grades should increase new_course_grade = CourseGrade.get_user_course_grade(self.course, self.data.get_authorized_student()) new_assignment_grade = AssignmentGrade.get_user_assignment_grade(self.assignment, self.data.get_authorized_student()) self.assertGreater(new_course_grade.grade, course_grade) self.assertGreater(new_assignment_grade.grade, assignment_grade) mocked_update_assignment_grades_run.assert_called_once_with( lti_consumer.id, [(lti_user_resource_link2.lis_result_sourcedid, new_assignment_grade.id)] ) mocked_update_assignment_grades_run.reset_mock() mocked_update_course_grades_run.assert_called_once_with( lti_consumer.id, [(lti_user_resource_link1.lis_result_sourcedid, new_course_grade.id)] ) mocked_update_assignment_grades_run.reset_mock()
def setUp(self): super(ComPAIRLearningRecordTestCase, self).setUp() self.data = ComparisonTestData() self.lti_data = LTITestData() self.user = self.data.authorized_student self.setup_session_data(self.user) self.course = self.data.main_course self.lti_context = self.lti_data.create_context( self.lti_data.lti_consumer, compair_course_id=self.course.id, lis_course_offering_sourcedid="sis_course_id", lis_course_section_sourcedid="sis_section_id", ) self.assignment = self.data.assignments[0] self.criterion = self.assignment.criteria[0] self.answer1 = self.data.answers[0] self.answer2 = self.data.answers[1] self.example_comparison = ComparisonFactory( assignment=self.assignment, user=self.user, answer1_id=self.answer1.id, answer2_id=self.answer2.id, winner=None, completed=False ) self.example_comparison_criterion = ComparisonCriterionFactory( comparison=self.example_comparison, criterion=self.criterion, winner=WinningAnswer.answer1, ) self.comparison = ComparisonFactory( assignment=self.assignment, user=self.user, answer1_id=self.answer1.id, answer2_id=self.answer2.id, winner=None, completed=False ) self.comparison_criterion = ComparisonCriterionFactory( comparison=self.comparison, criterion=self.criterion, winner=WinningAnswer.answer1, ) db.session.commit() self.expected_caliper_course = { '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", }] } } self.expected_caliper_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': self.expected_caliper_course, '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' }], } self.expected_caliper_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': self.expected_caliper_assignment, } self.expected_caliper_answer1_attempt = { 'assignable': self.expected_caliper_assignment_question, 'assignee': self.get_compair_caliper_actor(self.answer1.user), 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/question/attempt/"+self.answer1.attempt_uuid, 'duration': "PT05M00S", 'startedAtTime': self.answer1.attempt_started.replace(tzinfo=pytz.utc).isoformat(), 'endedAtTime': self.answer1.attempt_ended.replace(tzinfo=pytz.utc).isoformat(), 'type': 'Attempt' } self.expected_caliper_answer1 = { 'attempt': self.expected_caliper_answer1_attempt, 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/answer/"+self.answer1.uuid, 'type': 'Response', 'dateCreated': self.answer1.created.replace(tzinfo=pytz.utc).isoformat(), 'dateModified': self.answer1.modified.replace(tzinfo=pytz.utc).isoformat(), 'extensions': { 'characterCount': len(self.answer1.content), 'content': self.answer1.content, 'isDraft': False, 'wordCount': 8, '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 }, } }, } } self.expected_caliper_answer2_attempt = { 'assignable': self.expected_caliper_assignment_question, 'assignee': self.get_compair_caliper_actor(self.answer2.user), 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/question/attempt/"+self.answer2.attempt_uuid, 'duration': "PT05M00S", 'startedAtTime': self.answer2.attempt_started.replace(tzinfo=pytz.utc).isoformat(), 'endedAtTime': self.answer2.attempt_ended.replace(tzinfo=pytz.utc).isoformat(), 'type': 'Attempt' } self.expected_caliper_answer2 = { 'attempt': self.expected_caliper_answer2_attempt, 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/answer/"+self.answer2.uuid, 'type': 'Response', 'dateCreated': self.answer2.created.replace(tzinfo=pytz.utc).isoformat(), 'dateModified': self.answer2.modified.replace(tzinfo=pytz.utc).isoformat(), 'extensions': { 'characterCount': len(self.answer2.content), 'content': self.answer2.content, 'isDraft': False, 'wordCount': 8, '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 }, } }, } } self.expected_caliper_comparison_question = { 'name': "Assignment comparison #1", 'type': 'AssessmentItem', 'dateCreated': self.assignment.created.replace(tzinfo=pytz.utc).isoformat(), 'dateModified': self.assignment.modified.replace(tzinfo=pytz.utc).isoformat(), 'dateToStartOn': self.assignment.answer_end.replace(tzinfo=pytz.utc).isoformat(), 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/comparison/question/1", 'isPartOf': self.expected_caliper_assignment, } self.expected_xapi_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' } self.expected_xapi_sis_course = { 'id': 'https://localhost:8888/course/'+self.lti_context.lis_course_offering_sourcedid, 'objectType': 'Activity' } self.expected_xapi_sis_section = { 'id': 'https://localhost:8888/course/'+self.lti_context.lis_course_offering_sourcedid+'/section/'+self.lti_context.lis_course_section_sourcedid, 'objectType': 'Activity' } self.expected_xapi_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' } self.expected_xapi_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' } self.expected_xapi_answer1_attempt = { 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/question/attempt/"+self.answer1.attempt_uuid, 'definition': { 'type': 'http://adlnet.gov/expapi/activities/attempt', 'extensions': { 'http://id.tincanapi.com/extension/attempt': { 'duration': "PT05M00S", 'startedAtTime': self.answer1.attempt_started.replace(tzinfo=pytz.utc).isoformat(), 'endedAtTime': self.answer1.attempt_ended.replace(tzinfo=pytz.utc).isoformat(), } } }, 'objectType': 'Activity' } self.expected_xapi_answer1 = { 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/answer/"+self.answer1.uuid, 'definition': { 'type': 'http://id.tincanapi.com/activitytype/solution', 'extensions': { 'http://id.tincanapi.com/extension/isDraft': False } }, 'objectType': 'Activity' } self.expected_xapi_answer2_attempt = { 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/question/attempt/"+self.answer2.attempt_uuid, 'definition': { 'type': 'http://adlnet.gov/expapi/activities/attempt', 'extensions': { 'http://id.tincanapi.com/extension/attempt': { 'duration': "PT05M00S", 'startedAtTime': self.answer2.attempt_started.replace(tzinfo=pytz.utc).isoformat(), 'endedAtTime': self.answer2.attempt_ended.replace(tzinfo=pytz.utc).isoformat(), } } }, 'objectType': 'Activity' } self.expected_xapi_answer2 = { 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/answer/"+self.answer2.uuid, 'definition': { 'type': 'http://id.tincanapi.com/activitytype/solution', 'extensions': { 'http://id.tincanapi.com/extension/isDraft': False } }, 'objectType': 'Activity' } self.expected_xapi_comparison_question = { 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/comparison/question/1", 'definition': { 'type': 'http://adlnet.gov/expapi/activities/question', 'name': {'en-US': "Assignment comparison #1"} }, 'objectType': 'Activity' }
def setUp(self): super(AnswersAPITests, self).setUp() self.fixtures = TestFixture().add_course(num_students=30, num_groups=2, with_draft_student=True) self.base_url = self._build_url(self.fixtures.course.uuid, self.fixtures.assignment.uuid) self.lti_data = LTITestData()
class AnswerLearningRecordTests(ComPAIRLearningRecordTestCase): def setUp(self): super(ComPAIRLearningRecordTestCase, self).setUp() self.data = SimpleAnswersTestData() self.lti_data = LTITestData() self.user = self.data.authorized_student self.setup_session_data(self.user) self.course = self.data.main_course self.lti_context = self.lti_data.create_context( self.lti_data.lti_consumer, compair_course_id=self.course.id, lis_course_offering_sourcedid="sis_course_id", lis_course_section_sourcedid="sis_section_id", ) self.assignment = self.data.assignments[0] self.criterion = self.assignment.criteria[0] self.answer = self.data.create_answer(self.assignment, self.user) self.expected_caliper_course = { '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", }] } } self.expected_caliper_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': self.expected_caliper_course, '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' }], } self.expected_caliper_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': self.expected_caliper_assignment, } self.expected_caliper_answer_attempt = { 'assignable': self.expected_caliper_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' } self.expected_caliper_answer = { 'attempt': self.expected_caliper_answer_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': 8, '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 }, } }, } } self.expected_xapi_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' } self.expected_xapi_sis_course = { 'id': 'https://localhost:8888/course/' + self.lti_context.lis_course_offering_sourcedid, 'objectType': 'Activity' } self.expected_xapi_sis_section = { 'id': 'https://localhost:8888/course/' + self.lti_context.lis_course_offering_sourcedid + '/section/' + self.lti_context.lis_course_section_sourcedid, 'objectType': 'Activity' } self.expected_xapi_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' } self.expected_xapi_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' } self.expected_xapi_answer_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' } self.expected_xapi_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' } def test_on_answer_create(self): for draft in [True, False]: self.answer.draft = draft db.session.commit() self.expected_xapi_answer['definition']['extensions'][ 'http://id.tincanapi.com/extension/isDraft'] = draft self.expected_caliper_answer['extensions']['isDraft'] = draft self.expected_caliper_answer[ 'dateModified'] = self.answer.modified.replace( tzinfo=pytz.utc).isoformat() # test without tracking on_answer_create.send(current_app._get_current_object(), event_name=on_answer_create.name, user=self.user, answer=self.answer) events = self.get_and_clear_caliper_event_log() expected_caliper_events = [{ 'action': 'Completed', 'actor': self.get_compair_caliper_actor(self.user), 'membership': self.get_caliper_membership(self.course, self.user, self.lti_context), 'object': self.expected_caliper_assignment_question, 'generated': self.expected_caliper_answer, 'session': self.get_caliper_session( self.get_compair_caliper_actor(self.user)), 'type': 'AssessmentItemEvent' }, { 'action': 'Submitted', 'actor': self.get_compair_caliper_actor(self.user), 'membership': self.get_caliper_membership(self.course, self.user, self.lti_context), 'object': self.expected_caliper_assignment, 'generated': { 'assignable': self.expected_caliper_assignment, 'assignee': self.get_compair_caliper_actor(self.user), 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/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' }, 'session': self.get_caliper_session( self.get_compair_caliper_actor(self.user)), 'type': 'AssessmentEvent' }] self.assertEqual(len(events), len(expected_caliper_events)) for index, expected_event in enumerate(expected_caliper_events): self.assertEqual(events[index], expected_event) statements = self.get_and_clear_xapi_statement_log() expected_xapi_statements = [{ "actor": self.get_compair_xapi_actor(self.user), "verb": { 'id': 'http://adlnet.gov/expapi/verbs/completed', 'display': { 'en-US': 'completed' } }, "object": self.expected_xapi_answer, "context": { 'registration': self.answer.attempt_uuid, 'contextActivities': { 'parent': [ self.expected_xapi_assignment_question, self.expected_xapi_answer_attempt ], 'grouping': [ self.expected_xapi_assignment, self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section ] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } }, "result": { 'success': True, 'duration': "PT05M00S", 'completion': not draft, '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(" ")) } } }, { "actor": self.get_compair_xapi_actor(self.user), "verb": { 'id': 'http://activitystrea.ms/schema/1.0/submit', 'display': { 'en-US': 'submitted' } }, "object": { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/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' }, "context": { 'registration': self.answer.attempt_uuid, 'contextActivities': { 'parent': [self.expected_xapi_assignment], 'grouping': [ self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section ] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } }, "result": { 'success': True, 'completion': not draft } }] self.assertEqual(len(statements), len(expected_xapi_statements)) for index, expected_statement in enumerate( expected_xapi_statements): self.assertEqual(statements[index], expected_statement) def test_on_answer_modified(self): for draft in [True, False]: self.answer.draft = draft db.session.commit() self.expected_xapi_answer['definition']['extensions'][ 'http://id.tincanapi.com/extension/isDraft'] = draft self.expected_caliper_answer['extensions']['isDraft'] = draft self.expected_caliper_answer[ 'dateModified'] = self.answer.modified.replace( tzinfo=pytz.utc).isoformat() # test without tracking on_answer_modified.send(current_app._get_current_object(), event_name=on_answer_modified.name, user=self.user, answer=self.answer) events = self.get_and_clear_caliper_event_log() expected_caliper_events = [{ 'action': 'Completed', 'actor': self.get_compair_caliper_actor(self.user), 'membership': self.get_caliper_membership(self.course, self.user, self.lti_context), 'object': self.expected_caliper_assignment_question, 'generated': self.expected_caliper_answer, 'session': self.get_caliper_session( self.get_compair_caliper_actor(self.user)), 'type': 'AssessmentItemEvent' }, { 'action': 'Submitted', 'actor': self.get_compair_caliper_actor(self.user), 'membership': self.get_caliper_membership(self.course, self.user, self.lti_context), 'object': self.expected_caliper_assignment, 'generated': { 'assignable': self.expected_caliper_assignment, 'assignee': self.get_compair_caliper_actor(self.user), 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/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' }, 'session': self.get_caliper_session( self.get_compair_caliper_actor(self.user)), 'type': 'AssessmentEvent' }] self.assertEqual(len(events), len(expected_caliper_events)) for index, expected_event in enumerate(expected_caliper_events): self.assertEqual(events[index], expected_event) statements = self.get_and_clear_xapi_statement_log() expected_xapi_statements = [{ "actor": self.get_compair_xapi_actor(self.user), "verb": { 'id': 'http://adlnet.gov/expapi/verbs/completed', 'display': { 'en-US': 'completed' } }, "object": self.expected_xapi_answer, "context": { 'registration': self.answer.attempt_uuid, 'contextActivities': { 'parent': [ self.expected_xapi_assignment_question, self.expected_xapi_answer_attempt ], 'grouping': [ self.expected_xapi_assignment, self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section ] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } }, "result": { 'success': True, 'duration': "PT05M00S", 'completion': not draft, '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(" ")) } } }, { "actor": self.get_compair_xapi_actor(self.user), "verb": { 'id': 'http://activitystrea.ms/schema/1.0/submit', 'display': { 'en-US': 'submitted' } }, "object": { 'id': "https://localhost:8888/app/course/" + self.course.uuid + "/assignment/" + self.assignment.uuid + "/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' }, "context": { 'registration': self.answer.attempt_uuid, 'contextActivities': { 'parent': [self.expected_xapi_assignment], 'grouping': [ self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section ] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } }, "result": { 'success': True, 'completion': not draft } }] self.assertEqual(len(statements), len(expected_xapi_statements)) for index, expected_statement in enumerate( expected_xapi_statements): self.assertEqual(statements[index], expected_statement) def test_on_answer_delete(self): # send delete on_answer_delete.send(current_app._get_current_object(), event_name=on_answer_delete.name, user=self.user, answer=self.answer) events = self.get_and_clear_caliper_event_log() expected_caliper_event = { 'action': 'Deleted', 'actor': self.get_compair_caliper_actor(self.user), 'membership': self.get_caliper_membership(self.course, self.user, self.lti_context), 'object': self.expected_caliper_answer, 'session': self.get_caliper_session(self.get_compair_caliper_actor( self.user)), 'type': 'Event' } self.assertEqual(len(events), 1) self.assertEqual(events[0], expected_caliper_event) statements = self.get_and_clear_xapi_statement_log() expected_xapi_statement = { "actor": self.get_compair_xapi_actor(self.user), "verb": { 'id': 'http://activitystrea.ms/schema/1.0/delete', 'display': { 'en-US': 'deleted' } }, "object": self.expected_xapi_answer, "context": { 'contextActivities': { 'parent': [ self.expected_xapi_assignment_question, self.expected_xapi_answer_attempt ], 'grouping': [ self.expected_xapi_assignment, self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section ] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } }, } self.assertEqual(len(statements), 1) self.assertEqual(statements[0], expected_xapi_statement)
class AnswersAPITests(ComPAIRAPITestCase): def setUp(self): super(AnswersAPITests, self).setUp() self.fixtures = TestFixture().add_course(num_students=30, num_groups=2, with_draft_student=True) self.base_url = self._build_url(self.fixtures.course.uuid, self.fixtures.assignment.uuid) self.lti_data = LTITestData() def _build_url(self, course_uuid, assignment_uuid, tail=""): url = '/api/courses/' + course_uuid + '/assignments/' + assignment_uuid + '/answers' + tail return url def test_get_all_answers(self): # add some answers to top answers top_answers = self.fixtures.answers[:5] for answer in top_answers: answer.top_answer = True db.session.commit() # Test login required rv = self.client.get(self.base_url) self.assert401(rv) # test unauthorized users with self.login(self.fixtures.unauthorized_instructor.username): rv = self.client.get(self.base_url) self.assert403(rv) with self.login(self.fixtures.unauthorized_student.username): rv = self.client.get(self.base_url) self.assert403(rv) with self.login(self.fixtures.students[0].username): # test non-existent entry rv = self.client.get(self._build_url(self.fixtures.course.uuid, "4903409")) self.assert404(rv) # test data retrieve is correct self.fixtures.assignment.answer_end = datetime.datetime.now() - datetime.timedelta(days=1) db.session.add(self.fixtures.assignment) db.session.commit() rv = self.client.get(self.base_url) self.assert200(rv) actual_answers = rv.json['objects'] expected_answers = Answer.query \ .filter_by(active=True, draft=False, assignment_id=self.fixtures.assignment.id) \ .filter(~Answer.id.in_([a.id for a in self.fixtures.dropped_answers])) \ .order_by(Answer.created.desc()) \ .paginate(1, 20) for i, expected in enumerate(expected_answers.items): actual = actual_answers[i] self.assertEqual(expected.content, actual['content']) if expected.score: self.assertEqual(expected.score.rank, actual['score']['rank']) self.assertFalse('normalized_score' in actual['score']) else: self.assertIsNone(actual['score']) self.assertEqual(1, rv.json['page']) self.assertEqual(2, rv.json['pages']) self.assertEqual(20, rv.json['per_page']) self.assertEqual(expected_answers.total, rv.json['total']) # test the second page rv = self.client.get(self.base_url + '?page=2') self.assert200(rv) actual_answers = rv.json['objects'] expected_answers = Answer.query \ .filter_by(active=True, draft=False, assignment_id=self.fixtures.assignment.id) \ .filter(~Answer.id.in_([a.id for a in self.fixtures.dropped_answers])) \ .order_by(Answer.created.desc()) \ .paginate(2, 20) for i, expected in enumerate(expected_answers.items): actual = actual_answers[i] self.assertEqual(expected.content, actual['content']) if expected.score: self.assertEqual(expected.score.rank, actual['score']['rank']) self.assertFalse('normalized_score' in actual['score']) else: self.assertIsNone(actual['score']) self.assertEqual(2, rv.json['page']) self.assertEqual(2, rv.json['pages']) self.assertEqual(20, rv.json['per_page']) self.assertEqual(expected_answers.total, rv.json['total']) # test sorting by rank (display_rank_limit 10) self.fixtures.assignment.rank_display_limit = 10 db.session.commit() rv = self.client.get(self.base_url + '?orderBy=score') self.assert200(rv) result = rv.json['objects'] # test the result is paged and sorted expected = sorted( [answer for answer in self.fixtures.answers if answer.score], key=lambda ans: (ans.score.score, ans.created), reverse=True)[:10] self.assertEqual([a.uuid for a in expected], [a['id'] for a in result]) self.assertEqual(1, rv.json['page']) self.assertEqual(1, rv.json['pages']) self.assertEqual(20, rv.json['per_page']) self.assertEqual(len(expected), rv.json['total']) # test sorting by rank (display_rank_limit 20) self.fixtures.assignment.rank_display_limit = 20 db.session.commit() rv = self.client.get(self.base_url + '?orderBy=score') self.assert200(rv) result = rv.json['objects'] # test the result is paged and sorted expected = sorted( [answer for answer in self.fixtures.answers if answer.score], key=lambda ans: (ans.score.score, ans.created), reverse=True)[:20] self.assertEqual([a.uuid for a in expected], [a['id'] for a in result]) self.assertEqual(1, rv.json['page']) self.assertEqual(1, rv.json['pages']) self.assertEqual(20, rv.json['per_page']) self.assertEqual(len(expected), rv.json['total']) # test sorting by rank (display_rank_limit None) self.fixtures.assignment.rank_display_limit = None db.session.commit() rv = self.client.get(self.base_url + '?orderBy=score') self.assert200(rv) result = rv.json['objects'] # test the result is paged and sorted expected = sorted( [answer for answer in self.fixtures.answers if answer.score], key=lambda ans: (ans.score.score, ans.created), reverse=True)[:20] self.assertEqual([a.uuid for a in expected], [a['id'] for a in result]) self.assertEqual(1, rv.json['page']) self.assertEqual(1, rv.json['pages']) self.assertEqual(20, rv.json['per_page']) self.assertEqual(len(expected), rv.json['total']) # test author filter rv = self.client.get(self.base_url + '?author={}'.format(self.fixtures.students[0].uuid)) self.assert200(rv) result = rv.json['objects'] self.assertEqual(len(result), 1) self.assertEqual(result[0]['user_id'], self.fixtures.students[0].uuid) # test group filter rv = self.client.get(self.base_url + '?group={}'.format(self.fixtures.groups[0])) self.assert200(rv) result = rv.json['objects'] self.assertEqual(len(result), len(self.fixtures.answers) / len(self.fixtures.groups)) # test ids filter ids = {a.uuid for a in self.fixtures.answers[:3]} rv = self.client.get(self.base_url + '?ids={}'.format(','.join(ids))) self.assert200(rv) result = rv.json['objects'] self.assertEqual(ids, {str(a['id']) for a in result}) # test top_answer filter top_answer_ids = {a.uuid for a in top_answers} rv = self.client.get(self.base_url + '?top=true') self.assert200(rv) result = rv.json['objects'] self.assertEqual(top_answer_ids, {a['id'] for a in result}) # test combined filter rv = self.client.get( self.base_url + '?orderBy=score&group={}'.format( self.fixtures.groups[0] ) ) self.assert200(rv) result = rv.json['objects'] # test the result is paged and sorted answers_per_group = int(len(self.fixtures.answers) / len(self.fixtures.groups)) if len( self.fixtures.groups) else 0 answers = self.fixtures.answers[:answers_per_group] expected = sorted(answers, key=lambda ans: ans.score.score, reverse=True) self.assertEqual([a.uuid for a in expected], [a['id'] for a in result]) # all filters rv = self.client.get( self.base_url + '?orderBy=score&group={}&author={}&top=true&page=1&perPage=20'.format( self.fixtures.groups[0], self.fixtures.students[0].uuid ) ) self.assert200(rv) result = rv.json['objects'] self.assertEqual(len(result), 1) self.assertEqual(result[0]['user_id'], self.fixtures.students[0].uuid) # add instructor answer answer = AnswerFactory( assignment=self.fixtures.assignment, user=self.fixtures.instructor ) self.fixtures.answers.append(answer) db.session.commit() rv = self.client.get(self.base_url) self.assert200(rv) result = rv.json['objects'] user_uuids = [a['user_id'] for a in result] self.assertEqual(len(self.fixtures.answers), rv.json['total']) # first answer should be instructor answer self.assertEqual(self.fixtures.instructor.uuid, result[0]['user_id']) # no dropped student answers should be included for dropped_student in self.fixtures.dropped_students: self.assertNotIn(dropped_student.uuid, user_uuids) # test data retrieve before answer period ended with non-privileged user self.fixtures.assignment.answer_end = datetime.datetime.now() + datetime.timedelta(days=2) db.session.add(self.fixtures.assignment) db.session.commit() rv = self.client.get(self.base_url) self.assert200(rv) actual_answers = rv.json['objects'] self.assertEqual(1, len(actual_answers)) self.assertEqual(1, rv.json['page']) self.assertEqual(1, rv.json['pages']) self.assertEqual(20, rv.json['per_page']) self.assertEqual(1, rv.json['total']) # test data retrieve before answer period ended with privileged user with self.login(self.fixtures.instructor.username): rv = self.client.get(self.base_url) self.assert200(rv) actual_answers = rv.json['objects'] self.assertEqual(20, len(actual_answers)) self.assertEqual(1, rv.json['page']) self.assertEqual(2, rv.json['pages']) self.assertEqual(20, rv.json['per_page']) self.assertEqual(len(self.fixtures.answers), rv.json['total']) @mock.patch('compair.tasks.lti_outcomes.update_lti_course_grades.run') @mock.patch('compair.tasks.lti_outcomes.update_lti_assignment_grades.run') def test_create_answer(self, mocked_update_assignment_grades_run, mocked_update_course_grades_run): # test login required expected_answer = {'content': 'this is some answer content'} rv = self.client.post( self.base_url, data=json.dumps(expected_answer), content_type='application/json') self.assert401(rv) # test unauthorized users with self.login(self.fixtures.unauthorized_student.username): rv = self.client.post(self.base_url, data=json.dumps(expected_answer), content_type='application/json') self.assert403(rv) with self.login(self.fixtures.unauthorized_instructor.username): rv = self.client.post( self.base_url, data=json.dumps(expected_answer), content_type='application/json') self.assert403(rv) # test invalid format with self.login(self.fixtures.students[0].username): invalid_answer = {'post': {'blah': 'blah'}} rv = self.client.post( self.base_url, data=json.dumps(invalid_answer), content_type='application/json') self.assert400(rv) # test invalid assignment rv = self.client.post( self._build_url(self.fixtures.course.uuid, "9392402"), data=json.dumps(expected_answer), content_type='application/json') self.assert404(rv) # test invalid course rv = self.client.post( self._build_url("9392402", self.fixtures.assignment.uuid), data=json.dumps(expected_answer), content_type='application/json') self.assert404(rv) # test create successful with self.login(self.fixtures.instructor.username): rv = self.client.post( self.base_url, data=json.dumps(expected_answer), content_type='application/json') self.assert200(rv) # retrieve again and verify actual_answer = Answer.query.filter_by(uuid=rv.json['id']).one() self.assertEqual(expected_answer['content'], actual_answer.content) # user should not have grades new_course_grade = CourseGrade.get_user_course_grade( self.fixtures.course, self.fixtures.instructor) new_assignment_grade = AssignmentGrade.get_user_assignment_grade(self.fixtures.assignment, self.fixtures.instructor) self.assertIsNone(new_course_grade) self.assertIsNone(new_assignment_grade) # test instructor could submit multiple answers for his/her own rv = self.client.post( self.base_url, data=json.dumps(expected_answer), content_type='application/json') self.assert200(rv) actual_answer = Answer.query.filter_by(uuid=rv.json['id']).one() self.assertEqual(expected_answer['content'], actual_answer.content) # user should not have grades new_course_grade = CourseGrade.get_user_course_grade( self.fixtures.course, self.fixtures.instructor) new_assignment_grade = AssignmentGrade.get_user_assignment_grade(self.fixtures.assignment, self.fixtures.instructor) self.assertIsNone(new_course_grade) self.assertIsNone(new_assignment_grade) # test instructor could submit multiple answers for his/her own expected_answer.update({'user_id': self.fixtures.instructor.uuid}) rv = self.client.post( self.base_url, data=json.dumps(expected_answer), content_type='application/json') self.assert200(rv) actual_answer = Answer.query.filter_by(uuid=rv.json['id']).one() self.assertEqual(expected_answer['content'], actual_answer.content) # user should not have grades new_course_grade = CourseGrade.get_user_course_grade( self.fixtures.course, self.fixtures.instructor) new_assignment_grade = AssignmentGrade.get_user_assignment_grade(self.fixtures.assignment, self.fixtures.instructor) self.assertIsNone(new_course_grade) self.assertIsNone(new_assignment_grade) # test instructor could submit on behave of a student self.fixtures.add_students(1) expected_answer.update({'user_id': self.fixtures.students[-1].uuid}) rv = self.client.post( self.base_url, data=json.dumps(expected_answer), content_type='application/json') self.assert200(rv) actual_answer = Answer.query.filter_by(uuid=rv.json['id']).one() self.assertEqual(expected_answer['content'], actual_answer.content) # user should have grades new_course_grade = CourseGrade.get_user_course_grade( self.fixtures.course, self.fixtures.students[-1]) new_assignment_grade = AssignmentGrade.get_user_assignment_grade(self.fixtures.assignment, self.fixtures.students[-1]) self.assertIsNotNone(new_course_grade) self.assertIsNotNone(new_assignment_grade) # test instructor can not submit additional answers for a student expected_answer.update({'user_id': self.fixtures.students[0].uuid}) rv = self.client.post( self.base_url, data=json.dumps(expected_answer), content_type='application/json') self.assert400(rv) self.assertEqual(rv.json['title'], "Answer Not Submitted") self.assertEqual(rv.json['message'], "An answer has already been submitted for this assignment by you or on your behalf.") self.fixtures.add_students(1) self.fixtures.course.calculate_grade(self.fixtures.students[-1]) self.fixtures.assignment.calculate_grade(self.fixtures.students[-1]) expected_answer = {'content': 'this is some answer content', 'draft': True} with self.login(self.fixtures.students[-1].username): course_grade = CourseGrade.get_user_course_grade( self.fixtures.course, self.fixtures.students[-1]).grade assignment_grade = AssignmentGrade.get_user_assignment_grade( self.fixtures.assignments[0], self.fixtures.students[-1]).grade # test create draft successful rv = self.client.post( self.base_url, data=json.dumps(expected_answer), content_type='application/json') self.assert200(rv) actual_answer = Answer.query.filter_by(uuid=rv.json['id']).one() self.assertEqual(expected_answer['content'], actual_answer.content) self.assertEqual(expected_answer['draft'], actual_answer.draft) # grades should not change new_course_grade = CourseGrade.get_user_course_grade( self.fixtures.course, self.fixtures.draft_student).grade new_assignment_grade = AssignmentGrade.get_user_assignment_grade( self.fixtures.assignments[0], self.fixtures.draft_student).grade self.assertEqual(new_course_grade, course_grade) self.assertEqual(new_assignment_grade, assignment_grade) with self.login(self.fixtures.instructor.username): # test instructor can submit outside of grace period self.fixtures.assignment.answer_end = datetime.datetime.utcnow() - datetime.timedelta(minutes=2) db.session.add(self.fixtures.assignment) db.session.commit() self.fixtures.add_students(1) expected_answer.update({'user_id': self.fixtures.students[-1].uuid}) rv = self.client.post( self.base_url, data=json.dumps(expected_answer), content_type='application/json') self.assert200(rv) actual_answer = Answer.query.filter_by(uuid=rv.json['id']).one() self.assertEqual(expected_answer['content'], actual_answer.content) # test create successful self.fixtures.add_students(1) self.fixtures.course.calculate_grade(self.fixtures.students[-1]) self.fixtures.assignment.calculate_grade(self.fixtures.students[-1]) expected_answer = {'content': 'this is some answer content'} with self.login(self.fixtures.students[-1].username): # test student can not submit answers after answer grace period self.fixtures.assignment.answer_end = datetime.datetime.utcnow() - datetime.timedelta(minutes=2) db.session.add(self.fixtures.assignment) db.session.commit() rv = self.client.post( self.base_url, data=json.dumps(expected_answer), content_type='application/json') self.assert403(rv) self.assertEqual("Answer Not Submitted", rv.json['title']) self.assertEqual("Sorry, the answer deadline has passed. No answers can be submitted after the deadline unless the instructor submits the answer for you.", rv.json['message']) # test student can submit answers within answer grace period self.fixtures.assignment.answer_end = datetime.datetime.utcnow() - datetime.timedelta(seconds=15) db.session.add(self.fixtures.assignment) db.session.commit() course_grade = CourseGrade.get_user_course_grade( self.fixtures.course, self.fixtures.students[-1]).grade assignment_grade = AssignmentGrade.get_user_assignment_grade( self.fixtures.assignment, self.fixtures.students[-1]).grade lti_consumer = self.lti_data.lti_consumer student = self.fixtures.students[-1] (lti_user_resource_link1, lti_user_resource_link2) = self.lti_data.setup_student_user_resource_links( student, self.fixtures.course, self.fixtures.assignment) rv = self.client.post( self.base_url, data=json.dumps(expected_answer), content_type='application/json') self.assert200(rv) actual_answer = Answer.query.filter_by(uuid=rv.json['id']).one() self.assertEqual(expected_answer['content'], actual_answer.content) # grades should increase new_course_grade = CourseGrade.get_user_course_grade( self.fixtures.course, self.fixtures.students[-1]) new_assignment_grade = AssignmentGrade.get_user_assignment_grade( self.fixtures.assignment, self.fixtures.students[-1]) self.assertGreater(new_course_grade.grade, course_grade) self.assertGreater(new_assignment_grade.grade, assignment_grade) mocked_update_assignment_grades_run.assert_called_once_with( lti_consumer.id, [(lti_user_resource_link2.lis_result_sourcedid, new_assignment_grade.id)] ) mocked_update_assignment_grades_run.reset_mock() mocked_update_course_grades_run.assert_called_once_with( lti_consumer.id, [(lti_user_resource_link1.lis_result_sourcedid, new_course_grade.id)] ) mocked_update_course_grades_run.reset_mock() # test create successful for system admin with self.login('root'): rv = self.client.post( self.base_url, data=json.dumps(expected_answer), content_type='application/json') self.assert200(rv) # retrieve again and verify actual_answer = Answer.query.filter_by(uuid=rv.json['id']).one() self.assertEqual(expected_answer['content'], actual_answer.content) # test system admin could submit multiple answers for his/her own rv = self.client.post( self.base_url, data=json.dumps(expected_answer), content_type='application/json') self.assert200(rv) actual_answer = Answer.query.filter_by(uuid=rv.json['id']).one() self.assertEqual(expected_answer['content'], actual_answer.content) def test_get_answer(self): assignment_uuid = self.fixtures.assignments[0].uuid answer = self.fixtures.answers[0] draft_answer = self.fixtures.draft_answers[0] # test login required rv = self.client.get(self.base_url + '/' + answer.uuid) self.assert401(rv) # test unauthorized user with self.login(self.fixtures.unauthorized_instructor.username): rv = self.client.get(self.base_url + '/' + answer.uuid) self.assert403(rv) # test invalid course id with self.login(self.fixtures.students[0].username): rv = self.client.get(self._build_url("999", assignment_uuid, '/' + answer.uuid)) self.assert404(rv) # test invalid answer id rv = self.client.get(self._build_url(self.fixtures.course.uuid, assignment_uuid, '/' + "999")) self.assert404(rv) # test invalid get another user's draft answer rv = self.client.get(self.base_url + '/' + draft_answer.uuid) self.assert403(rv) # test authorized student rv = self.client.get(self.base_url + '/' + answer.uuid) self.assert200(rv) self.assertEqual(assignment_uuid, rv.json['assignment_id']) self.assertEqual(answer.user_uuid, rv.json['user_id']) self.assertEqual(answer.content, rv.json['content']) self.assertFalse(rv.json['draft']) self.assertEqual(answer.score.rank, rv.json['score']['rank']) self.assertFalse('normalized_score' in rv.json['score']) # test authorized student draft answer with self.login(self.fixtures.draft_student.username): rv = self.client.get(self.base_url + '/' + draft_answer.uuid) self.assert200(rv) self.assertEqual(assignment_uuid, rv.json['assignment_id']) self.assertEqual(draft_answer.user_uuid, rv.json['user_id']) self.assertEqual(draft_answer.content, rv.json['content']) self.assertTrue(rv.json['draft']) # test authorized teaching assistant with self.login(self.fixtures.ta.username): rv = self.client.get(self.base_url + '/' + answer.uuid) self.assert200(rv) self.assertEqual(assignment_uuid, rv.json['assignment_id']) self.assertEqual(answer.user_uuid, rv.json['user_id']) self.assertEqual(answer.content, rv.json['content']) self.assertEqual(answer.score.rank, rv.json['score']['rank']) self.assertEqual(int(answer.score.normalized_score), rv.json['score']['normalized_score']) # test authorized instructor with self.login(self.fixtures.instructor.username): rv = self.client.get(self.base_url + '/' + answer.uuid) self.assert200(rv) self.assertEqual(assignment_uuid, rv.json['assignment_id']) self.assertEqual(answer.user_uuid, rv.json['user_id']) self.assertEqual(answer.content, rv.json['content']) self.assertEqual(answer.score.rank, rv.json['score']['rank']) self.assertEqual(int(answer.score.normalized_score), rv.json['score']['normalized_score']) @mock.patch('compair.tasks.lti_outcomes.update_lti_course_grades.run') @mock.patch('compair.tasks.lti_outcomes.update_lti_assignment_grades.run') def test_edit_answer(self, mocked_update_assignment_grades_run, mocked_update_course_grades_run): assignment_uuid = self.fixtures.assignments[0].uuid answer = self.fixtures.answers[0] expected = {'id': answer.uuid, 'content': 'This is an edit'} draft_answer = self.fixtures.draft_answers[0] draft_expected = {'id': draft_answer.uuid, 'content': 'This is an edit', 'draft': True} # test login required rv = self.client.post( self.base_url + '/' + answer.uuid, data=json.dumps(expected), content_type='application/json') self.assert401(rv) # test unauthorized user with self.login(self.fixtures.students[1].username): rv = self.client.post( self.base_url + '/' + answer.uuid, data=json.dumps(expected), content_type='application/json') self.assert403(rv) # test invalid course id with self.login(self.fixtures.students[0].username): rv = self.client.post( self._build_url("999", assignment_uuid, '/' + answer.uuid), data=json.dumps(expected), content_type='application/json') self.assert404(rv) # test invalid assignment id rv = self.client.post( self._build_url(self.fixtures.course.uuid, "999", '/' + answer.uuid), data=json.dumps(expected), content_type='application/json') self.assert404(rv) # test invalid answer id rv = self.client.post( self.base_url + '/999', data=json.dumps(expected), content_type='application/json') self.assert404(rv) # test unmatched answer id with self.login(self.fixtures.students[1].username): rv = self.client.post( self.base_url + '/' + self.fixtures.answers[1].uuid, data=json.dumps(expected), content_type='application/json') self.assert400(rv) with self.login(self.fixtures.draft_student.username): course_grade = CourseGrade.get_user_course_grade( self.fixtures.course, self.fixtures.draft_student).grade assignment_grade = AssignmentGrade.get_user_assignment_grade( self.fixtures.assignments[0], self.fixtures.draft_student).grade lti_consumer = self.lti_data.lti_consumer student = self.fixtures.draft_student (lti_user_resource_link1, lti_user_resource_link2) = self.lti_data.setup_student_user_resource_links( student, self.fixtures.course, self.fixtures.assignment) # test edit draft by author rv = self.client.post( self.base_url + '/' + draft_answer.uuid, data=json.dumps(draft_expected), content_type='application/json') self.assert200(rv) self.assertEqual(draft_answer.uuid, rv.json['id']) self.assertEqual('This is an edit', rv.json['content']) self.assertEqual(draft_answer.draft, rv.json['draft']) self.assertTrue(rv.json['draft']) # grades should not change new_course_grade = CourseGrade.get_user_course_grade( self.fixtures.course, self.fixtures.draft_student).grade new_assignment_grade = AssignmentGrade.get_user_assignment_grade( self.fixtures.assignments[0], self.fixtures.draft_student).grade self.assertEqual(new_course_grade, course_grade) self.assertEqual(new_assignment_grade, assignment_grade) mocked_update_assignment_grades_run.assert_not_called() mocked_update_course_grades_run.assert_not_called() # set draft to false draft_expected_copy = draft_expected.copy() draft_expected_copy['draft'] = False rv = self.client.post( self.base_url + '/' + draft_answer.uuid, data=json.dumps(draft_expected_copy), content_type='application/json') self.assert200(rv) self.assertEqual(draft_answer.uuid, rv.json['id']) self.assertEqual('This is an edit', rv.json['content']) self.assertEqual(draft_answer.draft, rv.json['draft']) self.assertFalse(rv.json['draft']) # grades should increase new_course_grade = CourseGrade.get_user_course_grade( self.fixtures.course, self.fixtures.draft_student) new_assignment_grade = AssignmentGrade.get_user_assignment_grade( self.fixtures.assignments[0], self.fixtures.draft_student) self.assertGreater(new_course_grade.grade, course_grade) self.assertGreater(new_assignment_grade.grade, assignment_grade) mocked_update_assignment_grades_run.assert_called_once_with( lti_consumer.id, [(lti_user_resource_link2.lis_result_sourcedid, new_assignment_grade.id)] ) mocked_update_assignment_grades_run.reset_mock() mocked_update_course_grades_run.assert_called_once_with( lti_consumer.id, [(lti_user_resource_link1.lis_result_sourcedid, new_course_grade.id)] ) mocked_update_course_grades_run.reset_mock() # setting draft to true when false should not work rv = self.client.post( self.base_url + '/' + draft_answer.uuid, data=json.dumps(draft_expected), content_type='application/json') self.assert200(rv) self.assertEqual(draft_answer.uuid, rv.json['id']) self.assertEqual('This is an edit', rv.json['content']) self.assertEqual(draft_answer.draft, rv.json['draft']) self.assertFalse(rv.json['draft']) # test edit by author with self.login(self.fixtures.students[0].username): rv = self.client.post( self.base_url + '/' + answer.uuid, data=json.dumps(expected), content_type='application/json') self.assert200(rv) self.assertEqual(answer.uuid, rv.json['id']) self.assertEqual('This is an edit', rv.json['content']) # test edit by user that can manage posts manage_expected = { 'id': answer.uuid, 'content': 'This is another edit' } with self.login(self.fixtures.instructor.username): rv = self.client.post( self.base_url + '/' + answer.uuid, data=json.dumps(manage_expected), content_type='application/json') self.assert200(rv) self.assertEqual(answer.uuid, rv.json['id']) self.assertEqual('This is another edit', rv.json['content']) # test edit by author with self.login(self.fixtures.students[0].username): # test student can not submit answers after answer grace period self.fixtures.assignment.answer_end = datetime.datetime.utcnow() - datetime.timedelta(minutes=10) db.session.add(self.fixtures.assignment) db.session.commit() rv = self.client.post( self.base_url + '/' + answer.uuid, data=json.dumps(expected), content_type='application/json') self.assert403(rv) self.assertEqual("Answer Not Submitted", rv.json['title']) self.assertEqual("Sorry, the answer deadline has passed. No answers can be submitted after the deadline unless the instructor submits the answer for you.", rv.json['message']) # test student can submit answers within answer grace period self.fixtures.assignment.answer_end = datetime.datetime.utcnow() - datetime.timedelta(seconds=15) db.session.add(self.fixtures.assignment) db.session.commit() rv = self.client.post( self.base_url + '/' + answer.uuid, data=json.dumps(expected), content_type='application/json') self.assert200(rv) self.assertEqual(answer.uuid, rv.json['id']) self.assertEqual('This is an edit', rv.json['content']) @mock.patch('compair.tasks.lti_outcomes.update_lti_course_grades.run') @mock.patch('compair.tasks.lti_outcomes.update_lti_assignment_grades.run') def test_delete_answer(self, mocked_update_assignment_grades_run, mocked_update_course_grades_run): answer_uuid = self.fixtures.answers[0].uuid # test login required rv = self.client.delete(self.base_url + '/' + answer_uuid) self.assert401(rv) # test unauthorized users with self.login(self.fixtures.students[1].username): rv = self.client.delete(self.base_url + '/' + answer_uuid) self.assert403(rv) # test invalid answer id with self.login(self.fixtures.students[0].username): rv = self.client.delete(self.base_url + '/999') self.assert404(rv) lti_consumer = self.lti_data.lti_consumer student = self.fixtures.students[0] (lti_user_resource_link1, lti_user_resource_link2) = self.lti_data.setup_student_user_resource_links( student, self.fixtures.course, self.fixtures.assignment) course_grade = CourseGrade.get_user_course_grade( self.fixtures.course, self.fixtures.students[0]).grade assignment_grade = AssignmentGrade.get_user_assignment_grade( self.fixtures.assignments[0], self.fixtures.students[0]).grade # test deletion by author rv = self.client.delete(self.base_url + '/' + answer_uuid) self.assert200(rv) self.assertEqual(answer_uuid, rv.json['id']) # grades should decrease new_course_grade = CourseGrade.get_user_course_grade( self.fixtures.course, self.fixtures.students[0]) new_assignment_grade = AssignmentGrade.get_user_assignment_grade( self.fixtures.assignments[0], self.fixtures.students[0]) self.assertLess(new_course_grade.grade, course_grade) self.assertLess(new_assignment_grade.grade, assignment_grade) mocked_update_assignment_grades_run.assert_called_once_with( lti_consumer.id, [(lti_user_resource_link2.lis_result_sourcedid, new_assignment_grade.id)] ) mocked_update_assignment_grades_run.reset_mock() mocked_update_course_grades_run.assert_called_once_with( lti_consumer.id, [(lti_user_resource_link1.lis_result_sourcedid, new_course_grade.id)] ) mocked_update_course_grades_run.reset_mock() # test deletion by user that can manage posts with self.login(self.fixtures.instructor.username): answer_uuid2 = self.fixtures.answers[1].uuid rv = self.client.delete(self.base_url + '/' + answer_uuid2) self.assert200(rv) self.assertEqual(answer_uuid2, rv.json['id']) def test_get_user_answers(self): assignment = self.fixtures.assignments[0] answer = self.fixtures.answers[0] draft_answer = self.fixtures.draft_answers[0] url = self._build_url(self.fixtures.course.uuid, assignment.uuid, '/user') # test login required rv = self.client.get(url) self.assert401(rv) with self.login(self.fixtures.students[0].username): # test invalid course rv = self.client.get(self._build_url("999", assignment.uuid, '/user')) self.assert404(rv) # test invalid assignment rv = self.client.get(self._build_url(self.fixtures.course.uuid, "999", '/user')) self.assert404(rv) # test successful query rv = self.client.get(url) self.assert200(rv) self.assertEqual(1, len(rv.json['objects'])) self.assertEqual(answer.uuid, rv.json['objects'][0]['id']) self.assertEqual(answer.content, rv.json['objects'][0]['content']) self.assertEqual(answer.draft, rv.json['objects'][0]['draft']) # test draft query rv = self.client.get(url, query_string={'draft': True}) self.assert200(rv) self.assertEqual(0, len(rv.json['objects'])) # test unsaved query rv = self.client.get(url, query_string={'unsaved': True}) self.assert200(rv) self.assertEqual(1, len(rv.json['objects'])) self.assertEqual(answer.uuid, rv.json['objects'][0]['id']) self.assertEqual(answer.content, rv.json['objects'][0]['content']) self.assertEqual(answer.draft, rv.json['objects'][0]['draft']) answer.content = answer.content+"123" db.session.commit() rv = self.client.get(url, query_string={'unsaved': True}) self.assert200(rv) self.assertEqual(0, len(rv.json['objects'])) with self.login(self.fixtures.draft_student.username): # test successful query rv = self.client.get(url) self.assert200(rv) self.assertEqual(0, len(rv.json['objects'])) # test draft query rv = self.client.get(url, query_string={'draft': True}) self.assert200(rv) self.assertEqual(1, len(rv.json['objects'])) self.assertEqual(draft_answer.uuid, rv.json['objects'][0]['id']) self.assertEqual(draft_answer.content, rv.json['objects'][0]['content']) self.assertEqual(draft_answer.draft, rv.json['objects'][0]['draft']) # test unsaved query rv = self.client.get(url, query_string={'unsaved': True}) self.assert200(rv) self.assertEqual(0, len(rv.json['objects'])) # test draft + unsaved query rv = self.client.get(url, query_string={'draft': True, 'unsaved': True}) self.assert200(rv) self.assertEqual(1, len(rv.json['objects'])) self.assertEqual(draft_answer.uuid, rv.json['objects'][0]['id']) self.assertEqual(draft_answer.content, rv.json['objects'][0]['content']) self.assertEqual(draft_answer.draft, rv.json['objects'][0]['draft']) draft_answer.content = draft_answer.content+"123" db.session.commit() rv = self.client.get(url, query_string={'draft': True, 'unsaved': True}) self.assert200(rv) self.assertEqual(0, len(rv.json['objects'])) with self.login(self.fixtures.instructor.username): rv = self.client.get(url) self.assert200(rv) self.assertEqual(0, len(rv.json['objects'])) # test draft query rv = self.client.get(url, query_string={'draft': True}) self.assert200(rv) self.assertEqual(0, len(rv.json['objects'])) # test unsaved query rv = self.client.get(url, query_string={'unsaved': True}) self.assert200(rv) self.assertEqual(0, len(rv.json['objects'])) def test_flag_answer(self): answer = self.fixtures.answers[0] flag_url = self.base_url + "/" + answer.uuid + "/flagged" # test login required expected_flag_on = {'flagged': True} expected_flag_off = {'flagged': False} rv = self.client.post( flag_url, data=json.dumps(expected_flag_on), content_type='application/json') self.assert401(rv) # test unauthorized users with self.login(self.fixtures.unauthorized_student.username): rv = self.client.post( flag_url, data=json.dumps(expected_flag_on), content_type='application/json') self.assert403(rv) # test flagging with self.login(self.fixtures.students[0].username): rv = self.client.post( flag_url, data=json.dumps(expected_flag_on), content_type='application/json') self.assert200(rv) self.assertEqual( expected_flag_on['flagged'], rv.json['flagged'], "Expected answer to be flagged.") # test unflagging rv = self.client.post( flag_url, data=json.dumps(expected_flag_off), content_type='application/json') self.assert200(rv) self.assertEqual( expected_flag_off['flagged'], rv.json['flagged'], "Expected answer to be flagged.") # test prevent unflagging by other students with self.login(self.fixtures.students[0].username): rv = self.client.post( flag_url, data=json.dumps(expected_flag_on), content_type='application/json') self.assert200(rv) # create another student self.fixtures.add_students(1) other_student = self.fixtures.students[-1] # try to unflag answer as other student, should fail with self.login(other_student.username): rv = self.client.post( flag_url, data=json.dumps(expected_flag_off), content_type='application/json') self.assert400(rv) # test allow unflagging by instructor with self.login(self.fixtures.instructor.username): rv = self.client.post( flag_url, data=json.dumps(expected_flag_off), content_type='application/json') self.assert200(rv) self.assertEqual( expected_flag_off['flagged'], rv.json['flagged'], "Expected answer to be flagged.") def test_top_answer(self): answer = self.fixtures.answers[0] top_answer_url = self.base_url + "/" + answer.uuid + "/top" expected_top_on = {'top_answer': True} expected_top_off = {'top_answer': False} # test login required rv = self.client.post( top_answer_url, data=json.dumps(expected_top_on), content_type='application/json') self.assert401(rv) # test unauthorized users with self.login(self.fixtures.unauthorized_student.username): rv = self.client.post( top_answer_url, data=json.dumps(expected_top_on), content_type='application/json') self.assert403(rv) with self.login(self.fixtures.students[0].username): rv = self.client.post( top_answer_url, data=json.dumps(expected_top_on), content_type='application/json') self.assert403(rv) # test allow setting top_answer by instructor with self.login(self.fixtures.instructor.username): rv = self.client.post( top_answer_url, data=json.dumps(expected_top_on), content_type='application/json') self.assert200(rv) self.assertTrue(rv.json['top_answer']) rv = self.client.post( top_answer_url, data=json.dumps(expected_top_off), content_type='application/json') self.assert200(rv) self.assertFalse(rv.json['top_answer']) # test allow setting top_answer by teaching assistant with self.login(self.fixtures.ta.username): rv = self.client.post( top_answer_url, data=json.dumps(expected_top_on), content_type='application/json') self.assert200(rv) self.assertTrue(rv.json['top_answer']) rv = self.client.post( top_answer_url, data=json.dumps(expected_top_off), content_type='application/json') self.assert200(rv) self.assertFalse(rv.json['top_answer'])
class AnswerCommentAPITests(ComPAIRAPITestCase): """ Tests for answer comment API """ resource = AnswerCommentAPI api = api def setUp(self): super(AnswerCommentAPITests, self).setUp() self.data = AnswerCommentsTestData() self.course = self.data.get_course() self.assignments = self.data.get_assignments() self.answers = self.data.get_answers_by_assignment() self.assignment = self.assignments[0] self.assignment.enable_self_evaluation = True db.session.commit() self.assignment.calculate_grades() self.lti_data = LTITestData() def test_get_single_answer_comment(self): comment = self.data.get_answer_comments_by_assignment(self.assignment)[0] url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid=comment.uuid) draft_comment = self.data.get_answer_comments_by_assignment(self.assignment)[2] draft_url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid=draft_comment.uuid) # test login required rv = self.client.get(url) self.assert401(rv) # test unauthorized user with self.login(self.data.get_unauthorized_instructor().username): rv = self.client.get(url) self.assert403(rv) # test unauthorized user student fetching draft of another student student = self.data.get_extra_student(0) for user_context in [ \ self.login(student.username), \ self.impersonate(self.data.get_authorized_instructor(), student)]: with user_context: rv = self.client.get(draft_url) self.assert403(rv) # test invalid course id with self.login(self.data.get_authorized_instructor().username): invalid_url = self.get_url( course_uuid="999", assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid=comment.uuid) rv = self.client.get(invalid_url) self.assert404(rv) # test invalid answer id invalid_url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid="999", answer_comment_uuid=comment.uuid) rv = self.client.get(invalid_url) self.assert404(rv) # test invalid comment id invalid_url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid="999") rv = self.client.get(invalid_url) self.assert404(rv) # test authorized instructor rv = self.client.get(url) self.assert200(rv) self.assertEqual(comment.content, rv.json['content']) self.assertIn('fullname', rv.json['user']) # test draft rv = self.client.get(draft_url) self.assert200(rv) self.assertEqual(draft_comment.content, rv.json['content']) self.assertTrue(rv.json['draft']) self.assertIn('fullname', rv.json['user']) # test author student = self.data.get_extra_student(0) for user_context in [self.login(student.username), self.impersonate(self.data.get_authorized_instructor(), student)]: with user_context: rv = self.client.get(url) self.assert200(rv) self.assertEqual(comment.content, rv.json['content']) self.assertNotIn('fullname', rv.json['user']) # test draft author student = self.data.get_extra_student(1) for user_context in [self.login(student.username), self.impersonate(self.data.get_authorized_instructor(), student)]: with user_context: rv = self.client.get(draft_url) self.assert200(rv) self.assertEqual(draft_comment.content, rv.json['content']) self.assertTrue(rv.json['draft']) self.assertNotIn('fullname', rv.json['user']) @mock.patch('compair.tasks.lti_outcomes.update_lti_course_grades.run') @mock.patch('compair.tasks.lti_outcomes.update_lti_assignment_grades.run') def test_edit_answer_comment(self, mocked_update_assignment_grades_run, mocked_update_course_grades_run): comment = self.data.get_answer_comments_by_assignment(self.assignment)[0] url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid=comment.uuid ) content = { 'id': comment.uuid, 'content': 'insightful.', 'comment_type': AnswerCommentType.private.value } draft_comment = self.data.get_answer_comments_by_assignment(self.assignment)[2] draft_url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid=draft_comment.uuid) draft_content = { 'id': draft_comment.uuid, 'content': 'insightful.', 'comment_type': AnswerCommentType.private.value, 'draft': True } # test login required rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert401(rv) # test unauthorized user with self.login(self.data.get_unauthorized_instructor().username): rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert403(rv) # test invalid course id with self.login(self.data.get_authorized_instructor().username): invalid_url = self.get_url( course_uuid="999", assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid=comment.uuid) rv = self.client.post(invalid_url, data=json.dumps(content), content_type='application/json') self.assert404(rv) # test invalid answer id invalid_url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid="999", answer_comment_uuid=comment.uuid) rv = self.client.post(invalid_url, data=json.dumps(content), content_type='application/json') self.assert404(rv) # test invalid comment id invalid_url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid="999") rv = self.client.post(invalid_url, data=json.dumps(content), content_type='application/json') self.assert404(rv) # test unmatched comment ids invalid = content.copy() invalid['id'] = self.data.get_answer_comments_by_assignment(self.assignment)[1].uuid rv = self.client.post(url, data=json.dumps(invalid), content_type='application/json') self.assert400(rv) self.assertEqual("Feedback Not Saved", rv.json['title']) self.assertEqual("The feedback's ID does not match the URL, which is required in order to save the feedback.", rv.json['message']) # test empty content empty = content.copy() empty['content'] = '' rv = self.client.post(url, data=json.dumps(empty), content_type='application/json') self.assert400(rv) self.assertEqual("Feedback Not Saved", rv.json['title']) self.assertEqual("Please provide content in the text editor and try saving again.", rv.json['message']) # test empty comment_type empty = content.copy() empty['comment_type'] = '' rv = self.client.post(url, data=json.dumps(empty), content_type='application/json') self.assert400(rv) # test authorized instructor with self.login(self.data.get_authorized_instructor().username): with mail.record_messages() as outbox: rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert200(rv) self.assertEqual(content['content'], rv.json['content']) self.assertIn('fullname', rv.json['user']) self.assertEqual(len(outbox), 0) # test author with self.login(self.data.get_extra_student(0).username): # test student can not change comment to self-eval / eval invalid = content.copy() invalid['comment_type'] = AnswerCommentType.self_evaluation.value rv = self.client.post(url, data=json.dumps(invalid), content_type='application/json') self.assert400(rv) self.assertEqual("Feedback Not Saved", rv.json['title']) self.assertEqual("Feedback type cannot be changed. Please contact support for assistance.", rv.json['message']) invalid = content.copy() invalid['comment_type'] = AnswerCommentType.evaluation.value rv = self.client.post(url, data=json.dumps(invalid), content_type='application/json') self.assert400(rv) self.assertEqual("Feedback Not Saved", rv.json['title']) self.assertEqual("Feedback type cannot be changed. Please contact support for assistance.", rv.json['message']) with mail.record_messages() as outbox: content['content'] = 'I am the author' rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert200(rv) self.assertEqual(content['content'], rv.json['content']) self.assertFalse(rv.json['draft']) self.assertNotIn('fullname', rv.json['user']) self.assertEqual(len(outbox), 0) # ignored setting draft to True when draft is already False with mail.record_messages() as outbox: content['draft'] = True rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert200(rv) self.assertEqual(content['content'], rv.json['content']) self.assertFalse(rv.json['draft']) self.assertNotIn('fullname', rv.json['user']) self.assertEqual(len(outbox), 0) # test author with impersonation student = self.data.get_extra_student(0) with self.impersonate(self.data.get_authorized_instructor(), student): content['content'] = 'I am the author' rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert403(rv) self.assertTrue(rv.json['disabled_by_impersonation']) content['draft'] = True rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert403(rv) self.assertTrue(rv.json['disabled_by_impersonation']) # test draft author with self.login(self.data.get_extra_student(1).username): with mail.record_messages() as outbox: draft_content['content'] = 'I am the author' rv = self.client.post(draft_url, data=json.dumps(draft_content), content_type='application/json') self.assert200(rv) self.assertEqual(draft_content['content'], rv.json['content']) self.assertTrue(rv.json['draft']) self.assertNotIn('fullname', rv.json['user']) self.assertEqual(len(outbox), 0) # can change draft to False when draft is True with mail.record_messages() as outbox: draft_content['draft'] = False rv = self.client.post(draft_url, data=json.dumps(draft_content), content_type='application/json') self.assert200(rv) self.assertFalse(rv.json['draft']) self.assertNotIn('fullname', rv.json['user']) self.assertEqual(len(outbox), 1) self.assertEqual(outbox[0].subject, "New Answer Feedback in "+self.data.get_course().name) self.assertEqual(outbox[0].recipients, [self.answers[self.assignment.id][0].user.email]) # test draft author with impersonation student = self.data.get_extra_student(1) with self.impersonate(self.data.get_authorized_instructor(), student): draft_content['content'] = 'I am the author' rv = self.client.post(draft_url, data=json.dumps(draft_content), content_type='application/json') self.assert403(rv) self.assertTrue(rv.json['disabled_by_impersonation']) # cant change draft to False draft_content['draft'] = False rv = self.client.post(draft_url, data=json.dumps(draft_content), content_type='application/json') self.assert403(rv) self.assertTrue(rv.json['disabled_by_impersonation']) answer = self.answers[self.assignment.id][0] self_evaluation = self.data.create_answer_comment( answer.user, answer, comment_type=AnswerCommentType.self_evaluation, draft=True) self_evaluation_url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=answer.uuid, answer_comment_uuid=self_evaluation.uuid) with self.login(answer.user.username): lti_consumer = self.lti_data.lti_consumer (lti_user_resource_link1, lti_user_resource_link2) = self.lti_data.setup_student_user_resource_links( answer.user, self.course, self.assignment) course_grade = CourseGrade.get_user_course_grade(self.course, answer.user).grade assignment_grade = AssignmentGrade.get_user_assignment_grade(self.assignment, answer.user).grade content = { 'id': self_evaluation.uuid, 'content': 'insightful.', 'comment_type': AnswerCommentType.self_evaluation.value, 'draft': True } # test student can not submit self-eval after self-eval grace period orig_answer_end = self.assignment.answer_end self.assignment.answer_end = datetime.datetime.utcnow() - datetime.timedelta(hours=12) self.assignment.self_eval_start = datetime.datetime.utcnow() - datetime.timedelta(hours=1) self.assignment.self_eval_end = datetime.datetime.utcnow() - datetime.timedelta(minutes=10) db.session.add(self.assignment) db.session.commit() rv = self.client.post(self_evaluation_url, data=json.dumps(content), content_type='application/json') self.assert403(rv) self.assertEqual("Self-Evaluation Not Saved", rv.json['title']) self.assertEqual("Sorry, the self-evaluation deadline has passed and therefore cannot be submitted.", rv.json['message']) self.assignment.answer_end = orig_answer_end self.assignment.self_eval_start = None self.assignment.self_eval_end = None with mail.record_messages() as outbox: self.assignment.answer_end = datetime.datetime.utcnow() - datetime.timedelta(hours=12) rv = self.client.post(self_evaluation_url, data=json.dumps(content), content_type='application/json') self.assert200(rv) self.assertEqual(content['content'], rv.json['content']) self.assertTrue(rv.json['draft']) self.assertNotIn('fullname', rv.json['user']) self.assertEqual(len(outbox), 0) # grades should not change new_course_grade = CourseGrade.get_user_course_grade(self.course, answer.user).grade new_assignment_grade = AssignmentGrade.get_user_assignment_grade(self.assignment, answer.user).grade self.assertEqual(new_course_grade, course_grade) self.assertEqual(new_assignment_grade, assignment_grade) # can change draft to False when draft is True with mail.record_messages() as outbox: content['draft'] = False rv = self.client.post(self_evaluation_url, data=json.dumps(content), content_type='application/json') self.assert200(rv) self.assertFalse(rv.json['draft']) self.assertNotIn('fullname', rv.json['user']) self.assertEqual(len(outbox), 0) # grades should increase new_course_grade = CourseGrade.get_user_course_grade(self.course, answer.user) new_assignment_grade = AssignmentGrade.get_user_assignment_grade(self.assignment, answer.user) self.assertGreater(new_course_grade.grade, course_grade) self.assertGreater(new_assignment_grade.grade, assignment_grade) mocked_update_assignment_grades_run.assert_called_once_with( lti_consumer.id, [(lti_user_resource_link2.lis_result_sourcedid, new_assignment_grade.id)] ) mocked_update_assignment_grades_run.reset_mock() mocked_update_course_grades_run.assert_called_once_with( lti_consumer.id, [(lti_user_resource_link1.lis_result_sourcedid, new_course_grade.id)] ) mocked_update_course_grades_run.reset_mock() # test self-evaluation with impersonation answers = self.answers[self.assignment.id] for answer in [a for a in answers if a.user.system_role == SystemRole.student]: self_evaluation = self.data.create_answer_comment( answer.user, answer, comment_type=AnswerCommentType.self_evaluation, draft=True) self_evaluation_url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=answer.uuid, answer_comment_uuid=self_evaluation.uuid) with self.impersonate(self.data.get_authorized_instructor(), answer.user): content = { 'id': self_evaluation.uuid, 'content': 'insightful.', 'comment_type': AnswerCommentType.self_evaluation.value, 'draft': True } rv = self.client.post(self_evaluation_url, data=json.dumps(content), content_type='application/json') self.assert403(rv) self.assertTrue(rv.json['disabled_by_impersonation']) # attempt to change draft to False content['draft'] = False rv = self.client.post(self_evaluation_url, data=json.dumps(content), content_type='application/json') self.assert403(rv) self.assertTrue(rv.json['disabled_by_impersonation']) @mock.patch('compair.tasks.lti_outcomes.update_lti_course_grades.run') @mock.patch('compair.tasks.lti_outcomes.update_lti_assignment_grades.run') def test_delete_answer_comment(self, mocked_update_assignment_grades_run, mocked_update_course_grades_run): comment = self.data.get_answer_comments_by_assignment(self.assignment)[0] url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid=comment.uuid) # test login required rv = self.client.delete(url) self.assert401(rv) # test unauthorized user with self.login(self.data.get_unauthorized_instructor().username): rv = self.client.delete(url) self.assert403(rv) # test invalid comment id with self.login(self.data.get_authorized_instructor().username): invalid_url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid="999") rv = self.client.delete(invalid_url) self.assert404(rv) # test authorized instructor rv = self.client.delete(url) self.assert200(rv) self.assertEqual(comment.uuid, rv.json['id']) # test author with impersonation student = self.data.get_extra_student(1) with self.impersonate(self.data.get_authorized_instructor(), student): url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid=comment.uuid) rv = self.client.delete(url) self.assert403(rv) self.assertTrue(rv.json['disabled_by_impersonation']) # test author with self.login(self.data.get_extra_student(1).username): comment = self.data.get_answer_comments_by_assignment(self.assignment)[1] url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid, answer_comment_uuid=comment.uuid) rv = self.client.delete(url) self.assert200(rv) self.assertEqual(comment.uuid, rv.json['id']) # test delete self-evaulation with impersonation answers = self.answers[self.assignment.id] for answer in [a for a in answers if a.user.system_role == SystemRole.student]: self_evaluation = self.data.create_answer_comment( answer.user, answer, comment_type=AnswerCommentType.self_evaluation, draft=True) with self.impersonate(self.data.get_authorized_instructor(), answer.user): url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=answer.uuid, answer_comment_uuid=self_evaluation.uuid) rv = self.client.delete(url) self.assert403(rv) self.assertTrue(rv.json['disabled_by_impersonation']) # test delete self-evaulation answer = self.answers[self.assignment.id][0] self_evaluation = self.data.create_answer_comment(answer.user, answer, comment_type=AnswerCommentType.self_evaluation) self.assignment.calculate_grade(answer.user) self.course.calculate_grade(answer.user) lti_consumer = self.lti_data.lti_consumer (lti_user_resource_link1, lti_user_resource_link2) = self.lti_data.setup_student_user_resource_links( answer.user, self.course, self.assignment) with self.login(self.data.get_authorized_instructor().username): course_grade = CourseGrade.get_user_course_grade(self.course, answer.user).grade assignment_grade = AssignmentGrade.get_user_assignment_grade(self.assignment, answer.user).grade url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=answer.uuid, answer_comment_uuid=self_evaluation.uuid) rv = self.client.delete(url) self.assert200(rv) self.assertEqual(self_evaluation.uuid, rv.json['id']) # grades should decrease new_course_grade = CourseGrade.get_user_course_grade(self.course, answer.user) new_assignment_grade = AssignmentGrade.get_user_assignment_grade(self.assignment, answer.user) self.assertLess(new_course_grade.grade, course_grade) self.assertLess(new_assignment_grade.grade, assignment_grade) mocked_update_assignment_grades_run.assert_called_once_with( lti_consumer.id, [(lti_user_resource_link2.lis_result_sourcedid, new_assignment_grade.id)] ) mocked_update_assignment_grades_run.reset_mock() mocked_update_course_grades_run.assert_called_once_with( lti_consumer.id, [(lti_user_resource_link1.lis_result_sourcedid, new_course_grade.id)] ) mocked_update_course_grades_run.reset_mock()
def setUp(self): super(LTIConsumersAPITests, self).setUp() self.data = BasicTestData() self.lti_data = LTITestData()
class AnswerCommentListAPITests(ComPAIRAPITestCase): """ Tests for answer comment list API """ resource = AnswerCommentListAPI api = api def setUp(self): super(AnswerCommentListAPITests, self).setUp() self.data = AnswerCommentsTestData() self.course = self.data.get_course() self.assignments = self.data.get_assignments() self.answers = self.data.get_answers_by_assignment() self.assignment = self.assignments[0] self.assignment.enable_self_evaluation = True db.session.commit() self.assignment.calculate_grades() self.lti_data = LTITestData() def test_get_all_answer_comments(self): url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid) # test login required rv = self.client.get(url) self.assert401(rv) # test unauthorized user with self.login(self.data.get_unauthorized_instructor().username): rv = self.client.get(url) self.assert403(rv) with self.login(self.data.get_authorized_instructor().username): # test invalid answer id invalid_url = self.get_url( course_uuid=self.course.id, assignment_uuid=self.assignment.uuid, answer_uuid="999") rv = self.client.get(invalid_url) self.assert404(rv) # test authorized user rv = self.client.get(url) self.assert200(rv) self.assertEqual(1, len(rv.json)) self.assertEqual( self.data.get_non_draft_answer_comments_by_assignment(self.assignment)[1].content, rv.json[0]['content']) self.assertIn( self.data.get_non_draft_answer_comments_by_assignment(self.assignment)[1].user_fullname, rv.json[0]['user']['fullname']) # test non-owner student of answer access comments student = self.data.get_authorized_student() for user_context in [self.login(student.username), self.impersonate(self.data.get_authorized_instructor(), student)]: with user_context: rv = self.client.get(url) self.assert200(rv) self.assertEqual(0, len(rv.json)) # test owner student of answer access comments student = self.data.get_extra_student(0) for user_context in [ \ self.login(student.username), \ self.impersonate(self.data.get_authorized_instructor(), student)]: with user_context: rv = self.client.get(url) self.assert200(rv) self.assertEqual(1, len(rv.json)) self.assertNotIn('fullname', rv.json[0]['user']) def test_get_list_query_params(self): comment = AnswerCommentsTestData.create_answer_comment( self.data.get_extra_student(0), self.answers[self.assignment.id][0], comment_type=AnswerCommentType.self_evaluation ) draft_comment = AnswerCommentsTestData.create_answer_comment( self.data.get_extra_student(0), self.answers[self.assignment.id][0], comment_type=AnswerCommentType.evaluation, draft=True ) base_params = { 'course_uuid': self.course.uuid, 'assignment_uuid': self.assignment.uuid, } with self.login(self.data.get_authorized_instructor().username): # no answer ids rv = self.client.get(self.get_url(**base_params)) self.assert404(rv) params = dict(base_params, answer_ids=self.answers[self.assignment.id][0].uuid) extra_student2_answer_comment_uuid = self.data.get_answer_comments_by_assignment(self.assignment)[1].uuid rv = self.client.get(self.get_url(**params)) self.assert200(rv) self.assertEqual(2, len(rv.json)) rv = self.client.get(self.get_url(self_evaluation='false', **params)) self.assert200(rv) self.assertEqual(1, len(rv.json)) self.assertEqual(extra_student2_answer_comment_uuid, rv.json[0]['id']) rv = self.client.get(self.get_url(self_evaluation='only', **params)) self.assert200(rv) self.assertEqual(1, len(rv.json)) self.assertEqual(comment.uuid, rv.json[0]['id']) ids = [extra_student2_answer_comment_uuid, comment.uuid] rv = self.client.get(self.get_url(ids=','.join(ids), **base_params)) self.assert200(rv) self.assertEqual(2, len(rv.json)) six.assertCountEqual(self, ids, [c['id'] for c in rv.json]) answer_ids = [answer.uuid for answer in self.answers[self.assignment.id]] params = dict(base_params, answer_ids=','.join(answer_ids)) rv = self.client.get(self.get_url(**params)) self.assert200(rv) self.assertEqual(3, len(rv.json)) rv = self.client.get(self.get_url(self_evaluation='false', **params)) self.assert200(rv) self.assertEqual(2, len(rv.json)) self.assertNotIn(comment.uuid, (c['id'] for c in rv.json)) rv = self.client.get(self.get_url(self_evaluation='only', **params)) self.assert200(rv) self.assertEqual(1, len(rv.json)) self.assertEqual(comment.uuid, rv.json[0]['id']) answer_ids = [answer.uuid for answer in self.answers[self.assignment.id]] params = dict(base_params, answer_ids=','.join(answer_ids), user_ids=self.data.get_extra_student(1).uuid) rv = self.client.get(self.get_url(**params)) self.assert200(rv) self.assertEqual(1, len(rv.json)) # test user_ids filter user_ids = ','.join([self.data.get_extra_student(0).uuid]) rv = self.client.get(self.get_url(user_ids=user_ids, **base_params)) self.assert200(rv) self.assertEqual(2, len(rv.json)) six.assertCountEqual( self, [comment.uuid, self.data.answer_comments_by_assignment[self.assignment.id][0].uuid], [c['id'] for c in rv.json]) student = self.data.get_extra_student(1) for user_context in [ \ self.login(student.username), \ self.impersonate(self.data.get_authorized_instructor(), student)]: with user_context: answer_ids = [answer.uuid for answer in self.answers[self.assignment.id]] params = dict(base_params, answer_ids=','.join(answer_ids), user_ids=self.data.get_extra_student(1).uuid) rv = self.client.get(self.get_url(**params)) self.assert200(rv) self.assertEqual(1, len(rv.json)) # answer is not from the student but comment is answer_ids = [self.answers[self.assignment.id][1].uuid] params = dict(base_params, answer_ids=','.join(answer_ids), user_ids=self.data.get_extra_student(0).uuid) rv = self.client.get(self.get_url(**params)) self.assert200(rv) self.assertEqual(1, len(rv.json)) self.assertEqual(self.data.get_extra_student(0).uuid, rv.json[0]['user_id']) # test drafts student = self.data.get_extra_student(0) for user_context in [self.login(student.username), self.impersonate(self.data.get_authorized_instructor(), student)]: with user_context: params = dict(base_params, user_ids=self.data.get_extra_student(0).uuid) rv = self.client.get(self.get_url(draft='only', **params)) self.assert200(rv) self.assertEqual(1, len(rv.json)) self.assertEqual(draft_comment.uuid, rv.json[0]['id']) rv = self.client.get(self.get_url(draft='false', **params)) self.assert200(rv) self.assertEqual(2, len(rv.json)) rv = self.client.get(self.get_url(draft='true', **params)) self.assert200(rv) self.assertEqual(3, len(rv.json)) self.assertEqual(draft_comment.uuid, rv.json[0]['id']) @mock.patch('compair.tasks.lti_outcomes.update_lti_course_grades.run') @mock.patch('compair.tasks.lti_outcomes.update_lti_assignment_grades.run') def test_create_answer_comment(self, mocked_update_assignment_grades_run, mocked_update_course_grades_run): url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid) content = { 'comment_type': AnswerCommentType.private.value, 'content': 'great answer' } # test login required rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert401(rv) # test unauthorized user with self.login(self.data.get_unauthorized_instructor().username): rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert403(rv) # test invalid course id with self.login(self.data.get_authorized_instructor().username): invalid_url = self.get_url( course_uuid="999", assignment_uuid=self.assignment.uuid, answer_uuid=self.answers[self.assignment.id][0].uuid) rv = self.client.post(invalid_url, data=json.dumps(content), content_type='application/json') self.assert404(rv) # test invalid assignment id invalid_url = self.get_url( course_uuid=self.course.uuid, assignment_uuid="999", answer_uuid=self.answers[self.assignment.id][0].uuid) rv = self.client.post(invalid_url, data=json.dumps(content), content_type='application/json') self.assert404(rv) # test invalid answer id invalid_url = self.get_url( course_uuid=self.course.uuid, assignment_uuid=self.assignment.uuid, answer_uuid="999") rv = self.client.post(invalid_url, data=json.dumps(content), content_type='application/json') self.assert404(rv) # test empty content empty = content.copy() empty['content'] = '' rv = self.client.post(url, data=json.dumps(empty), content_type='application/json') self.assert400(rv) # test empty comment type empty = content.copy() empty['comment_type'] = '' rv = self.client.post(url, data=json.dumps(empty), content_type='application/json') self.assert400(rv) # test authorized user with mail.record_messages() as outbox: rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert200(rv) self.assertEqual(content['content'], rv.json['content']) self.assertFalse(rv.json['draft']) self.assertIn('fullname', rv.json['user']) self.assertEqual(len(outbox), 1) self.assertEqual(outbox[0].subject, "New Answer Feedback in "+self.data.get_course().name) self.assertEqual(outbox[0].recipients, [self.answers[self.assignment.id][0].user.email]) # test authorized user draft with mail.record_messages() as outbox: draft_content = content.copy() draft_content['draft'] = True rv = self.client.post(url, data=json.dumps(draft_content), content_type='application/json') self.assert200(rv) self.assertEqual(content['content'], rv.json['content']) self.assertTrue(rv.json['draft']) self.assertEqual(len(outbox), 0) # test authorized user draft - empty content with mail.record_messages() as outbox: empty = draft_content.copy() empty['content'] = None rv = self.client.post(url, data=json.dumps(empty), content_type='application/json') self.assert200(rv) self.assertEqual(empty['content'], rv.json['content']) self.assertTrue(rv.json['draft']) self.assertEqual(len(outbox), 0) with self.login('root'): with mail.record_messages() as outbox: rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert200(rv) self.assertEqual(len(outbox), 1) self.assertIn('fullname', rv.json['user']) with self.login(self.data.get_authorized_student().username): lti_consumer = self.lti_data.lti_consumer (lti_user_resource_link1, lti_user_resource_link2) = self.lti_data.setup_student_user_resource_links( self.data.get_authorized_student(), self.course, self.assignment) course_grade = CourseGrade.get_user_course_grade(self.course, self.data.get_authorized_student()).grade assignment_grade = AssignmentGrade.get_user_assignment_grade(self.assignment, self.data.get_authorized_student()).grade content = { 'comment_type': AnswerCommentType.self_evaluation.value, 'content': 'great answer' } # test student can not submit self-eval after self-eval grace period orig_answer_end = self.assignment.answer_end self.assignment.answer_end = datetime.datetime.utcnow() - datetime.timedelta(hours=12) self.assignment.self_eval_start = datetime.datetime.utcnow() - datetime.timedelta(hours=1) self.assignment.self_eval_end = datetime.datetime.utcnow() - datetime.timedelta(minutes=10) db.session.add(self.assignment) db.session.commit() rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert403(rv) self.assertEqual("Self-Evaluation Not Saved", rv.json['title']) self.assertEqual("Sorry, the self-evaluation deadline has passed and therefore cannot be submitted.", rv.json['message']) self.assignment.answer_end = orig_answer_end self.assignment.self_eval_start = None self.assignment.self_eval_end = None with mail.record_messages() as outbox: orig_answer_end = self.assignment.answer_end self.assignment.answer_end = datetime.datetime.utcnow() - datetime.timedelta(hours=12) rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert200(rv) self.assertEqual(len(outbox), 0) self.assertNotIn('fullname', rv.json['user']) # grades should increase new_course_grade = CourseGrade.get_user_course_grade(self.course, self.data.get_authorized_student()) new_assignment_grade = AssignmentGrade.get_user_assignment_grade(self.assignment, self.data.get_authorized_student()) self.assertGreater(new_course_grade.grade, course_grade) self.assertGreater(new_assignment_grade.grade, assignment_grade) mocked_update_assignment_grades_run.assert_called_once_with( lti_consumer.id, [(lti_user_resource_link2.lis_result_sourcedid, new_assignment_grade.id)] ) mocked_update_assignment_grades_run.reset_mock() mocked_update_course_grades_run.assert_called_once_with( lti_consumer.id, [(lti_user_resource_link1.lis_result_sourcedid, new_course_grade.id)] ) mocked_update_assignment_grades_run.reset_mock() self.assignment.answer_end = orig_answer_end # test with impersonation student = self.data.get_extra_student(0) with self.impersonate(self.data.get_authorized_instructor(), student): lti_consumer = self.lti_data.lti_consumer (lti_user_resource_link1, lti_user_resource_link2) = self.lti_data.setup_student_user_resource_links( self.data.get_authorized_student(), self.course, self.assignment) course_grade = CourseGrade.get_user_course_grade(self.course, self.data.get_authorized_student()).grade assignment_grade = AssignmentGrade.get_user_assignment_grade(self.assignment, self.data.get_authorized_student()).grade content = { 'comment_type': AnswerCommentType.self_evaluation.value, 'content': 'great answer' } with mail.record_messages() as outbox: rv = self.client.post(url, data=json.dumps(content), content_type='application/json') self.assert403(rv) self.assertTrue(rv.json['disabled_by_impersonation'])
def setUp(self): super(ComPAIRLearningRecordTestCase, self).setUp() self.data = SimpleAssignmentTestData() self.lti_data = LTITestData() self.user = self.data.authorized_student self.setup_session_data(self.user) self.course = self.data.main_course self.lti_context = self.lti_data.create_context( self.lti_data.lti_consumer, compair_course_id=self.course.id, lis_course_offering_sourcedid="sis_course_id", lis_course_section_sourcedid="sis_section_id", ) self.assignment = self.data.assignments[0] self.expected_caliper_course = { '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", }] } } self.expected_caliper_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': self.expected_caliper_course, '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' }], } self.expected_xapi_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' } self.expected_xapi_sis_course = { 'id': 'https://localhost:8888/course/'+self.lti_context.lis_course_offering_sourcedid, 'objectType': 'Activity' } self.expected_xapi_sis_section = { 'id': 'https://localhost:8888/course/'+self.lti_context.lis_course_offering_sourcedid+'/section/'+self.lti_context.lis_course_section_sourcedid, 'objectType': 'Activity' } self.expected_xapi_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' }
class AnswerLearningRecordTests(ComPAIRLearningRecordTestCase): def setUp(self): super(ComPAIRLearningRecordTestCase, self).setUp() self.data = SimpleAnswersTestData() self.lti_data = LTITestData() self.user = self.data.authorized_student self.setup_session_data(self.user) self.course = self.data.main_course self.lti_context = self.lti_data.create_context( self.lti_data.lti_consumer, compair_course_id=self.course.id, lis_course_offering_sourcedid="sis_course_id", lis_course_section_sourcedid="sis_section_id", ) self.assignment = self.data.assignments[0] self.criterion = self.assignment.criteria[0] self.answer = self.data.create_answer(self.assignment, self.user) self.expected_caliper_course = { '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", }] } } self.expected_caliper_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': self.expected_caliper_course, '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' }], } self.expected_caliper_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': self.expected_caliper_assignment, } self.expected_caliper_answer_attempt = { 'assignable': self.expected_caliper_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' } self.expected_caliper_answer = { 'attempt': self.expected_caliper_answer_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': 8, '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 }, } }, } } self.expected_xapi_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' } self.expected_xapi_sis_course = { 'id': 'https://localhost:8888/course/'+self.lti_context.lis_course_offering_sourcedid, 'objectType': 'Activity' } self.expected_xapi_sis_section = { 'id': 'https://localhost:8888/course/'+self.lti_context.lis_course_offering_sourcedid+'/section/'+self.lti_context.lis_course_section_sourcedid, 'objectType': 'Activity' } self.expected_xapi_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' } self.expected_xapi_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' } self.expected_xapi_answer_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' } self.expected_xapi_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' } def test_on_answer_create(self): for draft in [True, False]: self.answer.draft = draft db.session.commit() self.expected_xapi_answer['definition']['extensions']['http://id.tincanapi.com/extension/isDraft'] = draft self.expected_caliper_answer['extensions']['isDraft'] = draft self.expected_caliper_answer['dateModified'] = self.answer.modified.replace(tzinfo=pytz.utc).isoformat() # test without tracking on_answer_create.send( current_app._get_current_object(), event_name=on_answer_create.name, user=self.user, answer=self.answer ) events = self.get_and_clear_caliper_event_log() expected_caliper_events = [{ 'action': 'Completed', 'actor': self.get_compair_caliper_actor(self.user), 'membership': self.get_caliper_membership(self.course, self.user, self.lti_context), 'object': self.expected_caliper_assignment_question, 'generated': self.expected_caliper_answer, 'session': self.get_caliper_session(self.get_compair_caliper_actor(self.user)), 'type': 'AssessmentItemEvent' }, { 'action': 'Submitted', 'actor': self.get_compair_caliper_actor(self.user), 'membership': self.get_caliper_membership(self.course, self.user, self.lti_context), 'object': self.expected_caliper_assignment, 'generated': { 'assignable': self.expected_caliper_assignment, 'assignee': self.get_compair_caliper_actor(self.user), 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/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' }, 'session': self.get_caliper_session(self.get_compair_caliper_actor(self.user)), 'type': 'AssessmentEvent' }] self.assertEqual(len(events), len(expected_caliper_events)) for index, expected_event in enumerate(expected_caliper_events): self.assertEqual(events[index], expected_event) statements = self.get_and_clear_xapi_statement_log() expected_xapi_statements = [{ "actor": self.get_compair_xapi_actor(self.user), "verb": { 'id': 'http://adlnet.gov/expapi/verbs/completed', 'display': {'en-US': 'completed'} }, "object": self.expected_xapi_answer, "context": { 'registration': self.answer.attempt_uuid, 'contextActivities': { 'parent': [self.expected_xapi_assignment_question, self.expected_xapi_answer_attempt], 'grouping': [self.expected_xapi_assignment, self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } }, "result": { 'success': True, 'duration': "PT05M00S", 'completion': not draft, '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(" ")) } } }, { "actor": self.get_compair_xapi_actor(self.user), "verb": { 'id': 'http://activitystrea.ms/schema/1.0/submit', 'display': {'en-US': 'submitted'} }, "object": { 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/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' }, "context": { 'registration': self.answer.attempt_uuid, 'contextActivities': { 'parent': [self.expected_xapi_assignment], 'grouping': [self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } }, "result": { 'success': True, 'completion': not draft } }] self.assertEqual(len(statements), len(expected_xapi_statements)) for index, expected_statement in enumerate(expected_xapi_statements): self.assertEqual(statements[index], expected_statement) def test_on_answer_modified(self): for draft in [True, False]: self.answer.draft = draft db.session.commit() self.expected_xapi_answer['definition']['extensions']['http://id.tincanapi.com/extension/isDraft'] = draft self.expected_caliper_answer['extensions']['isDraft'] = draft self.expected_caliper_answer['dateModified'] = self.answer.modified.replace(tzinfo=pytz.utc).isoformat() # test without tracking on_answer_modified.send( current_app._get_current_object(), event_name=on_answer_modified.name, user=self.user, answer=self.answer ) events = self.get_and_clear_caliper_event_log() expected_caliper_events = [{ 'action': 'Completed', 'actor': self.get_compair_caliper_actor(self.user), 'membership': self.get_caliper_membership(self.course, self.user, self.lti_context), 'object': self.expected_caliper_assignment_question, 'generated': self.expected_caliper_answer, 'session': self.get_caliper_session(self.get_compair_caliper_actor(self.user)), 'type': 'AssessmentItemEvent' }, { 'action': 'Submitted', 'actor': self.get_compair_caliper_actor(self.user), 'membership': self.get_caliper_membership(self.course, self.user, self.lti_context), 'object': self.expected_caliper_assignment, 'generated': { 'assignable': self.expected_caliper_assignment, 'assignee': self.get_compair_caliper_actor(self.user), 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/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' }, 'session': self.get_caliper_session(self.get_compair_caliper_actor(self.user)), 'type': 'AssessmentEvent' }] self.assertEqual(len(events), len(expected_caliper_events)) for index, expected_event in enumerate(expected_caliper_events): self.assertEqual(events[index], expected_event) statements = self.get_and_clear_xapi_statement_log() expected_xapi_statements = [{ "actor": self.get_compair_xapi_actor(self.user), "verb": { 'id': 'http://adlnet.gov/expapi/verbs/completed', 'display': {'en-US': 'completed'} }, "object": self.expected_xapi_answer, "context": { 'registration': self.answer.attempt_uuid, 'contextActivities': { 'parent': [self.expected_xapi_assignment_question, self.expected_xapi_answer_attempt], 'grouping': [self.expected_xapi_assignment, self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } }, "result": { 'success': True, 'duration': "PT05M00S", 'completion': not draft, '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(" ")) } } }, { "actor": self.get_compair_xapi_actor(self.user), "verb": { 'id': 'http://activitystrea.ms/schema/1.0/submit', 'display': {'en-US': 'submitted'} }, "object": { 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/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' }, "context": { 'registration': self.answer.attempt_uuid, 'contextActivities': { 'parent': [self.expected_xapi_assignment], 'grouping': [self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } }, "result": { 'success': True, 'completion': not draft } }] self.assertEqual(len(statements), len(expected_xapi_statements)) for index, expected_statement in enumerate(expected_xapi_statements): self.assertEqual(statements[index], expected_statement) def test_on_answer_delete(self): # send delete on_answer_delete.send( current_app._get_current_object(), event_name=on_answer_delete.name, user=self.user, answer=self.answer ) events = self.get_and_clear_caliper_event_log() expected_caliper_event = { 'action': 'Deleted', 'actor': self.get_compair_caliper_actor(self.user), 'membership': self.get_caliper_membership(self.course, self.user, self.lti_context), 'object': self.expected_caliper_answer, 'session': self.get_caliper_session(self.get_compair_caliper_actor(self.user)), 'type': 'Event' } self.assertEqual(len(events), 1) self.assertEqual(events[0], expected_caliper_event) statements = self.get_and_clear_xapi_statement_log() expected_xapi_statement = { "actor": self.get_compair_xapi_actor(self.user), "verb": { 'id': 'http://activitystrea.ms/schema/1.0/delete', 'display': {'en-US': 'deleted'} }, "object": self.expected_xapi_answer, "context": { 'contextActivities': { 'parent': [self.expected_xapi_assignment_question, self.expected_xapi_answer_attempt], 'grouping': [self.expected_xapi_assignment, self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } }, } self.assertEqual(len(statements), 1) self.assertEqual(statements[0], expected_xapi_statement)
class AccountLearningRecordTests(ComPAIRLearningRecordTestCase): def setUp(self): super(ComPAIRLearningRecordTestCase, self).setUp() self.data = SimpleAnswersTestData() self.auth_data = ThirdPartyAuthTestData() self.lti_data = LTITestData() self.course = self.data.main_course self.assignment = self.data.assignments[0] self.user = self.data.create_user(SystemRole.instructor) self.data.enrol_user(self.user, self.data.get_course(), CourseRole.instructor) self.global_unique_identifier = 'mock_puid_è_global_unique_identifier' def test_actor_accounts(self): user = self.user # test without homepage set # (should use compair actor account) self.app.config[ 'LRS_ACTOR_ACCOUNT_USE_GLOBAL_UNIQUE_IDENTIFIER'] = True self.app.config[ 'LRS_ACTOR_ACCOUNT_GLOBAL_UNIQUE_IDENTIFIER_HOMEPAGE'] = None expected_caliper_actor = self.get_compair_caliper_actor(user) expected_xapi_actor = self.get_compair_xapi_actor(user) on_assignment_modified.send(current_app._get_current_object(), event_name=on_assignment_modified.name, user=user, assignment=self.assignment) events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 1) self.assertEqual(events[0]['actor'], expected_caliper_actor) statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 1) self.assertEqual(statements[0]['actor'], expected_xapi_actor) # test with homepage set and global unique identifier not set # (should use compair actor account) self.app.config[ 'LRS_ACTOR_ACCOUNT_GLOBAL_UNIQUE_IDENTIFIER_HOMEPAGE'] = "http://third.party.homepage" on_assignment_modified.send(current_app._get_current_object(), event_name=on_assignment_modified.name, user=user, assignment=self.assignment) events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 1) self.assertEqual(events[0]['actor'], expected_caliper_actor) statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 1) self.assertEqual(statements[0]['actor'], expected_xapi_actor) # test with homepage set and global unique identifier set user.global_unique_identifier = self.global_unique_identifier db.session.commit() expected_caliper_actor = self.get_unique_identifier_caliper_actor( user, "http://third.party.homepage/", self.global_unique_identifier) expected_xapi_actor = self.get_unique_identifier_xapi_actor( user, "http://third.party.homepage/", self.global_unique_identifier) on_assignment_modified.send(current_app._get_current_object(), event_name=on_assignment_modified.name, user=user, assignment=self.assignment) events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 1) self.assertEqual(events[0]['actor'], expected_caliper_actor) statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 1) self.assertEqual(statements[0]['actor'], expected_xapi_actor) # disabling LRS_ACTOR_ACCOUNT_USE_GLOBAL_UNIQUE_IDENTIFIER should skip checking global unique identifer self.app.config[ 'LRS_ACTOR_ACCOUNT_USE_GLOBAL_UNIQUE_IDENTIFIER'] = False expected_caliper_actor = self.get_compair_caliper_actor(user) expected_xapi_actor = self.get_compair_xapi_actor(user) on_assignment_modified.send(current_app._get_current_object(), event_name=on_assignment_modified.name, user=user, assignment=self.assignment) events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 1) self.assertEqual(events[0]['actor'], expected_caliper_actor) statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 1) self.assertEqual(statements[0]['actor'], expected_xapi_actor) # test adding third party auths & lti user links # NOTE: xapi doesn't really add this extra info to actor since there # isn't anywhere to put it cas_auth = self.auth_data.create_third_party_user( user=user, third_party_type=ThirdPartyType.cas) saml_auth = self.auth_data.create_third_party_user( user=user, third_party_type=ThirdPartyType.saml) lti_user = self.lti_data.create_user( self.lti_data.lti_consumer, SystemRole.instructor, user, ) lti_user.student_number = '1234567890' lti_user.global_unique_identifier = self.global_unique_identifier lti_user.lis_person_sourcedid = 'asdfghjkl' db.session.commit() expected_caliper_actor = self.get_compair_caliper_actor( user, third_party_users=[cas_auth, saml_auth], lti_users=[lti_user]) expected_xapi_actor = self.get_compair_xapi_actor( user, third_party_users=[cas_auth, saml_auth], lti_users=[lti_user]) on_assignment_modified.send(current_app._get_current_object(), event_name=on_assignment_modified.name, user=user, assignment=self.assignment) events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 1) print(events[0]['actor']) self.assertEqual(events[0]['actor'], expected_caliper_actor) statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 1) self.assertEqual(statements[0]['actor'], expected_xapi_actor)
def setUp(self): super(CoursesLTIAPITests, self).setUp() self.data = SimpleAssignmentTestData() self.lti_data = LTITestData()
class ComparisonLearningRecordTests(ComPAIRLearningRecordTestCase): def setUp(self): super(ComPAIRLearningRecordTestCase, self).setUp() self.data = ComparisonTestData() self.lti_data = LTITestData() self.user = self.data.authorized_student self.setup_session_data(self.user) self.course = self.data.main_course self.lti_context = self.lti_data.create_context( self.lti_data.lti_consumer, compair_course_id=self.course.id, lis_course_offering_sourcedid="sis_course_id", lis_course_section_sourcedid="sis_section_id", ) self.assignment = self.data.assignments[0] self.criterion = self.assignment.criteria[0] self.answer1 = self.data.answers[0] self.answer2 = self.data.answers[1] self.example_comparison = ComparisonFactory( assignment=self.assignment, user=self.user, answer1_id=self.answer1.id, answer2_id=self.answer2.id, winner=None, completed=False ) self.example_comparison_criterion = ComparisonCriterionFactory( comparison=self.example_comparison, criterion=self.criterion, winner=WinningAnswer.answer1, ) self.comparison = ComparisonFactory( assignment=self.assignment, user=self.user, answer1_id=self.answer1.id, answer2_id=self.answer2.id, winner=None, completed=False ) self.comparison_criterion = ComparisonCriterionFactory( comparison=self.comparison, criterion=self.criterion, winner=WinningAnswer.answer1, ) db.session.commit() self.expected_caliper_course = { '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", }] } } self.expected_caliper_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': self.expected_caliper_course, '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' }], } self.expected_caliper_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': self.expected_caliper_assignment, } self.expected_caliper_answer1_attempt = { 'assignable': self.expected_caliper_assignment_question, 'assignee': self.get_compair_caliper_actor(self.answer1.user), 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/question/attempt/"+self.answer1.attempt_uuid, 'duration': "PT05M00S", 'startedAtTime': self.answer1.attempt_started.replace(tzinfo=pytz.utc).isoformat(), 'endedAtTime': self.answer1.attempt_ended.replace(tzinfo=pytz.utc).isoformat(), 'type': 'Attempt' } self.expected_caliper_answer1 = { 'attempt': self.expected_caliper_answer1_attempt, 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/answer/"+self.answer1.uuid, 'type': 'Response', 'dateCreated': self.answer1.created.replace(tzinfo=pytz.utc).isoformat(), 'dateModified': self.answer1.modified.replace(tzinfo=pytz.utc).isoformat(), 'extensions': { 'characterCount': len(self.answer1.content), 'content': self.answer1.content, 'isDraft': False, 'wordCount': 8, '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 }, } }, } } self.expected_caliper_answer2_attempt = { 'assignable': self.expected_caliper_assignment_question, 'assignee': self.get_compair_caliper_actor(self.answer2.user), 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/question/attempt/"+self.answer2.attempt_uuid, 'duration': "PT05M00S", 'startedAtTime': self.answer2.attempt_started.replace(tzinfo=pytz.utc).isoformat(), 'endedAtTime': self.answer2.attempt_ended.replace(tzinfo=pytz.utc).isoformat(), 'type': 'Attempt' } self.expected_caliper_answer2 = { 'attempt': self.expected_caliper_answer2_attempt, 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/answer/"+self.answer2.uuid, 'type': 'Response', 'dateCreated': self.answer2.created.replace(tzinfo=pytz.utc).isoformat(), 'dateModified': self.answer2.modified.replace(tzinfo=pytz.utc).isoformat(), 'extensions': { 'characterCount': len(self.answer2.content), 'content': self.answer2.content, 'isDraft': False, 'wordCount': 8, '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 }, } }, } } self.expected_caliper_comparison_question = { 'name': "Assignment comparison #1", 'type': 'AssessmentItem', 'dateCreated': self.assignment.created.replace(tzinfo=pytz.utc).isoformat(), 'dateModified': self.assignment.modified.replace(tzinfo=pytz.utc).isoformat(), 'dateToStartOn': self.assignment.answer_end.replace(tzinfo=pytz.utc).isoformat(), 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/comparison/question/1", 'isPartOf': self.expected_caliper_assignment, } self.expected_xapi_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' } self.expected_xapi_sis_course = { 'id': 'https://localhost:8888/course/'+self.lti_context.lis_course_offering_sourcedid, 'objectType': 'Activity' } self.expected_xapi_sis_section = { 'id': 'https://localhost:8888/course/'+self.lti_context.lis_course_offering_sourcedid+'/section/'+self.lti_context.lis_course_section_sourcedid, 'objectType': 'Activity' } self.expected_xapi_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' } self.expected_xapi_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' } self.expected_xapi_answer1_attempt = { 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/question/attempt/"+self.answer1.attempt_uuid, 'definition': { 'type': 'http://adlnet.gov/expapi/activities/attempt', 'extensions': { 'http://id.tincanapi.com/extension/attempt': { 'duration': "PT05M00S", 'startedAtTime': self.answer1.attempt_started.replace(tzinfo=pytz.utc).isoformat(), 'endedAtTime': self.answer1.attempt_ended.replace(tzinfo=pytz.utc).isoformat(), } } }, 'objectType': 'Activity' } self.expected_xapi_answer1 = { 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/answer/"+self.answer1.uuid, 'definition': { 'type': 'http://id.tincanapi.com/activitytype/solution', 'extensions': { 'http://id.tincanapi.com/extension/isDraft': False } }, 'objectType': 'Activity' } self.expected_xapi_answer2_attempt = { 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/question/attempt/"+self.answer2.attempt_uuid, 'definition': { 'type': 'http://adlnet.gov/expapi/activities/attempt', 'extensions': { 'http://id.tincanapi.com/extension/attempt': { 'duration': "PT05M00S", 'startedAtTime': self.answer2.attempt_started.replace(tzinfo=pytz.utc).isoformat(), 'endedAtTime': self.answer2.attempt_ended.replace(tzinfo=pytz.utc).isoformat(), } } }, 'objectType': 'Activity' } self.expected_xapi_answer2 = { 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/answer/"+self.answer2.uuid, 'definition': { 'type': 'http://id.tincanapi.com/activitytype/solution', 'extensions': { 'http://id.tincanapi.com/extension/isDraft': False } }, 'objectType': 'Activity' } self.expected_xapi_comparison_question = { 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/comparison/question/1", 'definition': { 'type': 'http://adlnet.gov/expapi/activities/question', 'name': {'en-US': "Assignment comparison #1"} }, 'objectType': 'Activity' } def test_on_comparison_update(self): completed_count = 0 for (is_comparison_example, comparison) in [(True, self.example_comparison), (False, self.comparison)]: for completed in [False, True]: comparison.completed = completed comparison.winner = WinningAnswer.answer1 if completed else None db.session.commit() on_comparison_update.send( current_app._get_current_object(), event_name=on_comparison_update.name, user=self.user, assignment=self.assignment, comparison=comparison, is_comparison_example=is_comparison_example ) if completed: completed_count += 1 current_comparison = completed_count if completed else completed_count + 1 self.expected_caliper_comparison_question['name'] = "Assignment comparison #"+str(current_comparison) self.expected_caliper_comparison_question['id'] = "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/comparison/question/"+str(current_comparison) self.expected_xapi_comparison_question['definition']['name']['en-US'] = "Assignment comparison #{}".format(current_comparison) self.expected_xapi_comparison_question['id'] = "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/comparison/question/"+str(current_comparison) expected_caliper_comparison_attempt = { 'assignable': self.expected_caliper_comparison_question, 'assignee': self.get_compair_caliper_actor(self.user), 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/comparison/question/"+str(current_comparison)+"/attempt/"+comparison.attempt_uuid, 'duration': "PT05M00S", 'startedAtTime': comparison.attempt_started.replace(tzinfo=pytz.utc).isoformat(), 'endedAtTime': comparison.attempt_ended.replace(tzinfo=pytz.utc).isoformat(), 'type': 'Attempt' } expected_caliper_comparison = { 'attempt': expected_caliper_comparison_attempt, 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/comparison/"+comparison.uuid, 'type': 'Response', 'dateCreated': comparison.created.replace(tzinfo=pytz.utc).isoformat(), 'dateModified': comparison.modified.replace(tzinfo=pytz.utc).isoformat(), 'extensions': { 'pairingAlgorithm': self.comparison.pairing_algorithm.value, 'winner': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/answer/"+self.answer1.uuid if completed else "Undecided", 'criteria': { "https://localhost:8888/app/criterion/"+self.criterion.uuid: "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/answer/"+self.answer1.uuid, }, "answers": [ "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/answer/"+self.answer1.uuid, "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/answer/"+self.answer2.uuid, ], "completed": completed } } events = self.get_and_clear_caliper_event_log() expected_caliper_events = [{ 'action': 'Completed', 'actor': self.get_compair_caliper_actor(self.user), 'membership': self.get_caliper_membership(self.course, self.user, self.lti_context), 'object': self.expected_caliper_comparison_question, 'generated': expected_caliper_comparison, 'session': self.get_caliper_session(self.get_compair_caliper_actor(self.user)), 'type': 'AssessmentItemEvent' }, { 'action': 'Submitted', 'actor': self.get_compair_caliper_actor(self.user), 'membership': self.get_caliper_membership(self.course, self.user, self.lti_context), 'object': self.expected_caliper_assignment, 'generated': { 'assignable': self.expected_caliper_assignment, 'assignee': self.get_compair_caliper_actor(self.user), 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/attempt/"+comparison.attempt_uuid, 'duration': "PT05M00S", 'startedAtTime': comparison.attempt_started.replace(tzinfo=pytz.utc).isoformat(), 'endedAtTime': comparison.attempt_ended.replace(tzinfo=pytz.utc).isoformat(), 'type': 'Attempt' }, 'session': self.get_caliper_session(self.get_compair_caliper_actor(self.user)), 'type': 'AssessmentEvent' }] if not is_comparison_example and completed: expected_caliper_events.append({ 'action': 'Ranked', 'actor': self.get_compair_caliper_actor(self.user), 'membership': self.get_caliper_membership(self.course, self.user, self.lti_context), 'object': self.expected_caliper_answer1, 'session': self.get_caliper_session(self.get_compair_caliper_actor(self.user)), 'type': 'Event' }) expected_caliper_events.append({ 'action': 'Ranked', 'actor': self.get_compair_caliper_actor(self.user), 'membership': self.get_caliper_membership(self.course, self.user, self.lti_context), 'object': self.expected_caliper_answer2, 'session': self.get_caliper_session(self.get_compair_caliper_actor(self.user)), 'type': 'Event' }) self.assertEqual(len(events), len(expected_caliper_events)) for index, expected_event in enumerate(expected_caliper_events): self.assertEqual(events[index], expected_event) expected_xapi_comparison_attempt = { 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/comparison/question/"+str(current_comparison)+"/attempt/"+comparison.attempt_uuid, 'definition': { 'type': 'http://adlnet.gov/expapi/activities/attempt', 'extensions': { 'http://id.tincanapi.com/extension/attempt': { 'duration': "PT05M00S", 'startedAtTime': comparison.attempt_started.replace(tzinfo=pytz.utc).isoformat(), 'endedAtTime': comparison.attempt_ended.replace(tzinfo=pytz.utc).isoformat(), } } }, 'objectType': 'Activity' } expected_xapi_comparison = { 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/comparison/"+comparison.uuid, 'definition': { 'type': 'http://id.tincanapi.com/activitytype/solution', 'name': { 'en-US': "Assignment comparison" }, 'extensions': { 'http://id.tincanapi.com/extension/completed': completed } }, 'objectType': 'Activity' } statements = self.get_and_clear_xapi_statement_log() expected_xapi_statements = [{ "actor": self.get_compair_xapi_actor(self.user), "verb": { 'id': 'http://adlnet.gov/expapi/verbs/completed', 'display': {'en-US': 'completed'} }, "object": expected_xapi_comparison, "context": { 'registration': comparison.attempt_uuid, 'contextActivities': { 'parent': [self.expected_xapi_comparison_question, self.expected_xapi_answer1, self.expected_xapi_answer2, expected_xapi_comparison_attempt], 'grouping': [self.expected_xapi_assignment, self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } }, "result": { 'success': True, 'duration': "PT05M00S", 'completion': completed, 'response': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/answer/"+self.answer1.uuid if completed else "Undecided", 'extensions': { 'http://xapi.learninganalytics.ubc.ca/extension/criteria': { "https://localhost:8888/app/criterion/"+self.criterion.uuid: "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/answer/"+self.answer1.uuid, } } } }, { "actor": self.get_compair_xapi_actor(self.user), "verb": { 'id': 'http://activitystrea.ms/schema/1.0/submit', 'display': {'en-US': 'submitted'} }, "object": { 'id': "https://localhost:8888/app/course/"+self.course.uuid+"/assignment/"+self.assignment.uuid+"/attempt/"+comparison.attempt_uuid, 'definition': { 'type': 'http://adlnet.gov/expapi/activities/attempt', 'extensions': { 'http://id.tincanapi.com/extension/attempt': { 'duration': "PT05M00S", 'startedAtTime': comparison.attempt_started.replace(tzinfo=pytz.utc).isoformat(), 'endedAtTime': comparison.attempt_ended.replace(tzinfo=pytz.utc).isoformat(), } } }, 'objectType': 'Activity' }, "context": { 'registration': comparison.attempt_uuid, 'contextActivities': { 'parent': [self.expected_xapi_assignment], 'grouping': [self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } }, "result": { 'success': True, 'completion': completed } }] if not is_comparison_example and completed: expected_xapi_statements.append({ "actor": self.get_compair_xapi_actor(self.user), "verb": { 'id': 'https://w3id.org/xapi/dod-isd/verbs/ranked', 'display': {'en-US': 'ranked'} }, "object": self.expected_xapi_answer1, "context": { 'registration': comparison.attempt_uuid, 'contextActivities': { 'parent': [self.expected_xapi_assignment_question, self.expected_xapi_answer1_attempt], 'grouping': [self.expected_xapi_assignment, self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } }, "result": { 'score': {'raw': 5.0}, 'extensions': { 'http://xapi.learninganalytics.ubc.ca/extension/score-details': { '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_xapi_statements.append({ "actor": self.get_compair_xapi_actor(self.user), "verb": { 'id': 'https://w3id.org/xapi/dod-isd/verbs/ranked', 'display': {'en-US': 'ranked'} }, "object": self.expected_xapi_answer2, "context": { 'registration': comparison.attempt_uuid, 'contextActivities': { 'parent': [self.expected_xapi_assignment_question, self.expected_xapi_answer2_attempt], 'grouping': [self.expected_xapi_assignment, self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } }, "result": { 'score': {'raw': 5.0}, 'extensions': { 'http://xapi.learninganalytics.ubc.ca/extension/score-details': { '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 }, } } } } }) self.assertEqual(len(statements), len(expected_xapi_statements)) for index, expected_statement in enumerate(expected_xapi_statements): self.assertEqual(statements[index], expected_statement)
class FileLearningRecordTests(ComPAIRLearningRecordTestCase): def setUp(self): super(ComPAIRLearningRecordTestCase, self).setUp() self.data = SimpleAssignmentTestData() self.lti_data = LTITestData() self.user = self.data.authorized_student self.setup_session_data(self.user) self.course = self.data.main_course self.lti_context = self.lti_data.create_context( self.lti_data.lti_consumer, compair_course_id=self.course.id, lis_course_offering_sourcedid="sis_course_id", lis_course_section_sourcedid="sis_section_id", ) self.assignment = self.data.assignments[0] self.answer = AnswerFactory(assignment=self.assignment, user=self.user) db.session.commit() self.expected_caliper_course = { '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", }] } } self.expected_caliper_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': self.expected_caliper_course, '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' }], } self.expected_caliper_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': self.expected_caliper_assignment, } self.expected_caliper_answer_attempt = { 'assignable': self.expected_caliper_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' } self.expected_caliper_answer = { 'attempt': self.expected_caliper_answer_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': 8, } } self.expected_xapi_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' } self.expected_xapi_sis_course = { 'id': 'https://localhost:8888/course/' + self.lti_context.lis_course_offering_sourcedid, 'objectType': 'Activity' } self.expected_xapi_sis_section = { 'id': 'https://localhost:8888/course/' + self.lti_context.lis_course_offering_sourcedid + '/section/' + self.lti_context.lis_course_section_sourcedid, 'objectType': 'Activity' } self.expected_xapi_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' } self.expected_xapi_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' } self.expected_xapi_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' } def test_on_get_file(self): # not report or attachment on_get_file.send(current_app._get_current_object(), event_name=on_get_file.name, user=self.user, file_type="none", file_name="some_file") events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 0) statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 0) # test report on_get_file.send(current_app._get_current_object(), event_name=on_get_file.name, user=self.user, file_type="report", file_name="some_report.csv") expected_caliper_object = { "id": 'https://localhost:8888/app/report/some_report.csv', "type": "Document", "name": "some_report.csv", "mediaType": "text/csv" } expected_caliper_event = { 'action': 'Viewed', 'actor': self.get_compair_caliper_actor(self.user), 'object': expected_caliper_object, 'session': self.get_caliper_session(self.get_compair_caliper_actor( self.user)), 'type': 'ViewEvent' } events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 1) self.assertEqual(events[0], expected_caliper_event) expected_xapi_object = { 'id': 'https://localhost:8888/app/report/some_report.csv', 'definition': { 'type': 'http://activitystrea.ms/schema/1.0/file', 'name': { 'en-US': 'some_report.csv' }, 'extensions': { 'http://id.tincanapi.com/extension/mime-type': "text/csv" } }, 'objectType': 'Activity' } expected_xapi_verb = { 'id': 'http://id.tincanapi.com/verb/downloaded', 'display': { 'en-US': 'downloaded' } } expected_xapi_context = { 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } } expected_xapi_statement = { "actor": self.get_compair_xapi_actor(self.user), "verb": expected_xapi_verb, "object": expected_xapi_object, "context": expected_xapi_context } statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 1) self.assertEqual(statements[0], expected_xapi_statement) # test attachment without file record on_get_file.send(current_app._get_current_object(), event_name=on_get_file.name, user=self.user, file_type="attachment", file_name="some_file") events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 0) statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 0) # test attachment file record (not linked to assignments or answers) file_record = self.data.create_file(self.user) on_get_file.send(current_app._get_current_object(), event_name=on_get_file.name, user=self.user, file_type="attachment", file_name=file_record.name) events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 0) statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 0) # test attachment file record (assignment) self.assignment.file_id = file_record.id db.session.commit() on_get_file.send(current_app._get_current_object(), event_name=on_get_file.name, user=self.user, file_type="attachment", file_name=file_record.name) expected_caliper_object = { "id": 'https://localhost:8888/app/attachment/' + file_record.name, "type": "Document", "name": file_record.alias, "mediaType": 'application/pdf', "isPartOf": self.expected_caliper_assignment, "dateCreated": file_record.created.replace(tzinfo=pytz.utc).isoformat(), "dateModified": file_record.modified.replace(tzinfo=pytz.utc).isoformat() } self.expected_caliper_assignment[ 'dateModified'] = self.assignment.modified.replace( tzinfo=pytz.utc).isoformat() expected_caliper_event['object'] = expected_caliper_object expected_caliper_event['membership'] = self.get_caliper_membership( self.course, self.user, self.lti_context) events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 1) self.assertEqual(events[0], expected_caliper_event) expected_xapi_object = { 'id': 'https://localhost:8888/app/attachment/' + file_record.name, 'definition': { 'type': 'http://activitystrea.ms/schema/1.0/file', 'name': { 'en-US': file_record.alias }, 'extensions': { 'http://id.tincanapi.com/extension/mime-type': 'application/pdf' } }, 'objectType': 'Activity' } expected_xapi_context = { 'contextActivities': { 'parent': [self.expected_xapi_assignment], 'grouping': [ self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section ] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } } expected_xapi_statement['object'] = expected_xapi_object expected_xapi_statement['context'] = expected_xapi_context statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 1) self.assertEqual(statements[0], expected_xapi_statement) # test attachment file record (answer) self.assignment.file_id = None self.answer.file_id = file_record.id db.session.commit() on_get_file.send(current_app._get_current_object(), event_name=on_get_file.name, user=self.user, file_type="attachment", file_name=file_record.name) self.expected_caliper_assignment_question[ 'dateModified'] = self.assignment.modified.replace( tzinfo=pytz.utc).isoformat() self.expected_caliper_assignment[ 'dateModified'] = self.assignment.modified.replace( tzinfo=pytz.utc).isoformat() self.expected_caliper_answer[ 'dateModified'] = self.answer.modified.replace( tzinfo=pytz.utc).isoformat() expected_caliper_object["isPartOf"] = self.expected_caliper_answer events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 1) self.assertEqual(events[0], expected_caliper_event) expected_xapi_context = { 'contextActivities': { 'parent': [self.expected_xapi_answer], 'grouping': [ self.expected_xapi_assignment_question, self.expected_xapi_assignment, self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section ] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } } expected_xapi_statement['context'] = expected_xapi_context statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 1) self.assertEqual(statements[0], expected_xapi_statement) def test_on_attach_file(self): file_record = self.data.create_file(self.user) self.assignment.file_id = file_record.id db.session.commit() # attache to assignment on_attach_file.send( current_app._get_current_object(), event_name=on_attach_file.name, user=self.user, file=file_record, ) expected_caliper_object = { "id": 'https://localhost:8888/app/attachment/' + file_record.name, "type": "Document", "name": file_record.alias, "mediaType": 'application/pdf', "isPartOf": self.expected_caliper_assignment, "dateCreated": file_record.created.replace(tzinfo=pytz.utc).isoformat(), "dateModified": file_record.modified.replace(tzinfo=pytz.utc).isoformat() } self.expected_caliper_assignment[ 'dateModified'] = self.assignment.modified.replace( tzinfo=pytz.utc).isoformat() expected_caliper_event = { 'action': 'Attached', 'actor': self.get_compair_caliper_actor(self.user), 'membership': self.get_caliper_membership(self.course, self.user, self.lti_context), 'object': expected_caliper_object, 'session': self.get_caliper_session(self.get_compair_caliper_actor( self.user)), 'type': 'Event' } events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 1) self.assertEqual(events[0], expected_caliper_event) expected_xapi_verb = { 'id': 'http://activitystrea.ms/schema/1.0/attach', 'display': { 'en-US': 'attached' } } expected_xapi_object = { 'id': 'https://localhost:8888/app/attachment/' + file_record.name, 'definition': { 'type': 'http://activitystrea.ms/schema/1.0/file', 'name': { 'en-US': file_record.alias }, 'extensions': { 'http://id.tincanapi.com/extension/mime-type': 'application/pdf' } }, 'objectType': 'Activity' } expected_xapi_context = { 'contextActivities': { 'parent': [self.expected_xapi_assignment], 'grouping': [ self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section ] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } } expected_xapi_statement = { "actor": self.get_compair_xapi_actor(self.user), "verb": expected_xapi_verb, "object": expected_xapi_object, "context": expected_xapi_context } statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 1) self.assertEqual(statements[0], expected_xapi_statement) # attach to answer self.assignment.file_id = None self.answer.file_id = file_record.id db.session.commit() on_attach_file.send( current_app._get_current_object(), event_name=on_attach_file.name, user=self.user, file=file_record, ) self.expected_caliper_assignment_question[ 'dateModified'] = self.assignment.modified.replace( tzinfo=pytz.utc).isoformat() self.expected_caliper_assignment[ 'dateModified'] = self.assignment.modified.replace( tzinfo=pytz.utc).isoformat() self.expected_caliper_answer[ 'dateModified'] = self.answer.modified.replace( tzinfo=pytz.utc).isoformat() expected_caliper_object["isPartOf"] = self.expected_caliper_answer events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 1) self.assertEqual(events[0], expected_caliper_event) expected_xapi_context = { 'contextActivities': { 'parent': [self.expected_xapi_answer], 'grouping': [ self.expected_xapi_assignment_question, self.expected_xapi_assignment, self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section ] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } } expected_xapi_statement['context'] = expected_xapi_context statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 1) self.assertEqual(statements[0], expected_xapi_statement) def test_on_detach_file(self): file_record = self.data.create_file(self.user) db.session.commit() # attache to assignment on_detach_file.send(current_app._get_current_object(), event_name=on_detach_file.name, user=self.user, file=file_record, assignment=self.assignment) expected_caliper_object = { "id": 'https://localhost:8888/app/attachment/' + file_record.name, "type": "Document", "name": file_record.alias, "mediaType": 'application/pdf', "isPartOf": self.expected_caliper_assignment, "dateCreated": file_record.created.replace(tzinfo=pytz.utc).isoformat(), "dateModified": file_record.modified.replace(tzinfo=pytz.utc).isoformat() } expected_caliper_event = { 'action': 'Removed', 'actor': self.get_compair_caliper_actor(self.user), 'membership': self.get_caliper_membership(self.course, self.user, self.lti_context), 'object': expected_caliper_object, 'session': self.get_caliper_session(self.get_compair_caliper_actor( self.user)), 'type': 'Event' } events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 1) self.assertEqual(events[0], expected_caliper_event) expected_xapi_verb = { 'id': 'http://activitystrea.ms/schema/1.0/delete', 'display': { 'en-US': 'deleted' } } expected_xapi_object = { 'id': 'https://localhost:8888/app/attachment/' + file_record.name, 'definition': { 'type': 'http://activitystrea.ms/schema/1.0/file', 'name': { 'en-US': file_record.alias }, 'extensions': { 'http://id.tincanapi.com/extension/mime-type': 'application/pdf' } }, 'objectType': 'Activity' } expected_xapi_context = { 'contextActivities': { 'parent': [self.expected_xapi_assignment], 'grouping': [ self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section ] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } } expected_xapi_statement = { "actor": self.get_compair_xapi_actor(self.user), "verb": expected_xapi_verb, "object": expected_xapi_object, "context": expected_xapi_context } statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 1) self.assertEqual(statements[0], expected_xapi_statement) # attach to answer on_detach_file.send(current_app._get_current_object(), event_name=on_detach_file.name, user=self.user, file=file_record, answer=self.answer) expected_caliper_object["isPartOf"] = self.expected_caliper_answer events = self.get_and_clear_caliper_event_log() self.assertEqual(len(events), 1) self.assertEqual(events[0], expected_caliper_event) expected_xapi_context = { 'contextActivities': { 'parent': [self.expected_xapi_answer], 'grouping': [ self.expected_xapi_assignment_question, self.expected_xapi_assignment, self.expected_xapi_course, self.expected_xapi_sis_course, self.expected_xapi_sis_section ] }, 'extensions': { 'http://id.tincanapi.com/extension/browser-info': {}, 'http://id.tincanapi.com/extension/session-info': self.get_xapi_session_info() } } expected_xapi_statement['context'] = expected_xapi_context statements = self.get_and_clear_xapi_statement_log() self.assertEqual(len(statements), 1) self.assertEqual(statements[0], expected_xapi_statement)
class ComparisonAPITests(ComPAIRAPITestCase): def setUp(self): super(ComparisonAPITests, self).setUp() self.data = ComparisonTestData() self.course = self.data.get_course() self.assignment = self.data.get_assignments()[0] self.base_url = self._build_url(self.course.uuid, self.assignment.uuid) self.lti_data = LTITestData() secondary_criterion = self.data.create_criterion(self.data.authorized_instructor) AssignmentCriterionFactory(criterion=secondary_criterion, assignment=self.assignment) db.session.commit() def _build_url(self, course_uuid, assignment_uuid, tail=""): url = '/api/courses/' + course_uuid + '/assignments/' + assignment_uuid + '/comparisons' + tail return url def _build_comparison_submit(self, winner_uuid, draft=False): submit = { 'comparisons': [ { 'criterion_id': self.assignment.criteria[0].uuid, 'winner_id': winner_uuid, 'draft': draft }, { 'criterion_id': self.assignment.criteria[1].uuid, 'winner_id': winner_uuid, 'draft': draft } ] } return submit def test_get_answer_pair_access_control(self): # test login required rv = self.client.get(self.base_url) self.assert401(rv) # test deny access to unenroled users with self.login(self.data.get_unauthorized_student().username): rv = self.client.get(self.base_url) self.assert403(rv) with self.login(self.data.get_unauthorized_instructor().username): rv = self.client.get(self.base_url) self.assert403(rv) # enroled user from this point on with self.login(self.data.get_authorized_student().username): # test non-existent course rv = self.client.get(self._build_url("9993929", self.assignment.uuid)) self.assert404(rv) # test non-existent assignment rv = self.client.get(self._build_url(self.course.uuid, "23902390")) self.assert404(rv) # no comparisons has been entered yet, assignment is not in comparing period rv = self.client.get(self._build_url( self.course.uuid, self.data.get_assignment_in_answer_period().uuid)) self.assert403(rv) def test_submit_comparison_access_control(self): # test login required rv = self.client.post( self.base_url, data=json.dumps({}), content_type='application/json') self.assert401(rv) # establish expected data by first getting an answer pair with self.login(self.data.get_authorized_student().username): rv = self.client.get(self.base_url) self.assert200(rv) # expected_comparisons = rv.json comparison_submit = self._build_comparison_submit(rv.json['objects'][0]['answer1_id']) # test deny access to unenroled users with self.login(self.data.get_unauthorized_student().username): rv = self.client.post( self.base_url, data=json.dumps(comparison_submit), content_type='application/json') self.assert403(rv) with self.login(self.data.get_unauthorized_instructor().username): rv = self.client.post( self.base_url, data=json.dumps(comparison_submit), content_type='application/json') self.assert403(rv) # authorized user from this point with self.login(self.data.get_authorized_student().username): # test non-existent course rv = self.client.post( self._build_url("9999999", self.assignment.uuid), data=json.dumps(comparison_submit), content_type='application/json') self.assert404(rv) # test non-existent assignment rv = self.client.post( self._build_url(self.course.uuid, "9999999"), data=json.dumps(comparison_submit), content_type='application/json') self.assert404(rv) # test reject missing criteria faulty_comparisons = copy.deepcopy(comparison_submit) faulty_comparisons['comparisons'] = [] rv = self.client.post( self.base_url, data=json.dumps(faulty_comparisons), content_type='application/json') self.assert400(rv) # test reject missing course criteria id faulty_comparisons = copy.deepcopy(comparison_submit) del faulty_comparisons['comparisons'][0]['criterion_id'] rv = self.client.post( self.base_url, data=json.dumps(faulty_comparisons), content_type='application/json') self.assert400(rv) # test invalid criterion id faulty_comparisons = copy.deepcopy(comparison_submit) faulty_comparisons['comparisons'][0]['criterion_id'] = 3930230 rv = self.client.post( self.base_url, data=json.dumps(faulty_comparisons), content_type='application/json') self.assert400(rv) # test invalid winner id faulty_comparisons = copy.deepcopy(comparison_submit) faulty_comparisons['comparisons'][0]['winner_id'] = 2382301 rv = self.client.post( self.base_url, data=json.dumps(faulty_comparisons), content_type='application/json') self.assert400(rv) # test past grace period self.assignment.compare_start = datetime.datetime.utcnow() - datetime.timedelta(days=7) self.assignment.compare_end = datetime.datetime.utcnow() - datetime.timedelta(minutes=2) db.session.add(self.assignment) db.session.commit() ok_comparisons = copy.deepcopy(comparison_submit) rv = self.client.post( self.base_url, data=json.dumps(ok_comparisons), content_type='application/json') self.assert403(rv) self.assertEqual("Assignment comparison deadline has passed.", rv.json['error']) # test within grace period self.assignment.compare_start = datetime.datetime.utcnow() - datetime.timedelta(days=7) self.assignment.compare_end = datetime.datetime.utcnow() - datetime.timedelta(seconds=15) db.session.add(self.assignment) db.session.commit() ok_comparisons = copy.deepcopy(comparison_submit) rv = self.client.post( self.base_url, data=json.dumps(ok_comparisons), content_type='application/json') self.assert200(rv) self.assignment.educators_can_compare = False db.session.commit() # instructors can access with self.login(self.data.get_authorized_instructor().username): rv = self.client.get(self.base_url) self.assert403(rv) self.assignment.educators_can_compare = True db.session.commit() rv = self.client.get(self.base_url) self.assert200(rv) # expected_comparisons = rv.json comparison_submit = self._build_comparison_submit(rv.json['objects'][0]['answer1_id']) ok_comparisons = copy.deepcopy(comparison_submit) rv = self.client.post( self.base_url, data=json.dumps(ok_comparisons), content_type='application/json') self.assert200(rv) self.assignment.educators_can_compare = False db.session.commit() # ta can access with self.login(self.data.get_authorized_ta().username): rv = self.client.get(self.base_url) self.assert403(rv) self.assignment.educators_can_compare = True db.session.commit() rv = self.client.get(self.base_url) self.assert200(rv) # expected_comparisons = rv.json comparison_submit = self._build_comparison_submit(rv.json['objects'][0]['answer1_id']) ok_comparisons = copy.deepcopy(comparison_submit) rv = self.client.post( self.base_url, data=json.dumps(ok_comparisons), content_type='application/json') self.assert200(rv) @mock.patch('compair.tasks.lti_outcomes.update_lti_course_grades.run') @mock.patch('compair.tasks.lti_outcomes.update_lti_assignment_grades.run') def test_get_and_submit_comparison(self, mocked_update_assignment_grades_run, mocked_update_course_grades_run): lti_consumer = self.lti_data.lti_consumer (lti_user_resource_link1, lti_user_resource_link2) = self.lti_data.setup_student_user_resource_links( self.data.get_authorized_student(), self.course, self.assignment) users = [self.data.get_authorized_student(), self.data.get_authorized_instructor(), self.data.get_authorized_ta()] for user in users: compared_answer_uuids = set() valid_answer_uuids = set() for answer in self.data.get_student_answers(): if answer.assignment.id == self.assignment.id and answer.user_id != user.id: valid_answer_uuids.add(answer.uuid) if user.id == self.data.get_authorized_student().id: for comparison_example in self.data.comparisons_examples: if comparison_example.assignment_id == self.assignment.id: valid_answer_uuids.add(comparison_example.answer1_uuid) valid_answer_uuids.add(comparison_example.answer2_uuid) with self.login(user.username): if user.id in [self.data.get_authorized_instructor().id, self.data.get_authorized_ta().id]: self.assignment.educators_can_compare = False db.session.commit() # cannot compare answers unless educators_can_compare is set for assignment rv = self.client.get(self.base_url) self.assert403(rv) self.assignment.educators_can_compare = True db.session.commit() current = 0 while len(valid_answer_uuids - compared_answer_uuids) > 0: current += 1 if user.id == self.data.get_authorized_student().id: course_grade = CourseGrade.get_user_course_grade(self.course, user).grade assignment_grade = AssignmentGrade.get_user_assignment_grade(self.assignment, user).grade # establish expected data by first getting an answer pair rv = self.client.get(self.base_url) self.assert200(rv) actual_answer1_uuid = rv.json['objects'][0]['answer1_id'] actual_answer2_uuid = rv.json['objects'][0]['answer2_id'] self.assertIn(actual_answer1_uuid, valid_answer_uuids) self.assertIn(actual_answer2_uuid, valid_answer_uuids) self.assertNotEqual(actual_answer1_uuid, actual_answer2_uuid) self.assertTrue(rv.json['new_pair']) self.assertEqual(rv.json['current'], current) # fetch again rv = self.client.get(self.base_url) self.assert200(rv) expected_comparisons = rv.json self.assertEqual(actual_answer1_uuid, rv.json['objects'][0]['answer1_id']) self.assertEqual(actual_answer2_uuid, rv.json['objects'][0]['answer2_id']) self.assertFalse(rv.json['new_pair']) self.assertEqual(rv.json['current'], current) # test draft post comparison_submit = self._build_comparison_submit(rv.json['objects'][0]['answer1_id'], True) rv = self.client.post( self.base_url, data=json.dumps(comparison_submit), content_type='application/json') self.assert200(rv) actual_comparisons = rv.json['objects'] self._validate_comparison_submit(comparison_submit, actual_comparisons, expected_comparisons) # test draft post (no answer id) comparison_submit = self._build_comparison_submit(None) rv = self.client.post( self.base_url, data=json.dumps(comparison_submit), content_type='application/json') self.assert200(rv) actual_comparisons = rv.json['objects'] self._validate_comparison_submit(comparison_submit, actual_comparisons, expected_comparisons) # test normal post comparison_submit = self._build_comparison_submit(rv.json['objects'][0]['answer1_id']) rv = self.client.post( self.base_url, data=json.dumps(comparison_submit), content_type='application/json') self.assert200(rv) actual_comparisons = rv.json['objects'] compared_answer_uuids.add(actual_comparisons[0]['answer1_id']) compared_answer_uuids.add(actual_comparisons[0]['answer2_id']) self._validate_comparison_submit(comparison_submit, actual_comparisons, expected_comparisons) # grades should increase for every comparison if user.id == self.data.get_authorized_student().id: new_course_grade = CourseGrade.get_user_course_grade(self.course, user) new_assignment_grade = AssignmentGrade.get_user_assignment_grade(self.assignment, user) self.assertGreater(new_course_grade.grade, course_grade) self.assertGreater(new_assignment_grade.grade, assignment_grade) mocked_update_course_grades_run.assert_called_once_with( lti_consumer.id, [(lti_user_resource_link1.lis_result_sourcedid, new_course_grade.id)] ) mocked_update_course_grades_run.reset_mock() mocked_update_assignment_grades_run.assert_called_once_with( lti_consumer.id, [(lti_user_resource_link2.lis_result_sourcedid, new_assignment_grade.id)] ) mocked_update_assignment_grades_run.reset_mock() else: new_course_grade = CourseGrade.get_user_course_grade(self.course, user) new_assignment_grade = AssignmentGrade.get_user_assignment_grade(self.assignment, user) self.assertIsNone(new_course_grade) self.assertIsNone(new_assignment_grade) mocked_update_assignment_grades_run.assert_not_called() mocked_update_course_grades_run.assert_not_called() # Resubmit of same comparison should fail rv = self.client.post( self.base_url, data=json.dumps(comparison_submit), content_type='application/json') self.assert400(rv) # all answers has been compared by the user, errors out when trying to get another pair rv = self.client.get(self.base_url) self.assert400(rv) def _validate_comparison_submit(self, comparison_submit, actual_comparisons, expected_comparisons): self.assertEqual( len(actual_comparisons), len(comparison_submit['comparisons']), "The number of comparisons saved does not match the number sent") for actual_comparison in actual_comparisons: self.assertEqual( expected_comparisons['objects'][0]['answer1_id'], actual_comparison['answer1_id'], "Expected and actual comparison answer1 id did not match") self.assertEqual( expected_comparisons['objects'][0]['answer2_id'], actual_comparison['answer2_id'], "Expected and actual comparison answer2 id did not match") found_comparison = False for expected_comparison in comparison_submit['comparisons']: if expected_comparison['criterion_id'] != \ actual_comparison['criterion_id']: continue self.assertEqual( expected_comparison['winner_id'], actual_comparison['winner_id'], "Expected and actual winner answer id did not match.") found_comparison = True self.assertTrue( found_comparison, "Actual comparison received contains a comparison that was not sent.") def _submit_all_possible_comparisons_for_user(self, user_id): example_winner_ids = [] example_loser_ids = [] for comparison_example in self.data.comparisons_examples: if comparison_example.assignment_id == self.assignment.id: comparisons = Comparison.create_new_comparison_set(self.assignment.id, user_id, False) self.assertEqual(comparisons[0].answer1_id, comparison_example.answer1_id) self.assertEqual(comparisons[0].answer2_id, comparison_example.answer2_id) min_id = min([comparisons[0].answer1_id, comparisons[0].answer2_id]) max_id = max([comparisons[0].answer1_id, comparisons[0].answer2_id]) example_winner_ids.append(min_id) example_loser_ids.append(max_id) for comparison in comparisons: comparison.completed = True comparison.winner_id = min_id db.session.add(comparison) db.session.commit() # self.login(username) # calculate number of comparisons to do before user has compared all the pairs it can num_eligible_answers = 0 # need to minus one to exclude the logged in user's own answer for answer in self.data.get_student_answers(): if answer.assignment_id == self.assignment.id and answer.user_id != user_id: num_eligible_answers += 1 # n - 1 possible pairs before all answers have been compared num_possible_comparisons = num_eligible_answers - 1 winner_ids = [] loser_ids = [] for i in range(num_possible_comparisons): comparisons = Comparison.create_new_comparison_set(self.assignment.id, user_id, False) answer1_id = comparisons[0].answer1_id answer2_id = comparisons[0].answer2_id min_id = min([answer1_id, answer2_id]) max_id = max([answer1_id, answer2_id]) winner_ids.append(min_id) loser_ids.append(max_id) for comparison in comparisons: comparison.completed = True comparison.winner_id = min_id db.session.add(comparison) db.session.commit() Comparison.calculate_scores(self.assignment.id) return { 'comparisons': { 'winners': winner_ids, 'losers': loser_ids }, 'comparison_examples': { 'winners': example_winner_ids, 'losers': example_loser_ids } } @mock.patch('random.shuffle') def test_score_calculation(self, mock_shuffle): """ This is just a rough check on whether score calculations are correct. Answers that has more wins should have the highest scores. """ # Make sure all answers are compared first comparisons_auth = self._submit_all_possible_comparisons_for_user( self.data.get_authorized_student().id) comparisons_secondary = self._submit_all_possible_comparisons_for_user( self.data.get_secondary_authorized_student().id) loser_ids = comparisons_auth['comparisons']['losers'] + comparisons_secondary['comparisons']['losers'] winner_ids = comparisons_auth['comparisons']['winners'] + comparisons_secondary['comparisons']['winners'] # Count the number of wins each answer has had num_wins_by_id = {} for loser_id in loser_ids: num_wins_by_id[loser_id] = num_wins_by_id.setdefault(loser_id, 0) for winner_id in winner_ids: num_wins = num_wins_by_id.setdefault(winner_id, 0) num_wins_by_id[winner_id] = num_wins + 1 # Get the actual score calculated for each answer answers = self.data.get_student_answers() answer_scores = {} for answer in answers: if answer.assignment.id == self.assignment.id: answer_scores[answer.id] = answer.scores[0].score # Check that ranking by score and by wins match, this only works for low number of # comparisons sorted_expect_ranking = sorted(num_wins_by_id.items(), key=operator.itemgetter(0), reverse=True) sorted_expect_ranking = sorted(sorted_expect_ranking, key=operator.itemgetter(1)) expected_ranking_by_wins = [answer_id for (answer_id, wins) in sorted_expect_ranking] sorted_actual_ranking = sorted(answer_scores.items(), key=operator.itemgetter(1)) actual_ranking_by_scores = [answer_id for (answer_id, score) in sorted_actual_ranking] self.assertSequenceEqual(actual_ranking_by_scores, expected_ranking_by_wins) def test_comparison_count_matched_pairing(self): # Make sure all answers are compared first answer_ids = self._submit_all_possible_comparisons_for_user( self.data.get_authorized_student().id) answer_ids2 = self._submit_all_possible_comparisons_for_user( self.data.get_secondary_authorized_student().id) compared_ids = \ answer_ids['comparisons']['winners'] + answer_ids2['comparisons']['winners'] + \ answer_ids['comparisons']['losers'] + answer_ids2['comparisons']['losers'] + \ answer_ids['comparison_examples']['winners'] + answer_ids2['comparison_examples']['winners'] + \ answer_ids['comparison_examples']['losers'] + answer_ids2['comparison_examples']['losers'] # Just a simple test for now, make sure that answers with the smaller number of # comparisons are matched up with each other # Count number of comparisons done for each answer num_comp_by_id = {} for answer_id in compared_ids: num_comp = num_comp_by_id.setdefault(answer_id, 0) num_comp_by_id[answer_id] = num_comp + 1 comp_groups = {} for answerId in num_comp_by_id: count = num_comp_by_id[answerId] comp_groups.setdefault(count, []) comp_groups[count].append(answerId) counts = sorted(comp_groups) # get the answerIds with the lowest count of comparisons possible_answer_ids = comp_groups[counts[0]] if len(possible_answer_ids) < 2: # if the lowest count group does not have enough to create a pair - add the next group possible_answer_ids += comp_groups[counts[1]] # Check that the 2 answers with 1 win gets returned with self.login(self.data.get_authorized_student_with_no_answers().username): rv = self.client.get(self.base_url) self.assert200(rv) answer1 = Answer.query.filter_by(uuid=rv.json['objects'][0]['answer1_id']).first() answer2 = Answer.query.filter_by(uuid=rv.json['objects'][0]['answer2_id']).first() self.assertIsNotNone(answer1) self.assertIsNotNone(answer2) self.assertIn(answer1.id, possible_answer_ids) self.assertIn(answer2.id, possible_answer_ids)
def setUp(self): super(ComPAIRLearningRecordTestCase, self).setUp() self.data = SimpleAnswersTestData() self.lti_data = LTITestData() self.user = self.data.authorized_student self.setup_session_data(self.user) self.course = self.data.main_course self.lti_context = self.lti_data.create_context( self.lti_data.lti_consumer, compair_course_id=self.course.id, lis_course_offering_sourcedid="sis_course_id", lis_course_section_sourcedid="sis_section_id", ) self.assignment = self.data.assignments[0] self.criterion = self.assignment.criteria[0] self.answer = self.data.create_answer(self.assignment, self.user) self.expected_caliper_course = { '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", }] } } self.expected_caliper_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': self.expected_caliper_course, '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' }], } self.expected_caliper_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': self.expected_caliper_assignment, } self.expected_caliper_answer_attempt = { 'assignable': self.expected_caliper_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' } self.expected_caliper_answer = { 'attempt': self.expected_caliper_answer_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': 8, '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 }, } }, } } self.expected_xapi_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' } self.expected_xapi_sis_course = { 'id': 'https://localhost:8888/course/' + self.lti_context.lis_course_offering_sourcedid, 'objectType': 'Activity' } self.expected_xapi_sis_section = { 'id': 'https://localhost:8888/course/' + self.lti_context.lis_course_offering_sourcedid + '/section/' + self.lti_context.lis_course_section_sourcedid, 'objectType': 'Activity' } self.expected_xapi_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' } self.expected_xapi_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' } self.expected_xapi_answer_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' } self.expected_xapi_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' }
class ComparisonAPITests(ComPAIRAPITestCase): def setUp(self): super(ComparisonAPITests, self).setUp() self.data = ComparisonTestData() self.course = self.data.get_course() self.assignment = self.data.get_assignments()[0] self.base_url = self._build_url(self.course.uuid, self.assignment.uuid) self.lti_data = LTITestData() secondary_criterion = self.data.create_criterion( self.data.authorized_instructor) AssignmentCriterionFactory(criterion=secondary_criterion, assignment=self.assignment) db.session.commit() def _build_url(self, course_uuid, assignment_uuid, tail=""): url = '/api/courses/' + course_uuid + '/assignments/' + assignment_uuid + '/comparisons' + tail return url def _build_comparison_submit(self, winner, draft=False): submit = {'comparison_criteria': [], 'draft': draft} for criterion in self.assignment.criteria: submit['comparison_criteria'].append({ 'criterion_id': criterion.uuid, 'winner': winner, 'content': None }) return submit def test_get_answer_pair_access_control(self): # test login required rv = self.client.get(self.base_url) self.assert401(rv) # test deny access to unenroled users with self.login(self.data.get_unauthorized_student().username): rv = self.client.get(self.base_url) self.assert403(rv) with self.login(self.data.get_unauthorized_instructor().username): rv = self.client.get(self.base_url) self.assert403(rv) # enroled user from this point on with self.login(self.data.get_authorized_student().username): # test non-existent course rv = self.client.get( self._build_url("9993929", self.assignment.uuid)) self.assert404(rv) # test non-existent assignment rv = self.client.get(self._build_url(self.course.uuid, "23902390")) self.assert404(rv) # no comparisons has been entered yet, assignment is not in comparing period rv = self.client.get( self._build_url( self.course.uuid, self.data.get_assignment_in_answer_period().uuid)) self.assert403(rv) def test_submit_comparison_access_control(self): # test login required rv = self.client.post(self.base_url, data=json.dumps({}), content_type='application/json') self.assert401(rv) # establish expected data by first getting an answer pair with self.login(self.data.get_authorized_student().username): rv = self.client.get(self.base_url) self.assert200(rv) # expected_comparisons = rv.json comparison_submit = self._build_comparison_submit( WinningAnswer.answer1.value) # test deny access to unenroled users with self.login(self.data.get_unauthorized_student().username): rv = self.client.post(self.base_url, data=json.dumps(comparison_submit), content_type='application/json') self.assert403(rv) with self.login(self.data.get_unauthorized_instructor().username): rv = self.client.post(self.base_url, data=json.dumps(comparison_submit), content_type='application/json') self.assert403(rv) # authorized user from this point with self.login(self.data.get_authorized_student().username): # test non-existent course rv = self.client.post(self._build_url("9999999", self.assignment.uuid), data=json.dumps(comparison_submit), content_type='application/json') self.assert404(rv) # test non-existent assignment rv = self.client.post(self._build_url(self.course.uuid, "9999999"), data=json.dumps(comparison_submit), content_type='application/json') self.assert404(rv) # test reject missing criteria faulty_comparisons = copy.deepcopy(comparison_submit) faulty_comparisons['comparison_criteria'] = [] rv = self.client.post(self.base_url, data=json.dumps(faulty_comparisons), content_type='application/json') self.assert400(rv) # test reject missing course criteria id faulty_comparisons = copy.deepcopy(comparison_submit) del faulty_comparisons['comparison_criteria'][0]['criterion_id'] rv = self.client.post(self.base_url, data=json.dumps(faulty_comparisons), content_type='application/json') self.assert400(rv) # test invalid criterion id faulty_comparisons = copy.deepcopy(comparison_submit) faulty_comparisons['comparison_criteria'][0][ 'criterion_id'] = 3930230 rv = self.client.post(self.base_url, data=json.dumps(faulty_comparisons), content_type='application/json') self.assert400(rv) # test invalid winner faulty_comparisons = copy.deepcopy(comparison_submit) faulty_comparisons['comparison_criteria'][0]['winner'] = "2382301" rv = self.client.post(self.base_url, data=json.dumps(faulty_comparisons), content_type='application/json') self.assert400(rv) # test past grace period self.assignment.compare_start = datetime.datetime.utcnow( ) - datetime.timedelta(days=7) self.assignment.compare_end = datetime.datetime.utcnow( ) - datetime.timedelta(minutes=2) db.session.add(self.assignment) db.session.commit() ok_comparisons = copy.deepcopy(comparison_submit) rv = self.client.post(self.base_url, data=json.dumps(ok_comparisons), content_type='application/json') self.assert403(rv) self.assertEqual("Comparison Not Saved", rv.json['title']) self.assertEqual( "Sorry, the comparison deadline has passed. No comparisons can be done after the deadline.", rv.json['message']) # test within grace period self.assignment.compare_start = datetime.datetime.utcnow( ) - datetime.timedelta(days=7) self.assignment.compare_end = datetime.datetime.utcnow( ) - datetime.timedelta(seconds=15) db.session.add(self.assignment) db.session.commit() ok_comparisons = copy.deepcopy(comparison_submit) rv = self.client.post(self.base_url, data=json.dumps(ok_comparisons), content_type='application/json') self.assert200(rv) self.assignment.educators_can_compare = False db.session.commit() # instructors can access with self.login(self.data.get_authorized_instructor().username): rv = self.client.get(self.base_url) self.assert403(rv) self.assignment.educators_can_compare = True db.session.commit() rv = self.client.get(self.base_url) self.assert200(rv) # expected_comparisons = rv.json comparison_submit = self._build_comparison_submit( WinningAnswer.answer1.value) ok_comparisons = copy.deepcopy(comparison_submit) rv = self.client.post(self.base_url, data=json.dumps(ok_comparisons), content_type='application/json') self.assert200(rv) self.assignment.educators_can_compare = False db.session.commit() # ta can access with self.login(self.data.get_authorized_ta().username): rv = self.client.get(self.base_url) self.assert403(rv) self.assignment.educators_can_compare = True db.session.commit() rv = self.client.get(self.base_url) self.assert200(rv) # expected_comparisons = rv.json comparison_submit = self._build_comparison_submit( WinningAnswer.answer1.value) ok_comparisons = copy.deepcopy(comparison_submit) rv = self.client.post(self.base_url, data=json.dumps(ok_comparisons), content_type='application/json') self.assert200(rv) @mock.patch('compair.tasks.lti_outcomes.update_lti_course_grades.run') @mock.patch('compair.tasks.lti_outcomes.update_lti_assignment_grades.run') def test_get_and_submit_comparison(self, mocked_update_assignment_grades_run, mocked_update_course_grades_run): lti_consumer = self.lti_data.lti_consumer (lti_user_resource_link1, lti_user_resource_link2 ) = self.lti_data.setup_student_user_resource_links( self.data.get_authorized_student(), self.course, self.assignment) users = [ self.data.get_authorized_student(), self.data.get_authorized_instructor(), self.data.get_authorized_ta() ] for user in users: max_comparisons = 0 other_student_answers = 0 valid_answer_uuids = set() for answer in self.data.get_student_answers(): if answer.assignment.id == self.assignment.id and answer.user_id != user.id: other_student_answers += 1 valid_answer_uuids.add(answer.uuid) max_comparisons = int(other_student_answers * (other_student_answers - 1) / 2) if user.id == self.data.get_authorized_student().id: for comparison_example in self.data.comparisons_examples: if comparison_example.assignment_id == self.assignment.id: max_comparisons += 1 valid_answer_uuids.add(comparison_example.answer1_uuid) valid_answer_uuids.add(comparison_example.answer2_uuid) with self.login(user.username): if user.id in [ self.data.get_authorized_instructor().id, self.data.get_authorized_ta().id ]: self.assignment.educators_can_compare = False db.session.commit() # cannot compare answers unless educators_can_compare is set for assignment rv = self.client.get(self.base_url) self.assert403(rv) self.assignment.educators_can_compare = True db.session.commit() current = 0 while current < max_comparisons: current += 1 if user.id == self.data.get_authorized_student().id: course_grade = CourseGrade.get_user_course_grade( self.course, user).grade assignment_grade = AssignmentGrade.get_user_assignment_grade( self.assignment, user).grade # establish expected data by first getting an answer pair rv = self.client.get(self.base_url) self.assert200(rv) actual_answer1_uuid = rv.json['comparison']['answer1_id'] actual_answer2_uuid = rv.json['comparison']['answer2_id'] self.assertIn(actual_answer1_uuid, valid_answer_uuids) self.assertIn(actual_answer2_uuid, valid_answer_uuids) self.assertNotEqual(actual_answer1_uuid, actual_answer2_uuid) self.assertTrue(rv.json['new_pair']) self.assertEqual(rv.json['current'], current) # fetch again rv = self.client.get(self.base_url) self.assert200(rv) expected_comparison = rv.json['comparison'] self.assertEqual(actual_answer1_uuid, rv.json['comparison']['answer1_id']) self.assertEqual(actual_answer2_uuid, rv.json['comparison']['answer2_id']) self.assertFalse(rv.json['new_pair']) self.assertEqual(rv.json['current'], current) # test draft post comparison_submit = self._build_comparison_submit( WinningAnswer.answer1.value, True) rv = self.client.post(self.base_url, data=json.dumps(comparison_submit), content_type='application/json') self.assert200(rv) actual_comparison = rv.json['comparison'] self._validate_comparison_submit(comparison_submit, actual_comparison, expected_comparison) # test draft post (no winner) comparison_submit = self._build_comparison_submit(None) rv = self.client.post(self.base_url, data=json.dumps(comparison_submit), content_type='application/json') self.assert200(rv) actual_comparison = rv.json['comparison'] self._validate_comparison_submit(comparison_submit, actual_comparison, expected_comparison) # test normal post comparison_submit = self._build_comparison_submit( WinningAnswer.answer1.value) rv = self.client.post(self.base_url, data=json.dumps(comparison_submit), content_type='application/json') self.assert200(rv) actual_comparison = rv.json['comparison'] self._validate_comparison_submit(comparison_submit, actual_comparison, expected_comparison) # grades should increase for every comparison if user.id == self.data.get_authorized_student().id: new_course_grade = CourseGrade.get_user_course_grade( self.course, user) new_assignment_grade = AssignmentGrade.get_user_assignment_grade( self.assignment, user) self.assertGreater(new_course_grade.grade, course_grade) self.assertGreater(new_assignment_grade.grade, assignment_grade) mocked_update_course_grades_run.assert_called_once_with( lti_consumer.id, [(lti_user_resource_link1.lis_result_sourcedid, new_course_grade.id)]) mocked_update_course_grades_run.reset_mock() mocked_update_assignment_grades_run.assert_called_once_with( lti_consumer.id, [(lti_user_resource_link2.lis_result_sourcedid, new_assignment_grade.id)]) mocked_update_assignment_grades_run.reset_mock() else: new_course_grade = CourseGrade.get_user_course_grade( self.course, user) new_assignment_grade = AssignmentGrade.get_user_assignment_grade( self.assignment, user) self.assertIsNone(new_course_grade) self.assertIsNone(new_assignment_grade) mocked_update_assignment_grades_run.assert_not_called() mocked_update_course_grades_run.assert_not_called() # Resubmit of same comparison should fail rv = self.client.post(self.base_url, data=json.dumps(comparison_submit), content_type='application/json') self.assert400(rv) # all answers has been compared by the user, errors out when trying to get another pair rv = self.client.get(self.base_url) self.assert400(rv) def _validate_comparison_submit(self, comparison_submit, actual_comparison, expected_comparison): self.assertEqual( len(actual_comparison['comparison_criteria']), len(comparison_submit['comparison_criteria']), "The number of comparisons saved does not match the number sent") self.assertEqual( expected_comparison['answer1_id'], actual_comparison['answer1_id'], "Expected and actual comparison answer1 id did not match") self.assertEqual( expected_comparison['answer2_id'], actual_comparison['answer2_id'], "Expected and actual comparison answer2 id did not match") for actual_comparison_criterion in actual_comparison[ 'comparison_criteria']: found_comparison = False for expected_comparison_criterion in comparison_submit[ 'comparison_criteria']: if expected_comparison_criterion[ 'criterion_id'] != actual_comparison_criterion[ 'criterion_id']: continue self.assertEqual(expected_comparison_criterion['winner'], actual_comparison_criterion['winner'], "Expected and actual winner did not match.") found_comparison = True self.assertTrue( found_comparison, "Actual comparison received contains a comparison that was not sent." ) def _submit_all_possible_comparisons_for_user(self, user_id): example_winner_ids = [] example_loser_ids = [] for comparison_example in self.data.comparisons_examples: if comparison_example.assignment_id == self.assignment.id: comparison = Comparison.create_new_comparison( self.assignment.id, user_id, False) self.assertEqual(comparison.answer1_id, comparison_example.answer1_id) self.assertEqual(comparison.answer2_id, comparison_example.answer2_id) min_id = min([comparison.answer1_id, comparison.answer2_id]) max_id = max([comparison.answer1_id, comparison.answer2_id]) example_winner_ids.append(min_id) example_loser_ids.append(max_id) comparison.completed = True comparison.winner = WinningAnswer.answer1 if comparison.answer1_id < comparison.answer2_id else WinningAnswer.answer2 for comparison_criterion in comparison.comparison_criteria: comparison_criterion.winner = comparison.winner db.session.add(comparison) db.session.commit() # self.login(username) # calculate number of comparisons to do before user has compared all the pairs it can num_eligible_answers = 0 # need to minus one to exclude the logged in user's own answer for answer in self.data.get_student_answers(): if answer.assignment_id == self.assignment.id and answer.user_id != user_id: num_eligible_answers += 1 # n(n-1)/2 possible pairs before all answers have been compared num_possible_comparisons = int(num_eligible_answers * (num_eligible_answers - 1) / 2) winner_ids = [] loser_ids = [] for i in range(num_possible_comparisons): comparison = Comparison.create_new_comparison( self.assignment.id, user_id, False) min_id = min([comparison.answer1_id, comparison.answer2_id]) max_id = max([comparison.answer1_id, comparison.answer2_id]) winner_ids.append(min_id) loser_ids.append(max_id) comparison.completed = True comparison.winner = WinningAnswer.answer1 if comparison.answer1_id < comparison.answer2_id else WinningAnswer.answer2 for comparison_criterion in comparison.comparison_criteria: comparison_criterion.winner = comparison.winner db.session.add(comparison) db.session.commit() Comparison.calculate_scores(self.assignment.id) return { 'comparisons': { 'winners': winner_ids, 'losers': loser_ids }, 'comparison_examples': { 'winners': example_winner_ids, 'losers': example_loser_ids } } @mock.patch('random.shuffle') def test_score_calculation(self, mock_shuffle): """ This is just a rough check on whether score calculations are correct. Answers that has more wins should have the highest scores. """ # Make sure all answers are compared first comparisons_auth = self._submit_all_possible_comparisons_for_user( self.data.get_authorized_student().id) comparisons_secondary = self._submit_all_possible_comparisons_for_user( self.data.get_secondary_authorized_student().id) loser_ids = comparisons_auth['comparisons'][ 'losers'] + comparisons_secondary['comparisons']['losers'] winner_ids = comparisons_auth['comparisons'][ 'winners'] + comparisons_secondary['comparisons']['winners'] # Count the number of wins each answer has had num_wins_by_id = {} for loser_id in loser_ids: num_wins_by_id[loser_id] = num_wins_by_id.setdefault(loser_id, 0) for winner_id in winner_ids: num_wins = num_wins_by_id.setdefault(winner_id, 0) num_wins_by_id[winner_id] = num_wins + 1 # Get the actual score calculated for each answer answers = self.data.get_student_answers() answer_scores = {} for answer in answers: if answer.assignment.id == self.assignment.id: answer_scores[answer.id] = answer.score.score # Check that ranking by score and by wins match, this only works for low number of # comparisons sorted_expect_ranking = sorted(num_wins_by_id.items(), key=operator.itemgetter(0)) sorted_expect_ranking = sorted(sorted_expect_ranking, key=operator.itemgetter(1)) expected_ranking_by_wins = [ answer_id for (answer_id, wins) in sorted_expect_ranking ] sorted_actual_ranking = sorted(answer_scores.items(), key=operator.itemgetter(1)) actual_ranking_by_scores = [ answer_id for (answer_id, score) in sorted_actual_ranking ] self.assertSequenceEqual(actual_ranking_by_scores, expected_ranking_by_wins) def test_comparison_count_matched_pairing(self): # Make sure all answers are compared first answer_ids = self._submit_all_possible_comparisons_for_user( self.data.get_authorized_student().id) answer_ids2 = self._submit_all_possible_comparisons_for_user( self.data.get_secondary_authorized_student().id) compared_ids = \ answer_ids['comparisons']['winners'] + answer_ids2['comparisons']['winners'] + \ answer_ids['comparisons']['losers'] + answer_ids2['comparisons']['losers'] + \ answer_ids['comparison_examples']['winners'] + answer_ids2['comparison_examples']['winners'] + \ answer_ids['comparison_examples']['losers'] + answer_ids2['comparison_examples']['losers'] # Just a simple test for now, make sure that answers with the smaller number of # comparisons are matched up with each other # Count number of comparisons done for each answer num_comp_by_id = {} for answer_id in compared_ids: num_comp = num_comp_by_id.setdefault(answer_id, 0) num_comp_by_id[answer_id] = num_comp + 1 comp_groups = {} for answerId in num_comp_by_id: count = num_comp_by_id[answerId] comp_groups.setdefault(count, []) comp_groups[count].append(answerId) counts = sorted(comp_groups) # get the answerIds with the lowest count of comparisons possible_answer_ids = comp_groups[counts[0]] if len(possible_answer_ids) < 2: # if the lowest count group does not have enough to create a pair - add the next group possible_answer_ids += comp_groups[counts[1]] # Check that the 2 answers with 1 win gets returned with self.login( self.data.get_authorized_student_with_no_answers().username): rv = self.client.get(self.base_url) self.assert200(rv) answer1 = Answer.query.filter_by( uuid=rv.json['comparison']['answer1_id']).first() answer2 = Answer.query.filter_by( uuid=rv.json['comparison']['answer2_id']).first() self.assertIsNotNone(answer1) self.assertIsNotNone(answer2) self.assertIn(answer1.id, possible_answer_ids) self.assertIn(answer2.id, possible_answer_ids) def test_comparison_winners(self): # disable current criteria for assignment_criterion in self.assignment.assignment_criteria: assignment_criterion.active = False # test 1 criterion: answer1, answer2 (draw not possible) criterion = self.data.create_criterion(self.data.authorized_instructor) AssignmentCriterionFactory(criterion=criterion, assignment=self.assignment, weight=100) student = self.data.create_user(SystemRole.student) self.data.enrol_student(student, self.course) db.session.commit() # test winner = answer1 with self.login(student.username): rv = self.client.get(self.base_url) self.assert200(rv) comparison_submit = self._build_comparison_submit( WinningAnswer.answer1.value) rv = self.client.post(self.base_url, data=json.dumps(comparison_submit), content_type='application/json') self.assert200(rv) actual_comparison = rv.json['comparison'] self.assertEqual(actual_comparison['winner'], WinningAnswer.answer1.value) self.assertEqual(len(actual_comparison['comparison_criteria']), 1) self.assertEqual( actual_comparison['comparison_criteria'][0]['winner'], WinningAnswer.answer1.value) # test winner = answer2 rv = self.client.get(self.base_url) self.assert200(rv) comparison_submit = self._build_comparison_submit( WinningAnswer.answer2.value) rv = self.client.post(self.base_url, data=json.dumps(comparison_submit), content_type='application/json') self.assert200(rv) actual_comparison = rv.json['comparison'] self.assertEqual(actual_comparison['winner'], WinningAnswer.answer2.value) self.assertEqual(len(actual_comparison['comparison_criteria']), 1) self.assertEqual( actual_comparison['comparison_criteria'][0]['winner'], WinningAnswer.answer2.value) # test 2 criterion: answer1, answer2, draw for assignment_criterion in self.assignment.assignment_criteria: assignment_criterion.active = False criterion1 = self.data.create_criterion( self.data.authorized_instructor) criterion2 = self.data.create_criterion( self.data.authorized_instructor) AssignmentCriterionFactory(criterion=criterion1, assignment=self.assignment, weight=100) AssignmentCriterionFactory(criterion=criterion2, assignment=self.assignment, weight=100) student = self.data.create_user(SystemRole.student) self.data.enrol_student(student, self.course) db.session.commit() # test winner = answer1 with self.login(student.username): rv = self.client.get(self.base_url) self.assert200(rv) comparison_submit = self._build_comparison_submit( WinningAnswer.answer1.value) rv = self.client.post(self.base_url, data=json.dumps(comparison_submit), content_type='application/json') self.assert200(rv) actual_comparison = rv.json['comparison'] self.assertEqual(actual_comparison['winner'], WinningAnswer.answer1.value) self.assertEqual(len(actual_comparison['comparison_criteria']), 2) self.assertEqual( actual_comparison['comparison_criteria'][0]['winner'], WinningAnswer.answer1.value) self.assertEqual( actual_comparison['comparison_criteria'][1]['winner'], WinningAnswer.answer1.value) # test winner = answer2 rv = self.client.get(self.base_url) self.assert200(rv) comparison_submit = self._build_comparison_submit( WinningAnswer.answer2.value) rv = self.client.post(self.base_url, data=json.dumps(comparison_submit), content_type='application/json') self.assert200(rv) actual_comparison = rv.json['comparison'] self.assertEqual(actual_comparison['winner'], WinningAnswer.answer2.value) self.assertEqual(len(actual_comparison['comparison_criteria']), 2) self.assertEqual( actual_comparison['comparison_criteria'][0]['winner'], WinningAnswer.answer2.value) self.assertEqual( actual_comparison['comparison_criteria'][1]['winner'], WinningAnswer.answer2.value) # test winner = draw rv = self.client.get(self.base_url) self.assert200(rv) comparison_submit = self._build_comparison_submit( WinningAnswer.answer1.value) comparison_submit['comparison_criteria'][1][ 'winner'] = WinningAnswer.answer2.value rv = self.client.post(self.base_url, data=json.dumps(comparison_submit), content_type='application/json') self.assert200(rv) actual_comparison = rv.json['comparison'] self.assertEqual(actual_comparison['winner'], WinningAnswer.draw.value) self.assertEqual(len(actual_comparison['comparison_criteria']), 2) self.assertEqual( actual_comparison['comparison_criteria'][0]['winner'], WinningAnswer.answer1.value) self.assertEqual( actual_comparison['comparison_criteria'][1]['winner'], WinningAnswer.answer2.value) # test 3 criterion: answer1, answer2, draw (with with different weights) for assignment_criterion in self.assignment.assignment_criteria: assignment_criterion.active = False criterion1 = self.data.create_criterion( self.data.authorized_instructor) criterion2 = self.data.create_criterion( self.data.authorized_instructor) criterion3 = self.data.create_criterion( self.data.authorized_instructor) AssignmentCriterionFactory(criterion=criterion1, assignment=self.assignment, weight=200) AssignmentCriterionFactory(criterion=criterion2, assignment=self.assignment, weight=100) AssignmentCriterionFactory(criterion=criterion3, assignment=self.assignment, weight=100) student = self.data.create_user(SystemRole.student) self.data.enrol_student(student, self.course) db.session.commit() # test winner = answer1 with self.login(student.username): rv = self.client.get(self.base_url) self.assert200(rv) comparison_submit = self._build_comparison_submit( WinningAnswer.answer1.value) comparison_submit['comparison_criteria'][1][ 'winner'] = WinningAnswer.answer2.value rv = self.client.post(self.base_url, data=json.dumps(comparison_submit), content_type='application/json') self.assert200(rv) actual_comparison = rv.json['comparison'] self.assertEqual(actual_comparison['winner'], WinningAnswer.answer1.value) self.assertEqual(len(actual_comparison['comparison_criteria']), 3) self.assertEqual( actual_comparison['comparison_criteria'][0]['winner'], WinningAnswer.answer1.value) self.assertEqual( actual_comparison['comparison_criteria'][1]['winner'], WinningAnswer.answer2.value) self.assertEqual( actual_comparison['comparison_criteria'][2]['winner'], WinningAnswer.answer1.value) # test winner = answer2 rv = self.client.get(self.base_url) self.assert200(rv) comparison_submit = self._build_comparison_submit( WinningAnswer.answer2.value) comparison_submit['comparison_criteria'][1][ 'winner'] = WinningAnswer.answer1.value rv = self.client.post(self.base_url, data=json.dumps(comparison_submit), content_type='application/json') self.assert200(rv) actual_comparison = rv.json['comparison'] self.assertEqual(actual_comparison['winner'], WinningAnswer.answer2.value) self.assertEqual(len(actual_comparison['comparison_criteria']), 3) self.assertEqual( actual_comparison['comparison_criteria'][0]['winner'], WinningAnswer.answer2.value) self.assertEqual( actual_comparison['comparison_criteria'][1]['winner'], WinningAnswer.answer1.value) self.assertEqual( actual_comparison['comparison_criteria'][2]['winner'], WinningAnswer.answer2.value) # test winner = draw rv = self.client.get(self.base_url) self.assert200(rv) comparison_submit = self._build_comparison_submit( WinningAnswer.answer1.value) comparison_submit['comparison_criteria'][1][ 'winner'] = WinningAnswer.answer2.value comparison_submit['comparison_criteria'][2][ 'winner'] = WinningAnswer.answer2.value rv = self.client.post(self.base_url, data=json.dumps(comparison_submit), content_type='application/json') self.assert200(rv) actual_comparison = rv.json['comparison'] self.assertEqual(actual_comparison['winner'], WinningAnswer.draw.value) self.assertEqual(len(actual_comparison['comparison_criteria']), 3) self.assertEqual( actual_comparison['comparison_criteria'][0]['winner'], WinningAnswer.answer1.value) self.assertEqual( actual_comparison['comparison_criteria'][1]['winner'], WinningAnswer.answer2.value) self.assertEqual( actual_comparison['comparison_criteria'][2]['winner'], WinningAnswer.answer2.value)