tests.py 20.2 KB
Newer Older
1
"""
2
Tests for open ended grading interfaces
3

4
./manage.py lms --settings test test lms/djangoapps/open_ended_grading
5
"""
6
from django.test import RequestFactory
7

8
import json
Calen Pennington committed
9
import logging
10

11
from django.conf import settings
12
from django.contrib.auth.models import User
Calen Pennington committed
13 14 15 16 17
from django.core.urlresolvers import reverse
from django.test.utils import override_settings
from mock import MagicMock, patch, Mock
from xblock.field_data import DictFieldData
from xblock.fields import ScopeIds
18

19
from xmodule import peer_grading_module
Calen Pennington committed
20
from xmodule.error_module import ErrorDescriptor
21
from xmodule.modulestore.django import modulestore
22
from opaque_keys.edx.locations import SlashSeparatedCourseKey
Calen Pennington committed
23 24 25
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.open_ended_grading_classes import peer_grading_service, controller_query_service
from xmodule.tests import test_util_open_ended
26

Calen Pennington committed
27
from courseware.tests import factories
28
from courseware.tests.helpers import LoginEnrollmentTestCase
Calen Pennington committed
29
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
30
from lms.lib.xblock.runtime import LmsModuleSystem
31
from student.roles import CourseStaffRole
David Baumgold committed
32
from edxmako.shortcuts import render_to_string
33
from edxmako.tests import mako_middleware_process_request
34
from student.models import unique_id_for_user
35

Calen Pennington committed
36
from open_ended_grading import staff_grading_service, views, utils
Vik Paruchuri committed
37

38
log = logging.getLogger(__name__)
39

40

Vik Paruchuri committed
41 42 43 44 45 46 47 48 49 50
class EmptyStaffGradingService(object):
    """
    A staff grading service that does not return a problem list from get_problem_list.
    Used for testing to see if error message for empty problem list is correctly displayed.
    """

    def get_problem_list(self, course_id, user_id):
        """
        Return a staff grading response that is missing a problem list key.
        """
51
        return {'success': True, 'error': 'No problems found.'}
52

53

54 55 56 57
def make_instructor(course, user_email):
    """
    Makes a given user an instructor in a course.
    """
58
    CourseStaffRole(course.id).add_users(User.objects.get(email=user_email))
59

60

61 62 63 64 65 66 67
class StudentProblemListMockQuery(object):
    """
    Mock controller query service for testing student problem list functionality.
    """
    def get_grading_status_list(self, *args, **kwargs):
        """
        Get a mock grading status list with locations from the open_ended test course.
68
        @returns: grading status message dictionary.
69
        """
70
        return {
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
                "version": 1,
                "problem_list": [
                    {
                        "problem_name": "Test1",
                        "grader_type": "IN",
                        "eta_available": True,
                        "state": "Finished",
                        "eta": 259200,
                        "location": "i4x://edX/open_ended/combinedopenended/SampleQuestion1Attempt"
                    },
                    {
                        "problem_name": "Test2",
                        "grader_type": "NA",
                        "eta_available": True,
                        "state": "Waiting to be Graded",
                        "eta": 259200,
                        "location": "i4x://edX/open_ended/combinedopenended/SampleQuestion"
                    },
                    {
                        "problem_name": "Test3",
                        "grader_type": "PE",
                        "eta_available": True,
                        "state": "Waiting to be Graded",
                        "eta": 259200,
                        "location": "i4x://edX/open_ended/combinedopenended/SampleQuestion454"
                    },
                ],
                "success": True
            }

101

102 103
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
class TestStaffGradingService(ModuleStoreTestCase, LoginEnrollmentTestCase):
104 105 106 107 108
    '''
    Check that staff grading service proxy works.  Basically just checking the
    access control and error handling logic -- all the actual work is on the
    backend.
    '''
Vik Paruchuri committed
109

110 111 112 113 114 115 116 117
    def setUp(self):
        self.student = 'view@test.com'
        self.instructor = 'view2@test.com'
        self.password = 'foo'
        self.create_account('u1', self.student, self.password)
        self.create_account('u2', self.instructor, self.password)
        self.activate_user(self.student)
        self.activate_user(self.instructor)
Calen Pennington committed
118

