test_course_settings.py 49 KB
Newer Older
cahrens committed
1 2 3
"""
Tests for Studio Course Settings.
"""
4 5
import datetime
import json
6
import copy
7
import mock
Oleg Marshev committed
8
from mock import patch
9
import unittest
10

Don Mitchell committed
11
from django.utils.timezone import UTC
12
from django.test.utils import override_settings
Oleg Marshev committed
13
from django.conf import settings
14

15
from models.settings.course_details import (CourseDetails, CourseSettingsEncoder)
16
from models.settings.course_grading import CourseGradingModel
17
from contentstore.utils import reverse_course_url, reverse_usage_url
JonahStanley committed
18
from xmodule.modulestore.tests.factories import CourseFactory
19

20
from models.settings.course_metadata import CourseMetadata
21
from xmodule.fields import Date
Calen Pennington committed
22

David Baumgold committed
23
from .utils import CourseTestCase
24
from xmodule.modulestore.django import modulestore
25
from contentstore.views.component import ADVANCED_COMPONENT_POLICY_KEY
26 27
import ddt
from xmodule.modulestore import ModuleStoreEnum
28

29 30
from util.milestones_helpers import seed_milestone_relationship_types

31

32 33 34
def get_url(course_id, handler_name='settings_handler'):
    return reverse_course_url(handler_name, course_id)

35

36
class CourseDetailsTestCase(CourseTestCase):
cahrens committed
37 38 39
    """
    Tests the first course settings page (course dates, overview, etc.).
    """
40
    def test_virgin_fetch(self):
41
        details = CourseDetails.fetch(self.course.id)
Don Mitchell committed
42 43 44
        self.assertEqual(details.org, self.course.location.org, "Org not copied into")
        self.assertEqual(details.course_id, self.course.location.course, "Course_id not copied into")
        self.assertEqual(details.run, self.course.location.name, "Course name not copied into")
45
        self.assertEqual(details.course_image_name, self.course.course_image)
Don Mitchell committed
46
        self.assertIsNotNone(details.start_date.tzinfo)
47 48 49 50 51 52
        self.assertIsNone(details.end_date, "end date somehow initialized " + str(details.end_date))
        self.assertIsNone(details.enrollment_start, "enrollment_start date somehow initialized " + str(details.enrollment_start))
        self.assertIsNone(details.enrollment_end, "enrollment_end date somehow initialized " + str(details.enrollment_end))
        self.assertIsNone(details.syllabus, "syllabus somehow initialized" + str(details.syllabus))
        self.assertIsNone(details.intro_video, "intro_video somehow initialized" + str(details.intro_video))
        self.assertIsNone(details.effort, "effort somehow initialized" + str(details.effort))
53
        self.assertIsNone(details.language, "language somehow initialized" + str(details.language))
Calen Pennington committed
54

55
    def test_encoder(self):
56
        details = CourseDetails.fetch(self.course.id)
Don Mitchell committed
57
        jsondetails = json.dumps(details, cls=CourseSettingsEncoder)
58
        jsondetails = json.loads(jsondetails)
59
        self.assertEqual(jsondetails['course_image_name'], self.course.course_image)
60 61 62 63 64 65
        self.assertIsNone(jsondetails['end_date'], "end date somehow initialized ")
        self.assertIsNone(jsondetails['enrollment_start'], "enrollment_start date somehow initialized ")
        self.assertIsNone(jsondetails['enrollment_end'], "enrollment_end date somehow initialized ")
        self.assertIsNone(jsondetails['syllabus'], "syllabus somehow initialized")
        self.assertIsNone(jsondetails['intro_video'], "intro_video somehow initialized")
        self.assertIsNone(jsondetails['effort'], "effort somehow initialized")
66
        self.assertIsNone(jsondetails['language'], "language somehow initialized")
Calen Pennington committed
67

68 69 70 71
    def test_ooc_encoder(self):
        """
        Test the encoder out of its original constrained purpose to see if it functions for general use
        """
David Baumgold committed
72 73 74 75 76
        details = {
            'number': 1,
            'string': 'string',
            'datetime': datetime.datetime.now(UTC())
        }
77 78 79 80 81 82
        jsondetails = json.dumps(details, cls=CourseSettingsEncoder)
        jsondetails = json.loads(jsondetails)

        self.assertEquals(1, jsondetails['number'])
        self.assertEqual(jsondetails['string'], 'string')

83
    def test_update_and_fetch(self):
84
        jsondetails = CourseDetails.fetch(self.course.id)
85
        jsondetails.syllabus = "<a href='foo'>bar</a>"
Don Mitchell committed
86
        # encode - decode to convert date fields and other data which changes form
87
        self.assertEqual(
88
            CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).syllabus,
89 90
            jsondetails.syllabus, "After set syllabus"
        )
91 92
        jsondetails.short_description = "Short Description"
        self.assertEqual(
93
            CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).short_description,
94 95
            jsondetails.short_description, "After set short_description"
        )
96
        jsondetails.overview = "Overview"
97
        self.assertEqual(
98
            CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).overview,
99 100
            jsondetails.overview, "After set overview"
        )
101
        jsondetails.intro_video = "intro_video"
102
        self.assertEqual(
103
            CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).intro_video,
104 105
            jsondetails.intro_video, "After set intro_video"
        )
106
        jsondetails.effort = "effort"
107
        self.assertEqual(
108
            CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).effort,
109 110
            jsondetails.effort, "After set effort"
        )
111 112
        jsondetails.start_date = datetime.datetime(2010, 10, 1, 0, tzinfo=UTC())
        self.assertEqual(
113
            CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).start_date,
114 115
            jsondetails.start_date
        )
116 117
        jsondetails.course_image_name = "an_image.jpg"
        self.assertEqual(
118
            CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).course_image_name,
119 120
            jsondetails.course_image_name
        )
121 122 123 124 125
        jsondetails.language = "hr"
        self.assertEqual(
            CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).language,
            jsondetails.language
        )
126

127 128
    @override_settings(MKTG_URLS={'ROOT': 'dummy-root'})
    def test_marketing_site_fetch(self):
129
        settings_details_url = get_url(self.course.id)
130

131
        with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}):
Don Mitchell committed
132
            response = self.client.get_html(settings_details_url)
133 134
            self.assertNotContains(response, "Course Summary Page")
            self.assertNotContains(response, "Send a note to students via email")
