tests.py 21.6 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
import ddt
7
import json
Calen Pennington committed
8
import logging
9

10
from django.conf import settings
11
from django.contrib.auth.models import User
Calen Pennington committed
12
from django.core.urlresolvers import reverse
13 14 15
from django.test import RequestFactory
from edxmako.shortcuts import render_to_string
from edxmako.tests import mako_middleware_process_request
Calen Pennington committed
16
from mock import MagicMock, patch, Mock
17
from opaque_keys.edx.locations import SlashSeparatedCourseKey
Calen Pennington committed
18 19
from xblock.field_data import DictFieldData
from xblock.fields import ScopeIds
20

21
from config_models.models import cache
22 23
from courseware.tests import factories
from courseware.tests.helpers import LoginEnrollmentTestCase
24
from lms.djangoapps.lms_xblock.runtime import LmsModuleSystem
25 26
from student.roles import CourseStaffRole
from student.models import unique_id_for_user
27
from xblock_django.models import XBlockDisableConfig
28
from xmodule import peer_grading_module
Calen Pennington committed
29
from xmodule.error_module import ErrorDescriptor
30
from xmodule.modulestore.django import modulestore
31
from xmodule.modulestore.tests.django_utils import TEST_DATA_MIXED_TOY_MODULESTORE, ModuleStoreTestCase
32
from xmodule.modulestore.tests.factories import CourseFactory
33
from xmodule.modulestore.xml_importer import import_course_from_xml
Calen Pennington committed
34 35
from xmodule.open_ended_grading_classes import peer_grading_service, controller_query_service
from xmodule.tests import test_util_open_ended
36

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

39 40 41
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT


42
log = logging.getLogger(__name__)
43

44

Vik Paruchuri committed
45 46 47 48 49 50 51 52 53 54
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.
        """
55
        return {'success': True, 'error': 'No problems found.'}
56

57

58 59 60 61
def make_instructor(course, user_email):
    """
    Makes a given user an instructor in a course.
    """
62
    CourseStaffRole(course.id).add_users(User.objects.get(email=user_email))
63

64

65 66 67 68 69 70 71
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.
72
        @returns: grading status message dictionary.
73
        """
74
        return {
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 101 102 103
            "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
        }
104

105

106
class TestStaffGradingService(ModuleStoreTestCase, LoginEnrollmentTestCase):
107 108 109 110 111
    '''
    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.
    '''
112
    MODULESTORE = TEST_DATA_MIXED_TOY_MODULESTORE
Vik Paruchuri committed
113

114
    def setUp(self):
115
        super(TestStaffGradingService, self).setUp()
116 117 118 119 120 121 122
        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
123

124
        self.course_id = SlashSeparatedCourseKey("edX", "toy", "2012_Fall")
125
        self.location_string = self.course_id.make_usage_key('html', 'TestLocation').to_deprecated_string()
126
        self.toy = modulestore().get_course(self.course_id)
Vik Paruchuri committed
127

128
        make_instructor(self.toy, self.instructor)
129

130
        self.mock_service = staff_grading_service.staff_grading_service()
131 132 133 134

        self.logout()

    def test_access(self):
135
        """
136
        Make sure only staff have access.
137
        """
138 139 140 141
        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'):
142
            url = reverse(view_name, kwargs={'course_id': self.course_id.to_deprecated_string()})
143 144
            self.assert_request_status_code(404, url, method="GET")
            self.assert_request_status_code(404, url, method="POST")
145 146 147 148

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

149
        url = reverse('staff_grading_get_next', kwargs={'course_id': self.course_id.to_deprecated_string()})
150
        data = {'location': self.location_string}
151

152
        response = self.assert_request_status_code(200, url, method="POST", data=data)
153

154
        content = json.loads(response.content)
155

156 157 158 159 160 161 162 163 164 165
        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'])
166

167
    def save_grade_base(self, skip=False):
168 169
        self.login(self.instructor, self.password)

170
        url = reverse('staff_grading_save_grade', kwargs={'course_id': self.course_id.to_deprecated_string()})
171 172 173 174

        data = {'score': '12',
                'feedback': 'great!',
                'submission_id': '123',
175
                'location': self.location_string,
176
                'submission_flagged': "true",
177
                'rubric_scores[]': ['1', '2']}