119
        self.course_id = SlashSeparatedCourseKey("edX", "toy", "2012_Fall")
120
        self.location_string = self.course_id.make_usage_key('html', 'TestLocation').to_deprecated_string()
121
        self.toy = modulestore().get_course(self.course_id)
Vik Paruchuri committed
122

123
        make_instructor(self.toy, self.instructor)
124

125
        self.mock_service = staff_grading_service.staff_grading_service()
126 127 128 129

        self.logout()

    def test_access(self):
130
        """
131
        Make sure only staff have access.
132
        """
133 134 135 136
        self.login(self.student, self.password)

        # both get and post should return 404
        for view_name in ('staff_grading_get_next', 'staff_grading_save_grade'):
137
            url = reverse(view_name, kwargs={'course_id': self.course_id.to_deprecated_string()})
138 139
            self.assert_request_status_code(404, url, method="GET")
            self.assert_request_status_code(404, url, method="POST")
140 141 142 143

    def test_get_next(self):
        self.login(self.instructor, self.password)

144
        url = reverse('staff_grading_get_next', kwargs={'course_id': self.course_id.to_deprecated_string()})
145
        data = {'location': self.location_string}
146

147
        response = self.assert_request_status_code(200, url, method="POST", data=data)
148

149
        content = json.loads(response.content)
150

151 152 153 154 155 156 157 158 159 160
        self.assertTrue(content['success'])
        self.assertEquals(content['submission_id'], self.mock_service.cnt)
        self.assertIsNotNone(content['submission'])
        self.assertIsNotNone(content['num_graded'])
        self.assertIsNotNone(content['min_for_ml'])
        self.assertIsNotNone(content['num_pending'])
        self.assertIsNotNone(content['prompt'])
        self.assertIsNotNone(content['ml_error_info'])
        self.assertIsNotNone(content['max_score'])
        self.assertIsNotNone(content['rubric'])
161

162
    def save_grade_base(self, skip=False):
163 164
        self.login(self.instructor, self.password)

165
        url = reverse('staff_grading_save_grade', kwargs={'course_id': self.course_id.to_deprecated_string()})
166 167 168 169

        data = {'score': '12',
                'feedback': 'great!',
                'submission_id': '123',
170
                'location': self.location_string,
171
                'submission_flagged': "true",
172
                'rubric_scores[]': ['1', '2']}
173
        if skip:
174
            data.update({'skipped': True})
175

176
        response = self.assert_request_status_code(200, url, method="POST", data=data)
177 178 179
        content = json.loads(response.content)
        self.assertTrue(content['success'], str(content))
        self.assertEquals(content['submission_id'], self.mock_service.cnt)
180

181 182 183 184 185 186
    def test_save_grade(self):
        self.save_grade_base(skip=False)

    def test_save_grade_skip(self):
        self.save_grade_base(skip=True)

187 188 189
    def test_get_problem_list(self):
        self.login(self.instructor, self.password)

190
        url = reverse('staff_grading_get_problem_list', kwargs={'course_id': self.course_id.to_deprecated_string()})
191 192
        data = {}

193
        response = self.assert_request_status_code(200, url, method="POST", data=data)
194
        content = json.loads(response.content)
Vik Paruchuri committed
195

Vik Paruchuri committed
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212
        self.assertTrue(content['success'])
        self.assertEqual(content['problem_list'], [])

    @patch('open_ended_grading.staff_grading_service._service', EmptyStaffGradingService())
    def test_get_problem_list_missing(self):
        """
        Test to see if a staff grading response missing a problem list is given the appropriate error.
        Mock the staff grading service to enable the key to be missing.
        """

        # Get a valid user object.
        instructor = User.objects.get(email=self.instructor)
        # Mock a request object.
        request = Mock(
            user=instructor,
        )
        # Get the response and load its content.
213
        response = json.loads(staff_grading_service.get_problem_list(request, self.course_id.to_deprecated_string()).content)
Vik Paruchuri committed
214 215 216 217 218

        # A valid response will have an "error" key.
        self.assertTrue('error' in response)
        # Check that the error text is correct.
        self.assertIn("Cannot find", response['error'])
219

220 221 222 223 224 225
    def test_save_grade_with_long_feedback(self):
        """
        Test if feedback is too long save_grade() should return error message.
        """
        self.login(self.instructor, self.password)