cahrens committed
135
            self.assertContains(response, "course summary page will not be viewable")
136 137 138

            self.assertContains(response, "Course Start Date")
            self.assertContains(response, "Course End Date")
139 140
            self.assertContains(response, "Enrollment Start Date")
            self.assertContains(response, "Enrollment End Date")
141 142
            self.assertContains(response, "not the dates shown on your course summary page")

143 144
            self.assertContains(response, "Introducing Your Course")
            self.assertContains(response, "Course Image")
145
            self.assertContains(response, "Course Short Description")
146 147
            self.assertNotContains(response, "Course Overview")
            self.assertNotContains(response, "Course Introduction Video")
148 149
            self.assertNotContains(response, "Requirements")

150
    @unittest.skipUnless(settings.FEATURES.get('ENTRANCE_EXAMS', False), True)
151
    def test_entrance_exam_created_updated_and_deleted_successfully(self):
152
        seed_milestone_relationship_types()
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
        settings_details_url = get_url(self.course.id)
        data = {
            'entrance_exam_enabled': 'true',
            'entrance_exam_minimum_score_pct': '60',
            'syllabus': 'none',
            'short_description': 'empty',
            'overview': '',
            'effort': '',
            'intro_video': ''
        }
        response = self.client.post(settings_details_url, data=json.dumps(data), content_type='application/json',
                                    HTTP_ACCEPT='application/json')
        self.assertEquals(response.status_code, 200)
        course = modulestore().get_course(self.course.id)
        self.assertTrue(course.entrance_exam_enabled)
        self.assertEquals(course.entrance_exam_minimum_score_pct, .60)

170 171 172 173 174 175 176 177 178 179 180 181 182 183
        # Update the entrance exam
        data['entrance_exam_enabled'] = "true"
        data['entrance_exam_minimum_score_pct'] = "80"
        response = self.client.post(
            settings_details_url,
            data=json.dumps(data),
            content_type='application/json',
            HTTP_ACCEPT='application/json'
        )
        self.assertEquals(response.status_code, 200)
        course = modulestore().get_course(self.course.id)
        self.assertTrue(course.entrance_exam_enabled)
        self.assertEquals(course.entrance_exam_minimum_score_pct, .80)

184 185 186 187 188 189 190 191 192 193 194 195 196
        # Delete the entrance exam
        data['entrance_exam_enabled'] = "false"
        response = self.client.post(
            settings_details_url,
            data=json.dumps(data),
            content_type='application/json',
            HTTP_ACCEPT='application/json'
        )
        course = modulestore().get_course(self.course.id)
        self.assertEquals(response.status_code, 200)
        self.assertFalse(course.entrance_exam_enabled)
        self.assertEquals(course.entrance_exam_minimum_score_pct, None)

197
    @unittest.skipUnless(settings.FEATURES.get('ENTRANCE_EXAMS', False), True)
198 199 200 201 202
    def test_entrance_exam_store_default_min_score(self):
        """
        test that creating an entrance exam should store the default value, if key missing in json request
        or entrance_exam_minimum_score_pct is an empty string
        """
203
        seed_milestone_relationship_types()
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247
        settings_details_url = get_url(self.course.id)
        test_data_1 = {
            'entrance_exam_enabled': 'true',
            'syllabus': 'none',
            'short_description': 'empty',
            'overview': '',
            'effort': '',
            'intro_video': ''
        }
        response = self.client.post(
            settings_details_url,
            data=json.dumps(test_data_1),
            content_type='application/json',
            HTTP_ACCEPT='application/json'
        )
        self.assertEquals(response.status_code, 200)
        course = modulestore().get_course(self.course.id)
        self.assertTrue(course.entrance_exam_enabled)

        # entrance_exam_minimum_score_pct is not present in the request so default value should be saved.
        self.assertEquals(course.entrance_exam_minimum_score_pct, .5)

        #add entrance_exam_minimum_score_pct with empty value in json request.
        test_data_2 = {
            'entrance_exam_enabled': 'true',
            'entrance_exam_minimum_score_pct': '',
            'syllabus': 'none',
            'short_description': 'empty',
            'overview': '',
            'effort': '',
            'intro_video': ''
        }

        response = self.client.post(
            settings_details_url,
            data=json.dumps(test_data_2),
            content_type='application/json',
            HTTP_ACCEPT='application/json'
        )
        self.assertEquals(response.status_code, 200)
        course = modulestore().get_course(self.course.id)
        self.assertTrue(course.entrance_exam_enabled)
        self.assertEquals(course.entrance_exam_minimum_score_pct, .5)

248
    def test_editable_short_description_fetch(self):
249
        settings_details_url = get_url(self.course.id)
250 251 252 253 254

        with mock.patch.dict('django.conf.settings.FEATURES', {'EDITABLE_SHORT_DESCRIPTION': False}):
            response = self.client.get_html(settings_details_url)
            self.assertNotContains(response, "Course Short Description")

255
    def test_regular_site_fetch(self):
256
        settings_details_url = get_url(self.course.id)
257

258
        with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': False}):
Don Mitchell committed
259
            response = self.client.get_html(settings_details_url)
260
            self.assertContains(response, "Course Summary Page")
261
            self.assertContains(response, "Send a note to students via email")
cahrens committed
262
            self.assertNotContains(response, "course summary page will not be viewable")
263 264 265 266 267 268 269 270

            self.assertContains(response, "Course Start Date")
            self.assertContains(response, "Course End Date")
            self.assertContains(response, "Enrollment Start Date")
            self.assertContains(response, "Enrollment End Date")
            self.assertNotContains(response, "not the dates shown on your course summary page")

            self.assertContains(response, "Introducing Your Course")
Chris Dodge committed
271
            self.assertContains(response, "Course Image")
272
            self.assertContains(response, "Course Short Description")
273 274
            self.assertContains(response, "Course Overview")
            self.assertContains(response, "Course Introduction Video")
275 276
            self.assertContains(response, "Requirements")

Calen Pennington committed
277

278
class CourseDetailsViewTest(CourseTestCase):
cahrens committed
279 280 281
    """
    Tests for modifying content on the first course settings page (course dates, overview, etc.).
    """
282 283 284
    def setUp(self):
        super(CourseDetailsViewTest, self).setUp()

