Exemplo n.º 1
0
    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')))
Exemplo n.º 2
0
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)
Exemplo n.º 3
0
 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()
Exemplo n.º 4
0
    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'
Exemplo n.º 5
0
    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()
Exemplo n.º 6
0
    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'))
Exemplo n.º 7
0
    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()
Exemplo n.º 8
0
 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()
Exemplo n.º 9
0
    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
Exemplo n.º 10
0
    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'
        }
Exemplo n.º 11
0
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)
Exemplo n.º 12
0
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)
Exemplo n.º 13
0
    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'
        }
Exemplo n.º 14
0
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)
Exemplo n.º 15
0
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'])
Exemplo n.º 16
0
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)
Exemplo n.º 17
0
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."
            )
Exemplo n.º 18
0
    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'
        }
Exemplo n.º 19
0
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()
Exemplo n.º 20
0
    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'
        }
Exemplo n.º 21
0
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)
Exemplo n.º 22
0
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)
Exemplo n.º 23
0
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()
Exemplo n.º 24
0
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()
Exemplo n.º 25
0
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)
Exemplo n.º 26
0
    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)
Exemplo n.º 27
0
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()
Exemplo n.º 28
0
    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'
        }
Exemplo n.º 29
0
 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()
Exemplo n.º 30
0
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)
Exemplo n.º 31
0
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'])
Exemplo n.º 32
0
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()
Exemplo n.º 33
0
 def setUp(self):
     super(LTIConsumersAPITests, self).setUp()
     self.data = BasicTestData()
     self.lti_data = LTITestData()
Exemplo n.º 34
0
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'])
Exemplo n.º 35
0
    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'
        }
Exemplo n.º 36
0
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)
Exemplo n.º 37
0
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)
Exemplo n.º 38
0
 def setUp(self):
     super(CoursesLTIAPITests, self).setUp()
     self.data = SimpleAssignmentTestData()
     self.lti_data = LTITestData()
Exemplo n.º 39
0
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)
Exemplo n.º 40
0
 def setUp(self):
     super(CoursesLTIAPITests, self).setUp()
     self.data = SimpleAssignmentTestData()
     self.lti_data = LTITestData()
Exemplo n.º 41
0
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)
Exemplo n.º 42
0
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)
Exemplo n.º 43
0
    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'
        }
Exemplo n.º 44
0
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)