226
        url = reverse('staff_grading_save_grade', kwargs={'course_id': self.course_id.to_deprecated_string()})
227 228 229 230 231

        data = {
            'score': '12',
            'feedback': '',
            'submission_id': '123',
232
            'location': self.location_string,
233 234 235 236 237 238 239 240 241
            'submission_flagged': "false",
            'rubric_scores[]': ['1', '2']
        }

        feedback_fragment = "This is very long feedback."
        data["feedback"] = feedback_fragment * (
            (staff_grading_service.MAX_ALLOWED_FEEDBACK_LENGTH / len(feedback_fragment) + 1)
        )

242
        response = self.assert_request_status_code(200, url, method="POST", data=data)
243 244 245 246 247 248 249 250 251 252 253
        content = json.loads(response.content)

        # Should not succeed.
        self.assertEquals(content['success'], False)
        self.assertEquals(
            content['error'],
            "Feedback is too long, Max length is {0} characters.".format(
                staff_grading_service.MAX_ALLOWED_FEEDBACK_LENGTH
            )
        )

Calen Pennington committed
254

255 256
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
class TestPeerGradingService(ModuleStoreTestCase, LoginEnrollmentTestCase):
257 258 259 260 261
    '''
    Check that staff grading service proxy works.  Basically just checking the
    access control and error handling logic -- all the actual work is on the
    backend.
    '''
Vik Paruchuri committed
262

263 264 265 266 267 268 269 270
    def setUp(self):
        self.student = 'view@test.com'
        self.instructor = 'view2@test.com'
        self.password = 'foo'
        self.create_account('u1', self.student, self.password)
        self.create_account('u2', self.instructor, self.password)
        self.activate_user(self.student)
        self.activate_user(self.instructor)
Calen Pennington committed
271

272
        self.course_id = SlashSeparatedCourseKey("edX", "toy", "2012_Fall")
273
        self.location_string = self.course_id.make_usage_key('html', 'TestLocation').to_deprecated_string()
274
        self.toy = modulestore().get_course(self.course_id)
275
        location = "i4x://edX/toy/peergrading/init"
Calen Pennington committed
276
        field_data = DictFieldData({'data': "<peergrading/>", 'location': location, 'category':'peergrading'})
277
        self.mock_service = peer_grading_service.MockPeerGradingService()
278
        self.system = LmsModuleSystem(
279
            static_url=settings.STATIC_URL,
280
            track_function=None,
281
            get_module=None,
282 283
            render_template=render_to_string,
            replace_urls=None,
284
            s3_interface=test_util_open_ended.S3_INTERFACE,
285 286
            open_ended_grading_interface=test_util_open_ended.OPEN_ENDED_GRADING_INTERFACE,
            mixins=settings.XBLOCK_MIXINS,
287
            error_descriptor_class=ErrorDescriptor,
288
            descriptor_runtime=None,
Vik Paruchuri committed
289
        )
Calen Pennington committed
290
        self.descriptor = peer_grading_module.PeerGradingDescriptor(self.system, field_data, ScopeIds(None, None, None, None))
291 292
        self.descriptor.xmodule_runtime = self.system
        self.peer_module = self.descriptor
293
        self.peer_module.peer_gs = self.mock_service
294 295 296
        self.logout()

    def test_get_next_submission_success(self):
297
        data = {'location': self.location_string}
298

299 300
        response = self.peer_module.get_next_submission(data)
        content = response
Vik Paruchuri committed
301

302 303 304 305 306
        self.assertTrue(content['success'])
        self.assertIsNotNone(content['submission_id'])
        self.assertIsNotNone(content['prompt'])
        self.assertIsNotNone(content['submission_key'])
        self.assertIsNotNone(content['max_score'])
307 308 309

    def test_get_next_submission_missing_location(self):
        data = {}
310
        d = self.peer_module.get_next_submission(data)
311 312 313 314
        self.assertFalse(d['success'])
        self.assertEqual(d['error'], "Missing required keys: location")

    def test_save_grade_success(self):