285
    def alter_field(self, url, details, field, val):
Don Mitchell committed
286 287 288
        """
        Change the one field to the given value and then invoke the update post to see if it worked.
        """
289
        setattr(details, field, val)
290 291 292 293 294 295
        # Need to partially serialize payload b/c the mock doesn't handle it correctly
        payload = copy.copy(details.__dict__)
        payload['start_date'] = CourseDetailsViewTest.convert_datetime_to_iso(details.start_date)
        payload['end_date'] = CourseDetailsViewTest.convert_datetime_to_iso(details.end_date)
        payload['enrollment_start'] = CourseDetailsViewTest.convert_datetime_to_iso(details.enrollment_start)
        payload['enrollment_end'] = CourseDetailsViewTest.convert_datetime_to_iso(details.enrollment_end)
296
        resp = self.client.ajax_post(url, payload)
Don Mitchell committed
297
        self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, field + str(val))
Calen Pennington committed
298

299
    @staticmethod
David Baumgold committed
300
    def convert_datetime_to_iso(datetime_obj):
Don Mitchell committed
301 302 303
        """
        Use the xblock serializer to convert the datetime
        """
David Baumgold committed
304
        return Date().to_json(datetime_obj)
305

306
    def test_update_and_fetch(self):
307
        details = CourseDetails.fetch(self.course.id)
Calen Pennington committed
308 309

        # resp s/b json from here on
310
        url = get_url(self.course.id)
Don Mitchell committed
311
        resp = self.client.get_json(url)
312 313
        self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, "virgin get")

Don Mitchell committed
314
        utc = UTC()
Calen Pennington committed
315 316 317 318
        self.alter_field(url, details, 'start_date', datetime.datetime(2012, 11, 12, 1, 30, tzinfo=utc))
        self.alter_field(url, details, 'start_date', datetime.datetime(2012, 11, 1, 13, 30, tzinfo=utc))
        self.alter_field(url, details, 'end_date', datetime.datetime(2013, 2, 12, 1, 30, tzinfo=utc))
        self.alter_field(url, details, 'enrollment_start', datetime.datetime(2012, 10, 12, 1, 30, tzinfo=utc))
319

Calen Pennington committed
320
        self.alter_field(url, details, 'enrollment_end', datetime.datetime(2012, 11, 15, 1, 30, tzinfo=utc))
321
        self.alter_field(url, details, 'short_description', "Short Description")
322 323 324
        self.alter_field(url, details, 'overview', "Overview")
        self.alter_field(url, details, 'intro_video', "intro_video")
        self.alter_field(url, details, 'effort', "effort")
325
        self.alter_field(url, details, 'course_image_name', "course_image_name")
326
        self.alter_field(url, details, 'language', "en")
327 328

    def compare_details_with_encoding(self, encoded, details, context):
Don Mitchell committed
329 330 331
        """
        compare all of the fields of the before and after dicts
        """
332 333 334 335
        self.compare_date_fields(details, encoded, context, 'start_date')
        self.compare_date_fields(details, encoded, context, 'end_date')
        self.compare_date_fields(details, encoded, context, 'enrollment_start')
        self.compare_date_fields(details, encoded, context, 'enrollment_end')
336
        self.assertEqual(details['short_description'], encoded['short_description'], context + " short_description not ==")
337 338 339
        self.assertEqual(details['overview'], encoded['overview'], context + " overviews not ==")
        self.assertEqual(details['intro_video'], encoded.get('intro_video', None), context + " intro_video not ==")
        self.assertEqual(details['effort'], encoded['effort'], context + " efforts not ==")
340
        self.assertEqual(details['course_image_name'], encoded['course_image_name'], context + " images not ==")
341
        self.assertEqual(details['language'], encoded['language'], context + " languages not ==")
Calen Pennington committed
342

343
    def compare_date_fields(self, details, encoded, context, field):
Don Mitchell committed
344 345 346
        """
        Compare the given date fields between the before and after doing json deserialization
        """
347
        if details[field] is not None:
348
            date = Date()
349
            if field in encoded and encoded[field] is not None:
350
                dt1 = date.from_json(encoded[field])
351
                dt2 = details[field]
Calen Pennington committed
352

353
                self.assertEqual(dt1, dt2, msg="{} != {} at {}".format(dt1, dt2, context))
354 355 356 357
            else:
                self.fail(field + " missing from encoded but in details at " + context)
        elif field in encoded and encoded[field] is not None:
            self.fail(field + " included in encoding but missing from details at " + context)
Calen Pennington committed
358

359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407
    @mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PREREQUISITE_COURSES': True, 'MILESTONES_APP': True})
    def test_pre_requisite_course_list_present(self):
        seed_milestone_relationship_types()
        settings_details_url = get_url(self.course.id)
        response = self.client.get_html(settings_details_url)
        self.assertContains(response, "Prerequisite Course")

    @mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PREREQUISITE_COURSES': True, 'MILESTONES_APP': True})
    def test_pre_requisite_course_update_and_fetch(self):
        seed_milestone_relationship_types()
        url = get_url(self.course.id)
        resp = self.client.get_json(url)
        course_detail_json = json.loads(resp.content)
        # assert pre_requisite_courses is initialized
        self.assertEqual([], course_detail_json['pre_requisite_courses'])

        # update pre requisite courses with a new course keys
        pre_requisite_course = CourseFactory.create(org='edX', course='900', run='test_run')
        pre_requisite_course2 = CourseFactory.create(org='edX', course='902', run='test_run')
        pre_requisite_course_keys = [unicode(pre_requisite_course.id), unicode(pre_requisite_course2.id)]
        course_detail_json['pre_requisite_courses'] = pre_requisite_course_keys
        self.client.ajax_post(url, course_detail_json)

        # fetch updated course to assert pre_requisite_courses has new values
        resp = self.client.get_json(url)
        course_detail_json = json.loads(resp.content)
        self.assertEqual(pre_requisite_course_keys, course_detail_json['pre_requisite_courses'])

        # remove pre requisite course
        course_detail_json['pre_requisite_courses'] = []
        self.client.ajax_post(url, course_detail_json)
        resp = self.client.get_json(url)
        course_detail_json = json.loads(resp.content)
        self.assertEqual([], course_detail_json['pre_requisite_courses'])

    @mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PREREQUISITE_COURSES': True, 'MILESTONES_APP': True})
    def test_invalid_pre_requisite_course(self):
        seed_milestone_relationship_types()
        url = get_url(self.course.id)
        resp = self.client.get_json(url)
        course_detail_json = json.loads(resp.content)

        # update pre requisite courses one valid and one invalid key
        pre_requisite_course = CourseFactory.create(org='edX', course='900', run='test_run')
        pre_requisite_course_keys = [unicode(pre_requisite_course.id), 'invalid_key']
        course_detail_json['pre_requisite_courses'] = pre_requisite_course_keys
        response = self.client.ajax_post(url, course_detail_json)
        self.assertEqual(400, response.status_code)