178
        if skip:
179
            data.update({'skipped': True})
180

181
        response = self.assert_request_status_code(200, url, method="POST", data=data)
182 183 184
        content = json.loads(response.content)
        self.assertTrue(content['success'], str(content))
        self.assertEquals(content['submission_id'], self.mock_service.cnt)
185

186 187 188 189 190 191
    def test_save_grade(self):
        self.save_grade_base(skip=False)

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

192 193 194
    def test_get_problem_list(self):
        self.login(self.instructor, self.password)

195
        url = reverse('staff_grading_get_problem_list', kwargs={'course_id': self.course_id.to_deprecated_string()})
196 197
        data = {}

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

Vik Paruchuri committed
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217
        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.
218
        response = json.loads(staff_grading_service.get_problem_list(request, self.course_id.to_deprecated_string()).content)
Vik Paruchuri committed
219 220 221 222 223

        # 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'])
224

225 226 227 228 229 230
    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)

231
        url = reverse('staff_grading_save_grade', kwargs={'course_id': self.course_id.to_deprecated_string()})
232 233 234 235 236

        data = {
            'score': '12',
            'feedback': '',
            'submission_id': '123',
237
            'location': self.location_string,
238 239 240 241 242 243 244 245 246
            '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)
        )

247
        response = self.assert_request_status_code(200, url, method="POST", data=data)
248 249 250 251 252 253 254 255 256 257 258
        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
259