315
        data = {
Vik Paruchuri committed
316
            'rubric_scores[]': [0, 0],
317
            'location': self.location_string,
Vik Paruchuri committed
318 319 320 321
            'submission_id': 1,
            'submission_key': 'fake key',
            'score': 2,
            'feedback': 'feedback',
Vik Paruchuri committed
322 323 324
            'submission_flagged': 'false',
            'answer_unknown': 'false',
            'rubric_scores_complete' : 'true'
Vik Paruchuri committed
325
        }
326 327

        qdict = MagicMock()
Vik Paruchuri committed
328

329 330
        def fake_get_item(key):
            return data[key]
Vik Paruchuri committed
331

332 333 334 335
        qdict.__getitem__.side_effect = fake_get_item
        qdict.getlist = fake_get_item
        qdict.keys = data.keys

336
        response = self.peer_module.save_grade(qdict)
Vik Paruchuri committed
337

338
        self.assertTrue(response['success'])
339 340 341

    def test_save_grade_missing_keys(self):
        data = {}
342
        d = self.peer_module.save_grade(data)
343 344 345
        self.assertFalse(d['success'])
        self.assertTrue(d['error'].find('Missing required keys:') > -1)

346
    def test_is_calibrated_success(self):
347
        data = {'location': self.location_string}
348
        response = self.peer_module.is_student_calibrated(data)
Vik Paruchuri committed
349

350 351
        self.assertTrue(response['success'])
        self.assertTrue('calibrated' in response)
352

353 354
    def test_is_calibrated_failure(self):
        data = {}
355 356 357
        response = self.peer_module.is_student_calibrated(data)
        self.assertFalse(response['success'])
        self.assertFalse('calibrated' in response)
358 359

    def test_show_calibration_essay_success(self):
360
        data = {'location': self.location_string}
361

362
        response = self.peer_module.show_calibration_essay(data)
Vik Paruchuri committed
363

364 365 366 367 368
        self.assertTrue(response['success'])
        self.assertIsNotNone(response['submission_id'])
        self.assertIsNotNone(response['prompt'])
        self.assertIsNotNone(response['submission_key'])
        self.assertIsNotNone(response['max_score'])
369 370 371 372

    def test_show_calibration_essay_missing_key(self):
        data = {}

373
        response = self.peer_module.show_calibration_essay(data)
374

375 376
        self.assertFalse(response['success'])
        self.assertEqual(response['error'], "Missing required keys: location")
377 378

    def test_save_calibration_essay_success(self):
379
        data = {
Vik Paruchuri committed
380
            'rubric_scores[]': [0, 0],
381
            'location': self.location_string,
Vik Paruchuri committed
382 383 384 385 386 387
            'submission_id': 1,
            'submission_key': 'fake key',
            'score': 2,
            'feedback': 'feedback',
            'submission_flagged': 'false'
        }
388 389

        qdict = MagicMock()
Vik Paruchuri committed
390

391 392
        def fake_get_item(key):
            return data[key]
Vik Paruchuri committed
393

394 395 396 397
        qdict.__getitem__.side_effect = fake_get_item
        qdict.getlist = fake_get_item
        qdict.keys = data.keys

398 399 400
        response = self.peer_module.save_calibration_essay(qdict)
        self.assertTrue(response['success'])
        self.assertTrue('actual_score' in response)
401 402 403

    def test_save_calibration_essay_missing_keys(self):
        data = {}
404 405 406 407 408
        response = self.peer_module.save_calibration_essay(data)
        self.assertFalse(response['success'])
        self.assertTrue(response['error'].find('Missing required keys:') > -1)
        self.assertFalse('actual_score' in response)

409 410 411 412 413 414
    def test_save_grade_with_long_feedback(self):
        """
        Test if feedback is too long save_grade() should return error message.
        """
        data = {
            'rubric_scores[]': [0, 0],
415
            'location': self.location_string,
416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440
            'submission_id': 1,
            'submission_key': 'fake key',
            'score': 2,
            'feedback': '',
            'submission_flagged': 'false',
            'answer_unknown': 'false',
            'rubric_scores_complete': 'true'
        }

        feedback_fragment = "This is very long feedback."
        data["feedback"] = feedback_fragment * (
            (staff_grading_service.MAX_ALLOWED_FEEDBACK_LENGTH / len(feedback_fragment) + 1)
        )

        response_dict = self.peer_module.save_grade(data)

        # Should not succeed.
        self.assertEquals(response_dict['success'], False)
        self.assertEquals(
            response_dict['error'],
            "Feedback is too long, Max length is {0} characters.".format(
                staff_grading_service.MAX_ALLOWED_FEEDBACK_LENGTH
            )
        )