Calen Pennington committed
408

409
@ddt.ddt
410
class CourseGradingTest(CourseTestCase):
cahrens committed
411 412 413
    """
    Tests for the course settings grading page.
    """
414
    def test_initial_grader(self):
Don Mitchell committed
415 416 417
        test_grader = CourseGradingModel(self.course)
        self.assertIsNotNone(test_grader.graders)
        self.assertIsNotNone(test_grader.grade_cutoffs)
418 419

    def test_fetch_grader(self):
420
        test_grader = CourseGradingModel.fetch(self.course.id)
421 422
        self.assertIsNotNone(test_grader.graders, "No graders")
        self.assertIsNotNone(test_grader.grade_cutoffs, "No cutoffs")
Calen Pennington committed
423

424
        for i, grader in enumerate(test_grader.graders):
425
            subgrader = CourseGradingModel.fetch_grader(self.course.id, i)
426
            self.assertDictEqual(grader, subgrader, str(i) + "th graders not equal")
Calen Pennington committed
427

428 429 430 431
    @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
    def test_update_from_json(self, store):
        self.course = CourseFactory.create(default_store=store)

432 433
        test_grader = CourseGradingModel.fetch(self.course.id)
        altered_grader = CourseGradingModel.update_from_json(self.course.id, test_grader.__dict__, self.user)
434
        self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "Noop update")
Calen Pennington committed
435

436
        test_grader.graders[0]['weight'] = test_grader.graders[0].get('weight') * 2
437
        altered_grader = CourseGradingModel.update_from_json(self.course.id, test_grader.__dict__, self.user)
438
        self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "Weight[0] * 2")
Calen Pennington committed
439

440 441 442 443 444 445 446 447 448 449 450 451
        # test for bug LMS-11485
        with modulestore().bulk_operations(self.course.id):
            new_grader = test_grader.graders[0].copy()
            new_grader['type'] += '_foo'
            new_grader['short_label'] += '_foo'
            new_grader['id'] = len(test_grader.graders)
            test_grader.graders.append(new_grader)
            # don't use altered cached def, get a fresh one
            CourseGradingModel.update_from_json(self.course.id, test_grader.__dict__, self.user)
            altered_grader = CourseGradingModel.fetch(self.course.id)
            self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__)

452
        test_grader.grade_cutoffs['D'] = 0.3
453
        altered_grader = CourseGradingModel.update_from_json(self.course.id, test_grader.__dict__, self.user)
454
        self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "cutoff add D")
Calen Pennington committed
455

456
        test_grader.grace_period = {'hours': 4, 'minutes': 5, 'seconds': 0}
457
        altered_grader = CourseGradingModel.update_from_json(self.course.id, test_grader.__dict__, self.user)
458
        self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "4 hour grace period")
Calen Pennington committed
459

460
    def test_update_grader_from_json(self):
461
        test_grader = CourseGradingModel.fetch(self.course.id)
462
        altered_grader = CourseGradingModel.update_grader_from_json(
463
            self.course.id, test_grader.graders[1], self.user
464
        )
465
        self.assertDictEqual(test_grader.graders[1], altered_grader, "Noop update")
Calen Pennington committed
466

467
        test_grader.graders[1]['min_count'] = test_grader.graders[1].get('min_count') + 2
468
        altered_grader = CourseGradingModel.update_grader_from_json(
469
            self.course.id, test_grader.graders[1], self.user)
470
        self.assertDictEqual(test_grader.graders[1], altered_grader, "min_count[1] + 2")
Calen Pennington committed
471

472
        test_grader.graders[1]['drop_count'] = test_grader.graders[1].get('drop_count') + 1
473
        altered_grader = CourseGradingModel.update_grader_from_json(
474
            self.course.id, test_grader.graders[1], self.user)
475
        self.assertDictEqual(test_grader.graders[1], altered_grader, "drop_count[1] + 2")
476

477
    def test_update_cutoffs_from_json(self):
478 479
        test_grader = CourseGradingModel.fetch(self.course.id)
        CourseGradingModel.update_cutoffs_from_json(self.course.id, test_grader.grade_cutoffs, self.user)
480 481
        # Unlike other tests, need to actually perform a db fetch for this test since update_cutoffs_from_json
        #  simply returns the cutoffs you send into it, rather than returning the db contents.
482
        altered_grader = CourseGradingModel.fetch(self.course.id)
483 484 485
        self.assertDictEqual(test_grader.grade_cutoffs, altered_grader.grade_cutoffs, "Noop update")

        test_grader.grade_cutoffs['D'] = 0.3
486 487
        CourseGradingModel.update_cutoffs_from_json(self.course.id, test_grader.grade_cutoffs, self.user)
        altered_grader = CourseGradingModel.fetch(self.course.id)
488 489 490
        self.assertDictEqual(test_grader.grade_cutoffs, altered_grader.grade_cutoffs, "cutoff add D")

        test_grader.grade_cutoffs['Pass'] = 0.75
491 492
        CourseGradingModel.update_cutoffs_from_json(self.course.id, test_grader.grade_cutoffs, self.user)
        altered_grader = CourseGradingModel.fetch(self.course.id)
493 494 495
        self.assertDictEqual(test_grader.grade_cutoffs, altered_grader.grade_cutoffs, "cutoff change 'Pass'")

    def test_delete_grace_period(self):
496
        test_grader = CourseGradingModel.fetch(self.course.id)
497
        CourseGradingModel.update_grace_period_from_json(
498
            self.course.id, test_grader.grace_period, self.user
499
        )
500
        # update_grace_period_from_json doesn't return anything, so query the db for its contents.
501
        altered_grader = CourseGradingModel.fetch(self.course.id)