260
class TestPeerGradingService(ModuleStoreTestCase, LoginEnrollmentTestCase):
261 262 263 264 265
    '''
    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
266

267
    def setUp(self):
268
        super(TestPeerGradingService, self).setUp()
269 270 271 272 273 274 275
        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
276

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

    def test_get_next_submission_success(self):
302
        data = {'location': self.location_string}
303

304 305
        response = self.peer_module.get_next_submission(data)
        content = response
Vik Paruchuri committed
306

307 308 309 310 311
        self.assertTrue(content['success'])
        self.assertIsNotNone(content['submission_id'])
        self.assertIsNotNone(content['prompt'])
        self.assertIsNotNone(content['submission_key'])
        self.assertIsNotNone(content['max_score'])
312 313 314

    def test_get_next_submission_missing_location(self):
        data = {}
315
        d = self.peer_module.get_next_submission(data)
316 317 318 319
        self.assertFalse(d['success'])
        self.assertEqual(d['error'], "Missing required keys: location")

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

        qdict = MagicMock()
Vik Paruchuri committed
333

334 335
        def fake_get_item(key):
            return data[key]
Vik Paruchuri committed
336

337 338 339 340
        qdict.__getitem__.side_effect = fake_get_item
        qdict.getlist = fake_get_item
        qdict.keys = data.keys

341
        response = self.peer_module.save_grade(qdict)
Vik Paruchuri committed
342

343
        self.assertTrue(response['success'])
344 345 346

    def test_save_grade_missing_keys(self):
        data = {}
347
        d = self.peer_module.save_grade(data)
348 349 350
        self.assertFalse(d['success'])
        self.assertTrue(d['error'].find('Missing required keys:') > -1)

351
    def test_is_calibrated_success(self):
352
        data = {'location': self.location_string}
353
        response = self.peer_module.is_student_calibrated(data)
Vik Paruchuri committed
354

355 356
        self.assertTrue(response['success'])
        self.assertTrue('calibrated' in response)
357

358 359
    def test_is_calibrated_failure(self):
        data = {}
360 361 362
        response = self.peer_module.is_student_calibrated(data)
        self.assertFalse(response['success'])
        self.assertFalse('calibrated' in response)
363 364

    def test_show_calibration_essay_success(self):
365
        data = {'location': self.location_string}
366

367
        response = self.peer_module.show_calibration_essay(data)
Vik Paruchuri committed
368

369 370 371 372 373
        self.assertTrue(response['success'])
        self.assertIsNotNone(response['submission_id'])
        self.assertIsNotNone(response['prompt'])
        self.assertIsNotNone(response['submission_key'])
        self.assertIsNotNone(response['max_score'])
374 375 376 377

    def test_show_calibration_essay_missing_key(self):
        data = {}

378
        response = self.peer_module.show_calibration_essay(data)
379

380 381
        self.assertFalse(response['success'])
        self.assertEqual(response['error'], "Missing required keys: location")
382 383

    def test_save_calibration_essay_success(self):
384
        data = {
Vik Paruchuri committed
385
            'rubric_scores[]': [0, 0],
386
            'location': self.location_string,
Vik Paruchuri committed
387 388 389 390 391 392
            'submission_id': 1,
            'submission_key': 'fake key',
            'score': 2,
            'feedback': 'feedback',
            'submission_flagged': 'false'
        }
393 394

        qdict = MagicMock()
Vik Paruchuri committed
395

396 397
        def fake_get_item(key):
            return data[key]
Vik Paruchuri committed
398

399 400 401 402
        qdict.__getitem__.side_effect = fake_get_item
        qdict.getlist = fake_get_item
        qdict.keys = data.keys

403 404 405
        response = self.peer_module.save_calibration_essay(qdict)
        self.assertTrue(response['success'])
        self.assertTrue('actual_score' in response)
406 407 408

    def test_save_calibration_essay_missing_keys(self):
        data = {}
409 410 411 412 413
        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)

414 415 416 417 418 419
    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],
420
            'location': self.location_string,
421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445
            '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
            )
        )

446

447
class TestPanel(ModuleStoreTestCase):
Vik Paruchuri committed
448 449 450
    """
    Run tests on the open ended panel
    """
451
    def setUp(self):
452
        super(TestPanel, self).setUp()
453
        self.user = factories.UserFactory()
454
        store = modulestore()
455
        course_items = import_course_from_xml(store, self.user.id, TEST_DATA_DIR, ['open_ended'])  # pylint: disable=maybe-no-member
456 457
        self.course = course_items[0]
        self.course_key = self.course.id
458 459 460 461 462 463 464 465 466

    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)

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

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

490

Vik Paruchuri committed
491 492 493 494 495
class TestPeerGradingFound(ModuleStoreTestCase):
    """
    Test to see if peer grading modules can be found properly.
    """
    def setUp(self):
496
        super(TestPeerGradingFound, self).setUp()
497 498
        self.user = factories.UserFactory()
        store = modulestore()
499
        course_items = import_course_from_xml(store, self.user.id, TEST_DATA_DIR, ['open_ended_nopath'])  # pylint: disable=maybe-no-member
500 501
        self.course = course_items[0]
        self.course_key = self.course.id
Vik Paruchuri committed
502 503 504 505 506 507 508 509

    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)
510 511
        self.assertEqual(found, False)

512

513 514 515 516 517
class TestStudentProblemList(ModuleStoreTestCase):
    """
    Test if the student problem list correctly fetches and parses problems.
    """
    def setUp(self):
518 519
        super(TestStudentProblemList, self).setUp()

520 521
        # Load an open ended course with several problems.
        self.user = factories.UserFactory()
522
        store = modulestore()
523
        course_items = import_course_from_xml(store, self.user.id, TEST_DATA_DIR, ['open_ended'])  # pylint: disable=maybe-no-member
524 525 526
        self.course = course_items[0]
        self.course_key = self.course.id

527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552
        # 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")
553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588


@ddt.ddt
class TestTabs(ModuleStoreTestCase):
    """
    Test tabs.
    """
    def setUp(self):
        super(TestTabs, self).setUp()
        self.course = CourseFactory(advanced_modules=('combinedopenended'))
        self.addCleanup(lambda: self._enable_xblock_disable_config(False))

    def _enable_xblock_disable_config(self, enabled):
        """ Enable or disable xblocks disable. """
        config = XBlockDisableConfig.current()
        config.enabled = enabled
        config.disabled_blocks = "\n".join(('combinedopenended', 'peergrading'))
        config.save()
        cache.clear()

    @ddt.data(
        views.StaffGradingTab,
        views.PeerGradingTab,
        views.OpenEndedGradingTab,
    )
    def test_tabs_enabled(self, tab):
        self.assertTrue(tab.is_enabled(self.course))

    @ddt.data(
        views.StaffGradingTab,
        views.PeerGradingTab,
        views.OpenEndedGradingTab,
    )
    def test_tabs_disabled(self, tab):
        self._enable_xblock_disable_config(True)
        self.assertFalse(tab.is_enabled(self.course))