441

442
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
443
class TestPanel(ModuleStoreTestCase):
Vik Paruchuri committed
444 445 446
    """
    Run tests on the open ended panel
    """
447 448 449

    def setUp(self):
        # Toy courses should be loaded
450 451
        self.course_key = SlashSeparatedCourseKey('edX', 'open_ended', '2012_Fall')
        self.course = modulestore().get_course(self.course_key)
452 453 454 455 456 457 458 459 460 461
        self.user = factories.UserFactory()

    def test_open_ended_panel(self):
        """
        Test to see if the peer grading module in the demo course is found
        @return:
        """
        found_module, peer_grading_module = views.find_peer_grading_module(self.course)
        self.assertTrue(found_module)

462 463 464 465 466
    @patch(
        'open_ended_grading.utils.create_controller_query_service',
        Mock(
            return_value=controller_query_service.MockControllerQueryService(
                settings.OPEN_ENDED_GRADING_INTERFACE,
467
                utils.SYSTEM
468 469 470
            )
        )
    )
471
    def test_problem_list(self):
Vik Paruchuri committed
472 473 474 475
        """
        Ensure that the problem list from the grading controller server can be rendered properly locally
        @return:
        """
476 477 478 479 480 481
        request = RequestFactory().get(
            reverse("open_ended_problems", kwargs={'course_id': self.course_key})
        )
        request.user = self.user

        mako_middleware_process_request(request)
482
        response = views.student_problem_list(request, self.course.id.to_deprecated_string())
483
        self.assertRegexpMatches(response.content, "Here is a list of open ended problems for this course.")
Vik Paruchuri committed
484

485

Vik Paruchuri committed
486 487 488 489 490 491 492
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
class TestPeerGradingFound(ModuleStoreTestCase):
    """
    Test to see if peer grading modules can be found properly.
    """

    def setUp(self):
493 494
        self.course_key = SlashSeparatedCourseKey('edX', 'open_ended_nopath', '2012_Fall')
        self.course = modulestore().get_course(self.course_key)
Vik Paruchuri committed
495 496 497 498 499 500 501 502

    def test_peer_grading_nopath(self):
        """
        The open_ended_nopath course contains a peer grading module with no path to it.
        Ensure that the exception is caught.
        """

        found, url = views.find_peer_grading_module(self.course)
503 504
        self.assertEqual(found, False)

505

506 507 508 509 510 511 512 513
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
class TestStudentProblemList(ModuleStoreTestCase):
    """
    Test if the student problem list correctly fetches and parses problems.
    """

    def setUp(self):
        # Load an open ended course with several problems.
514 515
        self.course_key = SlashSeparatedCourseKey('edX', 'open_ended', '2012_Fall')
        self.course = modulestore().get_course(self.course_key)
516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542
        self.user = factories.UserFactory()
        # Enroll our user in our course and make them an instructor.
        make_instructor(self.course, self.user.email)

    @patch(
        'open_ended_grading.utils.create_controller_query_service',
        Mock(return_value=StudentProblemListMockQuery())
    )
    def test_get_problem_list(self):
        """
        Test to see if the StudentProblemList class can get and parse a problem list from ORA.
        Mock the get_grading_status_list function using StudentProblemListMockQuery.
        """
        # Initialize a StudentProblemList object.
        student_problem_list = utils.StudentProblemList(self.course.id, unique_id_for_user(self.user))
        # Get the initial problem list from ORA.
        success = student_problem_list.fetch_from_grading_service()
        # Should be successful, and we should have three problems.  See mock class for details.
        self.assertTrue(success)
        self.assertEqual(len(student_problem_list.problem_list), 3)

        # See if the problem locations are valid.
        valid_problems = student_problem_list.add_problem_data(reverse('courses'))
        # One location is invalid, so we should now have two.
        self.assertEqual(len(valid_problems), 2)
        # Ensure that human names are being set properly.
        self.assertEqual(valid_problems[0]['grader_type_display_name'], "Instructor Assessment")