502 503 504
        self.assertEqual(test_grader.grace_period, altered_grader.grace_period, "Noop update")

        test_grader.grace_period = {'hours': 15, 'minutes': 5, 'seconds': 30}
505
        CourseGradingModel.update_grace_period_from_json(
506 507
            self.course.id, test_grader.grace_period, self.user)
        altered_grader = CourseGradingModel.fetch(self.course.id)
508 509 510 511
        self.assertDictEqual(test_grader.grace_period, altered_grader.grace_period, "Adding in a grace period")

        test_grader.grace_period = {'hours': 1, 'minutes': 10, 'seconds': 0}
        # Now delete the grace period
512
        CourseGradingModel.delete_grace_period(self.course.id, self.user)
513
        # update_grace_period_from_json doesn't return anything, so query the db for its contents.
514
        altered_grader = CourseGradingModel.fetch(self.course.id)
515 516 517 518 519
        # Once deleted, the grace period should simply be None
        self.assertEqual(None, altered_grader.grace_period, "Delete grace period")

    def test_update_section_grader_type(self):
        # Get the descriptor and the section_grader_type and assert they are the default values
520
        descriptor = modulestore().get_item(self.course.location)
521
        section_grader_type = CourseGradingModel.get_section_grader_type(self.course.location)
522

523
        self.assertEqual('notgraded', section_grader_type['graderType'])
Calen Pennington committed
524 525
        self.assertEqual(None, descriptor.format)
        self.assertEqual(False, descriptor.graded)
526 527

        # Change the default grader type to Homework, which should also mark the section as graded
528
        CourseGradingModel.update_section_grader_type(self.course, 'Homework', self.user)
529
        descriptor = modulestore().get_item(self.course.location)
530
        section_grader_type = CourseGradingModel.get_section_grader_type(self.course.location)
531 532

        self.assertEqual('Homework', section_grader_type['graderType'])
Calen Pennington committed
533 534
        self.assertEqual('Homework', descriptor.format)
        self.assertEqual(True, descriptor.graded)
535

536
        # Change the grader type back to notgraded, which should also unmark the section as graded
537
        CourseGradingModel.update_section_grader_type(self.course, 'notgraded', self.user)
538
        descriptor = modulestore().get_item(self.course.location)
539
        section_grader_type = CourseGradingModel.get_section_grader_type(self.course.location)
540

541
        self.assertEqual('notgraded', section_grader_type['graderType'])
Calen Pennington committed
542 543
        self.assertEqual(None, descriptor.format)
        self.assertEqual(False, descriptor.graded)
544

Don Mitchell committed
545 546 547 548
    def test_get_set_grader_types_ajax(self):
        """
        Test configuring the graders via ajax calls
        """
549
        grader_type_url_base = get_url(self.course.id, 'grading_handler')
Don Mitchell committed
550 551 552 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 589 590 591 592 593 594 595 596
        # test get whole
        response = self.client.get_json(grader_type_url_base)
        whole_model = json.loads(response.content)
        self.assertIn('graders', whole_model)
        self.assertIn('grade_cutoffs', whole_model)
        self.assertIn('grace_period', whole_model)
        # test post/update whole
        whole_model['grace_period'] = {'hours': 1, 'minutes': 30, 'seconds': 0}
        response = self.client.ajax_post(grader_type_url_base, whole_model)
        self.assertEqual(200, response.status_code)
        response = self.client.get_json(grader_type_url_base)
        whole_model = json.loads(response.content)
        self.assertEqual(whole_model['grace_period'], {'hours': 1, 'minutes': 30, 'seconds': 0})
        # test get one grader
        self.assertGreater(len(whole_model['graders']), 1)  # ensure test will make sense
        response = self.client.get_json(grader_type_url_base + '/1')
        grader_sample = json.loads(response.content)
        self.assertEqual(grader_sample, whole_model['graders'][1])
        # test add grader
        new_grader = {
            "type": "Extra Credit",
            "min_count": 1,
            "drop_count": 2,
            "short_label": None,
            "weight": 15,
        }
        response = self.client.ajax_post(
            '{}/{}'.format(grader_type_url_base, len(whole_model['graders'])),
            new_grader
        )
        self.assertEqual(200, response.status_code)
        grader_sample = json.loads(response.content)
        new_grader['id'] = len(whole_model['graders'])
        self.assertEqual(new_grader, grader_sample)
        # test delete grader
        response = self.client.delete(grader_type_url_base + '/1', HTTP_ACCEPT="application/json")
        self.assertEqual(204, response.status_code)
        response = self.client.get_json(grader_type_url_base)
        updated_model = json.loads(response.content)
        new_grader['id'] -= 1  # one fewer and the id mutates
        self.assertIn(new_grader, updated_model['graders'])
        self.assertNotIn(whole_model['graders'][1], updated_model['graders'])

    def setup_test_set_get_section_grader_ajax(self):
        """
        Populate the course, grab a section, get the url for the assignment type access
        """
597
        self.populate_course()
598
        sections = modulestore().get_items(self.course.id, qualifiers={'category': "sequential"})
Don Mitchell committed
599 600 601
        # see if test makes sense
        self.assertGreater(len(sections), 0, "No sections found")
        section = sections[0]  # just take the first one
602
        return reverse_usage_url('xblock_handler', section.location)
Don Mitchell committed
603 604 605 606 607 608 609 610 611 612 613

    def test_set_get_section_grader_ajax(self):
        """
        Test setting and getting section grades via the grade as url
        """
        grade_type_url = self.setup_test_set_get_section_grader_ajax()
        response = self.client.ajax_post(grade_type_url, {'graderType': u'Homework'})
        self.assertEqual(200, response.status_code)
        response = self.client.get_json(grade_type_url + '?fields=graderType')
        self.assertEqual(json.loads(response.content).get('graderType'), u'Homework')
        # and unset
614
        response = self.client.ajax_post(grade_type_url, {'graderType': u'notgraded'})
Don Mitchell committed
615 616
        self.assertEqual(200, response.status_code)
        response = self.client.get_json(grade_type_url + '?fields=graderType')
617
        self.assertEqual(json.loads(response.content).get('graderType'), u'notgraded')
Don Mitchell committed
618

619

620
class CourseMetadataEditingTest(CourseTestCase):
cahrens committed
621 622 623
    """
    Tests for CourseMetadata.
    """
624 625
    def setUp(self):
        CourseTestCase.setUp(self)
Don Mitchell committed
626
        self.fullcourse = CourseFactory.create()
627 628
        self.course_setting_url = get_url(self.course.id, 'advanced_settings_handler')
        self.fullcourse_setting_url = get_url(self.fullcourse.id, 'advanced_settings_handler')
629 630

    def test_fetch_initial_fields(self):
631
        test_model = CourseMetadata.fetch(self.course)
632
        self.assertIn('display_name', test_model, 'Missing editable metadata field')
Don Mitchell committed
633
        self.assertEqual(test_model['display_name']['value'], self.course.display_name)
634

635
        test_model = CourseMetadata.fetch(self.fullcourse)
636 637
        self.assertNotIn('graceperiod', test_model, 'blacklisted field leaked in')
        self.assertIn('display_name', test_model, 'full missing editable metadata field')
Don Mitchell committed
638
        self.assertEqual(test_model['display_name']['value'], self.fullcourse.display_name)
639
        self.assertIn('rerandomize', test_model, 'Missing rerandomize metadata field')
640 641
        self.assertIn('showanswer', test_model, 'showanswer field ')
        self.assertIn('xqa_key', test_model, 'xqa_key field ')
642

Oleg Marshev committed
643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664
    @patch.dict(settings.FEATURES, {'ENABLE_EXPORT_GIT': True})
    def test_fetch_giturl_present(self):
        """
        If feature flag ENABLE_EXPORT_GIT is on, show the setting as a non-deprecated Advanced Setting.
        """
        test_model = CourseMetadata.fetch(self.fullcourse)
        self.assertIn('giturl', test_model)

    @patch.dict(settings.FEATURES, {'ENABLE_EXPORT_GIT': False})
    def test_fetch_giturl_not_present(self):
        """
        If feature flag ENABLE_EXPORT_GIT is off, don't show the setting at all on the Advanced Settings page.
        """
        test_model = CourseMetadata.fetch(self.fullcourse)
        self.assertNotIn('giturl', test_model)

    @patch.dict(settings.FEATURES, {'ENABLE_EXPORT_GIT': False})
    def test_validate_update_filtered_off(self):
        """
        If feature flag is off, then giturl must be filtered.
        """
        # pylint: disable=unused-variable
665
        is_valid, errors, test_model = CourseMetadata.validate_and_update_from_json(
Oleg Marshev committed
666 667 668 669 670 671 672 673 674 675 676 677 678 679
            self.course,
            {
                "giturl": {"value": "http://example.com"},
            },
            user=self.user
        )
        self.assertNotIn('giturl', test_model)

    @patch.dict(settings.FEATURES, {'ENABLE_EXPORT_GIT': True})
    def test_validate_update_filtered_on(self):
        """
        If feature flag is on, then giturl must not be filtered.
        """
        # pylint: disable=unused-variable
680
        is_valid, errors, test_model = CourseMetadata.validate_and_update_from_json(
Oleg Marshev committed
681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716
            self.course,
            {
                "giturl": {"value": "http://example.com"},
            },
            user=self.user
        )
        self.assertIn('giturl', test_model)

    @patch.dict(settings.FEATURES, {'ENABLE_EXPORT_GIT': True})
    def test_update_from_json_filtered_on(self):
        """
        If feature flag is on, then giturl must be updated.
        """
        test_model = CourseMetadata.update_from_json(
            self.course,
            {
                "giturl": {"value": "http://example.com"},
            },
            user=self.user
        )
        self.assertIn('giturl', test_model)

    @patch.dict(settings.FEATURES, {'ENABLE_EXPORT_GIT': False})
    def test_update_from_json_filtered_off(self):
        """
        If feature flag is on, then giturl must not be updated.
        """
        test_model = CourseMetadata.update_from_json(
            self.course,
            {
                "giturl": {"value": "http://example.com"},
            },
            user=self.user
        )
        self.assertNotIn('giturl', test_model)

717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738
    @patch.dict(settings.FEATURES, {'ENABLE_EDXNOTES': True})
    def test_edxnotes_present(self):
        """
        If feature flag ENABLE_EDXNOTES is on, show the setting as a non-deprecated Advanced Setting.
        """
        test_model = CourseMetadata.fetch(self.fullcourse)
        self.assertIn('edxnotes', test_model)

    @patch.dict(settings.FEATURES, {'ENABLE_EDXNOTES': False})
    def test_edxnotes_not_present(self):
        """
        If feature flag ENABLE_EDXNOTES is off, don't show the setting at all on the Advanced Settings page.
        """
        test_model = CourseMetadata.fetch(self.fullcourse)
        self.assertNotIn('edxnotes', test_model)

    @patch.dict(settings.FEATURES, {'ENABLE_EDXNOTES': False})
    def test_validate_update_filtered_edxnotes_off(self):
        """
        If feature flag is off, then edxnotes must be filtered.
        """
        # pylint: disable=unused-variable
739
        is_valid, errors, test_model = CourseMetadata.validate_and_update_from_json(
740 741 742 743 744 745 746 747 748 749 750 751 752 753
            self.course,
            {
                "edxnotes": {"value": "true"},
            },
            user=self.user
        )
        self.assertNotIn('edxnotes', test_model)

    @patch.dict(settings.FEATURES, {'ENABLE_EDXNOTES': True})
    def test_validate_update_filtered_edxnotes_on(self):
        """
        If feature flag is on, then edxnotes must not be filtered.
        """
        # pylint: disable=unused-variable
754
        is_valid, errors, test_model = CourseMetadata.validate_and_update_from_json(
755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790
            self.course,
            {
                "edxnotes": {"value": "true"},
            },
            user=self.user
        )
        self.assertIn('edxnotes', test_model)

    @patch.dict(settings.FEATURES, {'ENABLE_EDXNOTES': True})
    def test_update_from_json_filtered_edxnotes_on(self):
        """
        If feature flag is on, then edxnotes must be updated.
        """
        test_model = CourseMetadata.update_from_json(
            self.course,
            {
                "edxnotes": {"value": "true"},
            },
            user=self.user
        )
        self.assertIn('edxnotes', test_model)

    @patch.dict(settings.FEATURES, {'ENABLE_EDXNOTES': False})
    def test_update_from_json_filtered_edxnotes_off(self):
        """
        If feature flag is off, then edxnotes must not be updated.
        """
        test_model = CourseMetadata.update_from_json(
            self.course,
            {
                "edxnotes": {"value": "true"},
            },
            user=self.user
        )
        self.assertNotIn('edxnotes', test_model)

791
    def test_validate_from_json_correct_inputs(self):
792
        is_valid, errors, test_model = CourseMetadata.validate_and_update_from_json(
793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808
            self.course,
            {
                "advertised_start": {"value": "start A"},
                "days_early_for_beta": {"value": 2},
                "advanced_modules": {"value": ['combinedopenended']},
            },
            user=self.user
        )
        self.assertTrue(is_valid)
        self.assertTrue(len(errors) == 0)
        self.update_check(test_model)

        # Tab gets tested in test_advanced_settings_munge_tabs
        self.assertIn('advanced_modules', test_model, 'Missing advanced_modules')
        self.assertEqual(test_model['advanced_modules']['value'], ['combinedopenended'], 'advanced_module is not updated')

809
    def test_validate_from_json_wrong_inputs(self):
810
        # input incorrectly formatted data
811
        is_valid, errors, test_model = CourseMetadata.validate_and_update_from_json(
812 813 814 815 816 817 818 819 820 821
            self.course,
            {
                "advertised_start": {"value": 1, "display_name": "Course Advertised Start Date", },
                "days_early_for_beta": {"value": "supposed to be an integer",
                                        "display_name": "Days Early for Beta Users", },
                "advanced_modules": {"value": 1, "display_name": "Advanced Module List", },
            },
            user=self.user
        )

822
        # Check valid results from validate_and_update_from_json
823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840
        self.assertFalse(is_valid)
        self.assertEqual(len(errors), 3)
        self.assertFalse(test_model)

        error_keys = set([error_obj['model']['display_name'] for error_obj in errors])
        test_keys = set(['Advanced Module List', 'Course Advertised Start Date', 'Days Early for Beta Users'])
        self.assertEqual(error_keys, test_keys)

        # try fresh fetch to ensure no update happened
        fresh = modulestore().get_course(self.course.id)
        test_model = CourseMetadata.fetch(fresh)

        self.assertNotEqual(test_model['advertised_start']['value'], 1, 'advertised_start should not be updated to a wrong value')
        self.assertNotEqual(test_model['days_early_for_beta']['value'], "supposed to be an integer",
                            'days_early_for beta should not be updated to a wrong value')

    def test_correct_http_status(self):
        json_data = json.dumps({
841 842 843 844 845 846 847
            "advertised_start": {"value": 1, "display_name": "Course Advertised Start Date", },
            "days_early_for_beta": {
                "value": "supposed to be an integer",
                "display_name": "Days Early for Beta Users",
            },
            "advanced_modules": {"value": 1, "display_name": "Advanced Module List", },
        })
848 849 850
        response = self.client.ajax_post(self.course_setting_url, json_data)
        self.assertEqual(400, response.status_code)

851
    def test_update_from_json(self):
852
        test_model = CourseMetadata.update_from_json(
853 854
            self.course,
            {
855 856
                "advertised_start": {"value": "start A"},
                "days_early_for_beta": {"value": 2},
857 858
            },
            user=self.user
859
        )
860
        self.update_check(test_model)
861
        # try fresh fetch to ensure persistence
862
        fresh = modulestore().get_course(self.course.id)
863
        test_model = CourseMetadata.fetch(fresh)
864
        self.update_check(test_model)
865
        # now change some of the existing metadata
866
        test_model = CourseMetadata.update_from_json(
867 868
            fresh,
            {
869 870
                "advertised_start": {"value": "start B"},
                "display_name": {"value": "jolly roger"},
871 872
            },
            user=self.user
873
        )
874
        self.assertIn('display_name', test_model, 'Missing editable metadata field')
875
        self.assertEqual(test_model['display_name']['value'], 'jolly roger', "not expected value")
876
        self.assertIn('advertised_start', test_model, 'Missing revised advertised_start metadata field')
877
        self.assertEqual(test_model['advertised_start']['value'], 'start B', "advertised_start not expected value")
878

879
    def update_check(self, test_model):
880 881 882
        """
        checks that updates were made
        """
883
        self.assertIn('display_name', test_model, 'Missing editable metadata field')
Don Mitchell committed
884
        self.assertEqual(test_model['display_name']['value'], self.course.display_name)
885
        self.assertIn('advertised_start', test_model, 'Missing new advertised_start metadata field')
886
        self.assertEqual(test_model['advertised_start']['value'], 'start A', "advertised_start not expected value")
887
        self.assertIn('days_early_for_beta', test_model, 'Missing days_early_for_beta metadata field')
888
        self.assertEqual(test_model['days_early_for_beta']['value'], 2, "days_early_for_beta not expected value")
889

890 891 892 893
    def test_http_fetch_initial_fields(self):
        response = self.client.get_json(self.course_setting_url)
        test_model = json.loads(response.content)
        self.assertIn('display_name', test_model, 'Missing editable metadata field')
Don Mitchell committed
894
        self.assertEqual(test_model['display_name']['value'], self.course.display_name)
895 896 897 898 899

        response = self.client.get_json(self.fullcourse_setting_url)
        test_model = json.loads(response.content)
        self.assertNotIn('graceperiod', test_model, 'blacklisted field leaked in')
        self.assertIn('display_name', test_model, 'full missing editable metadata field')
Don Mitchell committed
900
        self.assertEqual(test_model['display_name']['value'], self.fullcourse.display_name)
901 902 903 904 905 906
        self.assertIn('rerandomize', test_model, 'Missing rerandomize metadata field')
        self.assertIn('showanswer', test_model, 'showanswer field ')
        self.assertIn('xqa_key', test_model, 'xqa_key field ')

    def test_http_update_from_json(self):
        response = self.client.ajax_post(self.course_setting_url, {
907 908
            "advertised_start": {"value": "start A"},
            "days_early_for_beta": {"value": 2},
909 910 911 912 913 914 915 916 917
        })
        test_model = json.loads(response.content)
        self.update_check(test_model)

        response = self.client.get_json(self.course_setting_url)
        test_model = json.loads(response.content)
        self.update_check(test_model)
        # now change some of the existing metadata
        response = self.client.ajax_post(self.course_setting_url, {
918 919
            "advertised_start": {"value": "start B"},
            "display_name": {"value": "jolly roger"}
920 921 922
        })
        test_model = json.loads(response.content)
        self.assertIn('display_name', test_model, 'Missing editable metadata field')
923
        self.assertEqual(test_model['display_name']['value'], 'jolly roger', "not expected value")
924
        self.assertIn('advertised_start', test_model, 'Missing revised advertised_start metadata field')
925
        self.assertEqual(test_model['advertised_start']['value'], 'start B', "advertised_start not expected value")
926 927 928 929 930

    def test_advanced_components_munge_tabs(self):
        """
        Test that adding and removing specific advanced components adds and removes tabs.
        """
931 932 933 934 935 936 937 938 939 940
        open_ended_tab = {"type": "open_ended", "name": "Open Ended Panel"}
        peer_grading_tab = {"type": "peer_grading", "name": "Peer grading"}
        notes_tab = {"type": "notes", "name": "My Notes"}

        # First ensure that none of the tabs are visible
        self.assertNotIn(open_ended_tab, self.course.tabs)
        self.assertNotIn(peer_grading_tab, self.course.tabs)
        self.assertNotIn(notes_tab, self.course.tabs)

        # Now add the "combinedopenended" component and verify that the tab has been added
941
        self.client.ajax_post(self.course_setting_url, {
942
            ADVANCED_COMPONENT_POLICY_KEY: {"value": ["combinedopenended"]}
943
        })
944
        course = modulestore().get_course(self.course.id)
945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967
        self.assertIn(open_ended_tab, course.tabs)
        self.assertIn(peer_grading_tab, course.tabs)
        self.assertNotIn(notes_tab, course.tabs)

        # Now enable student notes and verify that the "My Notes" tab has also been added
        self.client.ajax_post(self.course_setting_url, {
            ADVANCED_COMPONENT_POLICY_KEY: {"value": ["combinedopenended", "notes"]}
        })
        course = modulestore().get_course(self.course.id)
        self.assertIn(open_ended_tab, course.tabs)
        self.assertIn(peer_grading_tab, course.tabs)
        self.assertIn(notes_tab, course.tabs)

        # Now remove the "combinedopenended" component and verify that the tab is gone
        self.client.ajax_post(self.course_setting_url, {
            ADVANCED_COMPONENT_POLICY_KEY: {"value": ["notes"]}
        })
        course = modulestore().get_course(self.course.id)
        self.assertNotIn(open_ended_tab, course.tabs)
        self.assertNotIn(peer_grading_tab, course.tabs)
        self.assertIn(notes_tab, course.tabs)

        # Finally disable student notes and verify that the "My Notes" tab is gone
968
        self.client.ajax_post(self.course_setting_url, {
969
            ADVANCED_COMPONENT_POLICY_KEY: {"value": [""]}
970
        })
971
        course = modulestore().get_course(self.course.id)
972 973 974
        self.assertNotIn(open_ended_tab, course.tabs)
        self.assertNotIn(peer_grading_tab, course.tabs)
        self.assertNotIn(notes_tab, course.tabs)
975

976 977

class CourseGraderUpdatesTest(CourseTestCase):
Don Mitchell committed
978 979 980
    """
    Test getting, deleting, adding, & updating graders
    """
981
    def setUp(self):
Don Mitchell committed
982
        """Compute the url to use in tests"""
983
        super(CourseGraderUpdatesTest, self).setUp()
984
        self.url = get_url(self.course.id, 'grading_handler')
Don Mitchell committed
985
        self.starting_graders = CourseGradingModel(self.course).graders
986 987

    def test_get(self):
Don Mitchell committed
988 989
        """Test getting a specific grading type record."""
        resp = self.client.get_json(self.url + '/0')
990
        self.assertEqual(resp.status_code, 200)
991
        obj = json.loads(resp.content)
Don Mitchell committed
992
        self.assertEqual(self.starting_graders[0], obj)
993 994

    def test_delete(self):
Don Mitchell committed
995 996
        """Test deleting a specific grading type record."""
        resp = self.client.delete(self.url + '/0', HTTP_ACCEPT="application/json")
997
        self.assertEqual(resp.status_code, 204)
998
        current_graders = CourseGradingModel.fetch(self.course.id).graders
Don Mitchell committed
999 1000
        self.assertNotIn(self.starting_graders[0], current_graders)
        self.assertEqual(len(self.starting_graders) - 1, len(current_graders))
1001

Don Mitchell committed
1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015
    def test_update(self):
        """Test updating a specific grading type record."""
        grader = {
            "id": 0,
            "type": "manual",
            "min_count": 5,
            "drop_count": 10,
            "short_label": "yo momma",
            "weight": 17.3,
        }
        resp = self.client.ajax_post(self.url + '/0', grader)
        self.assertEqual(resp.status_code, 200)
        obj = json.loads(resp.content)
        self.assertEqual(obj, grader)
1016
        current_graders = CourseGradingModel.fetch(self.course.id).graders
Don Mitchell committed
1017 1018 1019 1020 1021 1022 1023
        self.assertEqual(len(self.starting_graders), len(current_graders))

    def test_add(self):
        """Test adding a grading type record."""
        # the same url works for changing the whole grading model (graceperiod, cutoffs, and grading types) when
        # the grading_index is None; thus, using None to imply adding a grading_type doesn't work; so, it uses an
        # index out of bounds to imply create item.
1024 1025 1026 1027 1028 1029 1030
        grader = {
            "type": "manual",
            "min_count": 5,
            "drop_count": 10,
            "short_label": "yo momma",
            "weight": 17.3,
        }
Don Mitchell committed
1031
        resp = self.client.ajax_post('{}/{}'.format(self.url, len(self.starting_graders) + 1), grader)
1032
        self.assertEqual(resp.status_code, 200)
1033
        obj = json.loads(resp.content)
Don Mitchell committed
1034 1035 1036
        self.assertEqual(obj['id'], len(self.starting_graders))
        del obj['id']
        self.assertEqual(obj, grader)
1037
        current_graders = CourseGradingModel.fetch(self.course.id).graders
Don Mitchell committed
1038
        self.assertEqual(len(self.starting_graders) + 1, len(current_graders))