Exemple #1
0
def course_grader_updates(request, org, course, name, grader_index=None):
    """
    restful CRUD operations on course_info updates. This differs from get_course_settings by communicating purely
    through json (not rendering any html) and handles section level operations rather than whole page.

    org, course: Attributes of the Location for the item to edit
    """

    location = get_location_and_verify_access(request, org, course, name)

    real_method = get_request_method(request)

    if real_method == 'GET':
        # Cannot just do a get w/o knowing the course name :-(
        return HttpResponse(json.dumps(
            CourseGradingModel.fetch_grader(Location(location), grader_index)),
                            mimetype="application/json")
    elif real_method == "DELETE":
        # ??? Should this return anything? Perhaps success fail?
        CourseGradingModel.delete_grader(Location(location), grader_index)
        return HttpResponse()
    elif request.method == 'POST':  # post or put, doesn't matter.
        return HttpResponse(json.dumps(
            CourseGradingModel.update_grader_from_json(Location(location),
                                                       request.POST)),
                            mimetype="application/json")
    def test_fetch_cutoffs(self):
        test_grader = CourseGradingModel.fetch_cutoffs(self.course.location)
        # ??? should this check that it's at least a dict? (expected is { "pass" : 0.5 } I think)
        self.assertIsNotNone(test_grader, "No cutoffs via fetch")

        test_grader = CourseGradingModel.fetch_cutoffs(self.course.location.url())
        self.assertIsNotNone(test_grader, "No cutoffs via fetch with url")
    def test_fetch_grace(self):
        test_grader = CourseGradingModel.fetch_grace_period(self.course.location)
        # almost a worthless test
        self.assertIn('grace_period', test_grader, "No grace via fetch")

        test_grader = CourseGradingModel.fetch_grace_period(self.course.location.url())
        self.assertIn('grace_period', test_grader, "No cutoffs via fetch with url")
Exemple #4
0
    def test_update_from_json(self):
        test_grader = CourseGradingModel.fetch(self.course_locator)
        altered_grader = CourseGradingModel.update_from_json(
            self.course_locator, test_grader.__dict__, self.user)
        self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__,
                             "Noop update")

        test_grader.graders[0]['weight'] = test_grader.graders[0].get(
            'weight') * 2
        altered_grader = CourseGradingModel.update_from_json(
            self.course_locator, test_grader.__dict__, self.user)
        self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__,
                             "Weight[0] * 2")

        test_grader.grade_cutoffs['D'] = 0.3
        altered_grader = CourseGradingModel.update_from_json(
            self.course_locator, test_grader.__dict__, self.user)
        self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__,
                             "cutoff add D")

        test_grader.grace_period = {'hours': 4, 'minutes': 5, 'seconds': 0}
        altered_grader = CourseGradingModel.update_from_json(
            self.course_locator, test_grader.__dict__, self.user)
        self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__,
                             "4 hour grace period")
def course_grader_updates(request, org, course, name, grader_index=None):
    """
    Restful CRUD operations on course_info updates. This differs from
    get_course_settings by communicating purely through json (not rendering any
    html) and handles section level operations rather than whole page.

    org, course: Attributes of the Location for the item to edit
    """

    location = get_location_and_verify_access(request, org, course, name)

    if request.method == 'GET':
        # Cannot just do a get w/o knowing the course name :-(
        return JsonResponse(CourseGradingModel.fetch_grader(
                Location(location), grader_index
        ))
    elif request.method == "DELETE":
        # ??? Should this return anything? Perhaps success fail?
        CourseGradingModel.delete_grader(Location(location), grader_index)
        return JsonResponse()
    else:  # post or put, doesn't matter.
        return JsonResponse(CourseGradingModel.update_grader_from_json(
                Location(location),
                request.POST
        ))
    def test_update_from_json(self):
        test_grader = CourseGradingModel.fetch(self.course_location)
        altered_grader = CourseGradingModel.update_from_json(
            test_grader.__dict__)
        self.assertDictEqual(
            test_grader.__dict__, altered_grader.__dict__, "Noop update")

        test_grader.graders[0][
            'weight'] = test_grader.graders[0].get('weight') * 2
        altered_grader = CourseGradingModel.update_from_json(
            test_grader.__dict__)
        self.assertDictEqual(
            test_grader.__dict__, altered_grader.__dict__, "Weight[0] * 2")

        test_grader.grade_cutoffs['D'] = 0.3
        altered_grader = CourseGradingModel.update_from_json(
            test_grader.__dict__)
        self.assertDictEqual(
            test_grader.__dict__, altered_grader.__dict__, "cutoff add D")

        test_grader.grace_period = {'hours': 4, 'minutes': 5, 'seconds': 0}
        altered_grader = CourseGradingModel.update_from_json(
            test_grader.__dict__)
        print test_grader.grace_period, altered_grader.grace_period
        self.assertDictEqual(
            test_grader.__dict__, altered_grader.__dict__, "4 hour grace period")
Exemple #7
0
    def test_update_cutoffs_from_json(self):
        test_grader = CourseGradingModel.fetch(self.course_locator)
        CourseGradingModel.update_cutoffs_from_json(self.course_locator,
                                                    test_grader.grade_cutoffs,
                                                    self.user)
        # 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.
        altered_grader = CourseGradingModel.fetch(self.course_locator)
        self.assertDictEqual(test_grader.grade_cutoffs,
                             altered_grader.grade_cutoffs, "Noop update")

        test_grader.grade_cutoffs['D'] = 0.3
        CourseGradingModel.update_cutoffs_from_json(self.course_locator,
                                                    test_grader.grade_cutoffs,
                                                    self.user)
        altered_grader = CourseGradingModel.fetch(self.course_locator)
        self.assertDictEqual(test_grader.grade_cutoffs,
                             altered_grader.grade_cutoffs, "cutoff add D")

        test_grader.grade_cutoffs['Pass'] = 0.75
        CourseGradingModel.update_cutoffs_from_json(self.course_locator,
                                                    test_grader.grade_cutoffs,
                                                    self.user)
        altered_grader = CourseGradingModel.fetch(self.course_locator)
        self.assertDictEqual(test_grader.grade_cutoffs,
                             altered_grader.grade_cutoffs,
                             "cutoff change 'Pass'")
Exemple #8
0
    def test_delete_grace_period(self):
        test_grader = CourseGradingModel.fetch(self.course_locator)
        CourseGradingModel.update_grace_period_from_json(
            self.course_locator, test_grader.grace_period, self.user)
        # update_grace_period_from_json doesn't return anything, so query the db for its contents.
        altered_grader = CourseGradingModel.fetch(self.course_locator)
        self.assertEqual(test_grader.grace_period, altered_grader.grace_period,
                         "Noop update")

        test_grader.grace_period = {'hours': 15, 'minutes': 5, 'seconds': 30}
        CourseGradingModel.update_grace_period_from_json(
            self.course_locator, test_grader.grace_period, self.user)
        altered_grader = CourseGradingModel.fetch(self.course_locator)
        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
        CourseGradingModel.delete_grace_period(self.course_locator, self.user)
        # update_grace_period_from_json doesn't return anything, so query the db for its contents.
        altered_grader = CourseGradingModel.fetch(self.course_locator)
        # Once deleted, the grace period should simply be None
        self.assertEqual(None, altered_grader.grace_period,
                         "Delete grace period")
    def test_fetch_grader(self):
        test_grader = CourseGradingModel.fetch(self.course.id)
        self.assertIsNotNone(test_grader.graders, "No graders")
        self.assertIsNotNone(test_grader.grade_cutoffs, "No cutoffs")

        for i, grader in enumerate(test_grader.graders):
            subgrader = CourseGradingModel.fetch_grader(self.course.id, i)
            self.assertDictEqual(grader, subgrader, str(i) + "th graders not equal")
    def test_fetch_grader(self):
        test_grader = CourseGradingModel.fetch(self.course.id)
        self.assertIsNotNone(test_grader.graders, "No graders")
        self.assertIsNotNone(test_grader.grade_cutoffs, "No cutoffs")

        for i, grader in enumerate(test_grader.graders):
            subgrader = CourseGradingModel.fetch_grader(self.course.id, i)
            self.assertDictEqual(grader, subgrader, str(i) + "th graders not equal")
Exemple #11
0
def remove_entrance_exam_graders(course_key, user):
    """
    Removes existing entrance exam graders attached to the specified course
    Typically used when adding/removing an entrance exam.
    """
    grading_model = CourseGradingModel.fetch(course_key)
    graders = grading_model.graders
    for i, grader in enumerate(graders):
        if grader['type'] == GRADER_TYPES['ENTRANCE_EXAM']:
            CourseGradingModel.delete_grader(course_key, i, user)
Exemple #12
0
def remove_entrance_exam_graders(course_key, user):
    """
    Removes existing entrance exam graders attached to the specified course
    Typically used when adding/removing an entrance exam.
    """
    grading_model = CourseGradingModel.fetch(course_key)
    graders = grading_model.graders
    for i, grader in enumerate(graders):
        if grader['type'] == GRADER_TYPES['ENTRANCE_EXAM']:
            CourseGradingModel.delete_grader(course_key, i, user)
    def test_update_grader_from_json(self):
        test_grader = CourseGradingModel.fetch(self.course.location)
        altered_grader = CourseGradingModel.update_grader_from_json(test_grader.course_location, test_grader.graders[1])
        self.assertDictEqual(test_grader.graders[1], altered_grader, "Noop update")

        test_grader.graders[1]['min_count'] = test_grader.graders[1].get('min_count') + 2
        altered_grader = CourseGradingModel.update_grader_from_json(test_grader.course_location, test_grader.graders[1])
        self.assertDictEqual(test_grader.graders[1], altered_grader, "min_count[1] + 2")

        test_grader.graders[1]['drop_count'] = test_grader.graders[1].get('drop_count') + 1
        altered_grader = CourseGradingModel.update_grader_from_json(test_grader.course_location, test_grader.graders[1])
        self.assertDictEqual(test_grader.graders[1], altered_grader, "drop_count[1] + 2")
Exemple #14
0
def assignment_type_update(request, org, course, category, name):
    '''
    CRUD operations on assignment types for sections and subsections and anything else gradable.
    '''
    location = Location(['i4x', org, course, category, name])
    if not has_access(request.user, location):
        return HttpResponseForbidden()

    if request.method == 'GET':
        return JsonResponse(CourseGradingModel.get_section_grader_type(location))
    elif request.method in ('POST', 'PUT'):  # post or put, doesn't matter.
        return JsonResponse(CourseGradingModel.update_section_grader_type(location, request.POST))
    def test_update_grader_from_json(self):
        test_grader = CourseGradingModel.fetch(self.course_locator)
        altered_grader = CourseGradingModel.update_grader_from_json(self.course_locator, test_grader.graders[1])
        self.assertDictEqual(test_grader.graders[1], altered_grader, "Noop update")

        test_grader.graders[1]["min_count"] = test_grader.graders[1].get("min_count") + 2
        altered_grader = CourseGradingModel.update_grader_from_json(self.course_locator, test_grader.graders[1])
        self.assertDictEqual(test_grader.graders[1], altered_grader, "min_count[1] + 2")

        test_grader.graders[1]["drop_count"] = test_grader.graders[1].get("drop_count") + 1
        altered_grader = CourseGradingModel.update_grader_from_json(self.course_locator, test_grader.graders[1])
        self.assertDictEqual(test_grader.graders[1], altered_grader, "drop_count[1] + 2")
Exemple #16
0
    def test_wrong_weight_sums(self):
        """Сумма весов в настройках не равна 100"""
        self.graders = CourseGradingModel.fetch(self.course.id).graders
        self.graders[0]['weight'] = 103.
        CourseGradingModel.update_grader_from_json(self.course.id, self.graders[0], self.user)

        self.set_task(n_task=5, type_task="Homework")
        self.set_task(n_task=3, type_task="Lab")
        self.set_task(n_task=1, type_task="Final Exam")
        CV = CourseValid(None, str(self.course_key))
        rep = CV.val_grade()
        self.assertEqual(len(rep.warnings), 1)
Exemple #17
0
    def setUp(self):
        """Устанавливает грейдеры на 3 типа заданий и задает им веса"""
        super(GradingValTest, self).setUp()

        self.graders = CourseGradingModel.fetch(self.course.id).graders
        self.graders[0]['min_count'] = 5
        self.graders[1]['min_count'] = 3
        self.graders[0]['weight'] = 33.
        self.graders[1]['weight'] = 27.
        for g in self.graders:
            CourseGradingModel.update_grader_from_json(self.course.id, g, self.user)
        CourseGradingModel.delete_grader(self.course_key, 2, self.user)
Exemple #18
0
def assignment_type_update(request, org, course, category, name):
    '''
    CRUD operations on assignment types for sections and subsections and anything else gradable.
    '''
    location = Location(['i4x', org, course, category, name])
    if not has_access(request.user, location):
        return HttpResponseForbidden()

    if request.method == 'GET':
        return JsonResponse(CourseGradingModel.get_section_grader_type(location))
    elif request.method in ('POST', 'PUT'):  # post or put, doesn't matter.
        return JsonResponse(CourseGradingModel.update_section_grader_type(location, request.POST))
Exemple #19
0
def assignment_type_update(request, org, course, category, name):
    """
    CRUD operations on assignment types for sections and subsections and
    anything else gradable.
    """
    location = Location(["i4x", org, course, category, name])
    if not has_access(request.user, location):
        return HttpResponseForbidden()

    if request.method == "GET":
        rsp = CourseGradingModel.get_section_grader_type(location)
    elif request.method in ("POST", "PUT"):  # post or put, doesn't matter.
        rsp = CourseGradingModel.update_section_grader_type(location, request.POST)
    return JsonResponse(rsp)
    def test_update_from_json(self, store):
        self.course = CourseFactory.create(default_store=store)

        test_grader = CourseGradingModel.fetch(self.course.id)
        altered_grader = CourseGradingModel.update_from_json(self.course.id, test_grader.__dict__, self.user)
        self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "Noop update")

        test_grader.graders[0]['weight'] = test_grader.graders[0].get('weight') * 2
        altered_grader = CourseGradingModel.update_from_json(self.course.id, test_grader.__dict__, self.user)
        self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "Weight[0] * 2")

        # 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__)

        test_grader.grade_cutoffs['D'] = 0.3
        altered_grader = CourseGradingModel.update_from_json(self.course.id, test_grader.__dict__, self.user)
        self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "cutoff add D")

        test_grader.grace_period = {'hours': 4, 'minutes': 5, 'seconds': 0}
        altered_grader = CourseGradingModel.update_from_json(self.course.id, test_grader.__dict__, self.user)
        self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "4 hour grace period")
    def test_update_from_json(self, store):
        self.course = CourseFactory.create(default_store=store)

        test_grader = CourseGradingModel.fetch(self.course.id)
        altered_grader = CourseGradingModel.update_from_json(self.course.id, test_grader.__dict__, self.user)
        self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "Noop update")

        test_grader.graders[0]['weight'] = test_grader.graders[0].get('weight') * 2
        altered_grader = CourseGradingModel.update_from_json(self.course.id, test_grader.__dict__, self.user)
        self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "Weight[0] * 2")

        # 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__)

        test_grader.grade_cutoffs['D'] = 0.3
        altered_grader = CourseGradingModel.update_from_json(self.course.id, test_grader.__dict__, self.user)
        self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "cutoff add D")

        test_grader.grace_period = {'hours': 4, 'minutes': 5, 'seconds': 0}
        altered_grader = CourseGradingModel.update_from_json(self.course.id, test_grader.__dict__, self.user)
        self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "4 hour grace period")
Exemple #22
0
def course_index(request, org, course, name):
    """
    Display an editable course overview.

    org, course, name: Attributes of the Location for the item to edit
    """
    location = get_location_and_verify_access(request, org, course, name)

    lms_link = get_lms_link_for_item(location)

    upload_asset_callback_url = reverse('upload_asset', kwargs={
        'org': org,
        'course': course,
        'coursename': name
    })

    course = modulestore().get_item(location, depth=3)
    sections = course.get_children()

    return render_to_response('overview.html', {
        'context_course': course,
        'lms_link': lms_link,
        'sections': sections,
        'course_graders': json.dumps(
            CourseGradingModel.fetch(course.location).graders
        ),
        'parent_location': course.location,
        'new_section_category': 'chapter',
        'new_subsection_category': 'sequential',
        'upload_asset_callback_url': upload_asset_callback_url,
        'new_unit_category': 'vertical',
        'category': 'vertical'
    })
 def test_initial_grader(self):
     descriptor = get_modulestore(self.course.location).get_item(self.course.location)
     test_grader = CourseGradingModel(descriptor)
     # ??? How much should this test bake in expectations about defaults and thus fail if defaults change?
     self.assertEqual(self.course.location, test_grader.course_location, "Course locations")
     self.assertIsNotNone(test_grader.graders, "No graders")
     self.assertIsNotNone(test_grader.grade_cutoffs, "No cutoffs")
Exemple #24
0
def course_index(request, package_id, branch, version_guid, block):
    """
    Display an editable course overview.

    org, course, name: Attributes of the Location for the item to edit
    """
    locator, course = _get_locator_and_course(package_id,
                                              branch,
                                              version_guid,
                                              block,
                                              request.user,
                                              depth=3)
    lms_link = get_lms_link_for_item(course.location)
    sections = course.get_children()

    return render_to_response(
        'overview.html', {
            'context_course': course,
            'lms_link': lms_link,
            'sections': sections,
            'course_graders': json.dumps(
                CourseGradingModel.fetch(locator).graders),
            'parent_locator': locator,
            'new_section_category': 'chapter',
            'new_subsection_category': 'sequential',
            'new_unit_category': 'vertical',
            'category': 'vertical'
        })
Exemple #25
0
def course_index(request, package_id, branch, version_guid, block):
    """
    Display an editable course overview.

    org, course, name: Attributes of the Location for the item to edit
    """
    locator, course = _get_locator_and_course(
        package_id, branch, version_guid, block, request.user, depth=3
    )
    lms_link = get_lms_link_for_item(course.location)
    sections = course.get_children()

    return render_to_response('overview.html', {
        'context_course': course,
        'lms_link': lms_link,
        'sections': sections,
        'course_graders': json.dumps(
            CourseGradingModel.fetch(locator).graders
        ),
        'parent_locator': locator,
        'new_section_category': 'chapter',
        'new_subsection_category': 'sequential',
        'new_unit_category': 'vertical',
        'category': 'vertical'
    })
Exemple #26
0
def course_index(request, course_id, branch, version_guid, block):
    """
    Display an editable course overview.

    org, course, name: Attributes of the Location for the item to edit
    """
    location = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block)
    # TODO: when converting to split backend, if location does not have a usage_id,
    # we'll need to get the course's root block_id
    if not has_access(request.user, location):
        raise PermissionDenied()


    old_location = loc_mapper().translate_locator_to_location(location)

    lms_link = get_lms_link_for_item(old_location)

    course = modulestore().get_item(old_location, depth=3)
    sections = course.get_children()

    return render_to_response('overview.html', {
        'context_course': course,
        'lms_link': lms_link,
        'sections': sections,
        'course_graders': json.dumps(
            CourseGradingModel.fetch(course.location).graders
        ),
        'parent_location': course.location,
        'new_section_category': 'chapter',
        'new_subsection_category': 'sequential',
        'new_unit_category': 'vertical',
        'category': 'vertical'
    })
Exemple #27
0
def course_index(request, course_key):
    """
    Display an editable course overview.

    org, course, name: Attributes of the Location for the item to edit
    """
    course_module = _get_course_module(course_key, request.user, depth=3)
    lms_link = get_lms_link_for_item(course_module.location)
    sections = course_module.get_children()

    try:
        current_action = CourseRerunState.objects.find_first(course_key=course_key, should_display=True)
    except (ItemNotFoundError, CourseActionStateItemNotFoundError):
        current_action = None

    return render_to_response('overview.html', {
        'context_course': course_module,
        'lms_link': lms_link,
        'sections': sections,
        'course_graders': json.dumps(
            CourseGradingModel.fetch(course_key).graders
        ),
        'new_section_category': 'chapter',
        'new_subsection_category': 'sequential',
        'new_unit_category': 'vertical',
        'category': 'vertical',
        'rerun_notification_id': current_action.id if current_action else None,
    })
Exemple #28
0
    def test_contentstore_views_entrance_exam_post_new_sequential_confirm_grader(
            self):
        """
        Unit Test: test_contentstore_views_entrance_exam_post
        """
        resp = self.client.post(self.exam_url, {},
                                http_accept='application/json')
        self.assertEqual(resp.status_code, 201)
        resp = self.client.get(self.exam_url)
        self.assertEqual(resp.status_code, 200)

        # Reload the test course now that the exam module has been added
        self.course = modulestore().get_course(self.course.id)

        # Add a new child sequential to the exam module
        # Confirm that the grader type is 'Entrance Exam'
        chapter_locator_string = json.loads(
            resp.content.decode('utf-8')).get('locator')
        # chapter_locator = UsageKey.from_string(chapter_locator_string)
        seq_data = {
            'category': "sequential",
            'display_name': "Entrance Exam Subsection",
            'parent_locator': chapter_locator_string,
        }
        resp = self.client.ajax_post(reverse_url('xblock_handler'), seq_data)
        seq_locator_string = json.loads(
            resp.content.decode('utf-8')).get('locator')
        seq_locator = UsageKey.from_string(seq_locator_string)
        section_grader_type = CourseGradingModel.get_section_grader_type(
            seq_locator)
        self.assertEqual(GRADER_TYPES['ENTRANCE_EXAM'],
                         section_grader_type['graderType'])
Exemple #29
0
def course_index(request, course_key):
    """
    Display an editable course overview.

    org, course, name: Attributes of the Location for the item to edit
    """
    course_module = _get_course_module(course_key, request.user, depth=3)
    lms_link = get_lms_link_for_item(course_module.location)
    sections = course_module.get_children()

    return render_to_response(
        'overview.html', {
            'context_course':
            course_module,
            'lms_link':
            lms_link,
            'sections':
            sections,
            'course_graders':
            json.dumps(CourseGradingModel.fetch(course_key).graders),
            'new_section_category':
            'chapter',
            'new_subsection_category':
            'sequential',
            'new_unit_category':
            'vertical',
            'category':
            'vertical'
        })
    def test_contentstore_views_entrance_exam_post_new_sequential_confirm_grader(self):
        """
        Unit Test: test_contentstore_views_entrance_exam_post
        """
        resp = self.client.post(self.exam_url, {}, http_accept='application/json')
        self.assertEqual(resp.status_code, 201)
        resp = self.client.get(self.exam_url)
        self.assertEqual(resp.status_code, 200)

        # Reload the test course now that the exam module has been added
        self.course = modulestore().get_course(self.course.id)

        # Add a new child sequential to the exam module
        # Confirm that the grader type is 'Entrance Exam'
        chapter_locator_string = json.loads(resp.content).get('locator')
        # chapter_locator = UsageKey.from_string(chapter_locator_string)
        seq_data = {
            'category': "sequential",
            'display_name': "Entrance Exam Subsection",
            'parent_locator': chapter_locator_string,
        }
        resp = self.client.ajax_post(reverse_url('xblock_handler'), seq_data)
        seq_locator_string = json.loads(resp.content).get('locator')
        seq_locator = UsageKey.from_string(seq_locator_string)
        section_grader_type = CourseGradingModel.get_section_grader_type(seq_locator)
        self.assertEqual(GRADER_TYPES['ENTRANCE_EXAM'], section_grader_type['graderType'])
Exemple #31
0
def course_index(request, org, course, name):
    """
    Display an editable course overview.

    org, course, name: Attributes of the Location for the item to edit
    """
    location = get_location_and_verify_access(request, org, course, name)

    lms_link = get_lms_link_for_item(location)

    upload_asset_callback_url = reverse('upload_asset', kwargs={
        'org': org,
        'course': course,
        'coursename': name
    })

    course = modulestore().get_item(location, depth=3)
    sections = course.get_children()

    return render_to_response('overview.html', {
        'active_tab': 'courseware',
        'context_course': course,
        'lms_link': lms_link,
        'sections': sections,
        'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
        'parent_location': course.location,
        'new_section_template': Location('i4x', 'edx', 'templates', 'chapter', 'Empty'),
        'new_subsection_template': Location('i4x', 'edx', 'templates', 'sequential', 'Empty'),  # for now they are the same, but the could be different at some point...
        'upload_asset_callback_url': upload_asset_callback_url,
        'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty')
    })
 def test_delete(self):
     """Test deleting a specific grading type record."""
     resp = self.client.delete(self.url + '/0', HTTP_ACCEPT="application/json")
     self.assertEqual(resp.status_code, 204)
     current_graders = CourseGradingModel.fetch(self.course.id).graders
     self.assertNotIn(self.starting_graders[0], current_graders)
     self.assertEqual(len(self.starting_graders) - 1, len(current_graders))
 def test_delete(self):
     """Test deleting a specific grading type record."""
     resp = self.client.delete(self.url + '/0', HTTP_ACCEPT="application/json")
     self.assertEqual(resp.status_code, 204)
     current_graders = CourseGradingModel.fetch(self.course.id).graders
     self.assertNotIn(self.starting_graders[0], current_graders)
     self.assertEqual(len(self.starting_graders) - 1, len(current_graders))
Exemple #34
0
def course_index(request, org, course, name):
    """
    Display an editable course overview.

    org, course, name: Attributes of the Location for the item to edit
    """
    location = get_location_and_verify_access(request, org, course, name)

    lms_link = get_lms_link_for_item(location)

    upload_asset_callback_url = reverse('upload_asset', kwargs={
        'org': org,
        'course': course,
        'coursename': name
    })

    course = modulestore().get_item(location, depth=3)
    sections = course.get_children()

    return render_to_response('overview.html', {
        'active_tab': 'courseware',
        'context_course': course,
        'lms_link': lms_link,
        'sections': sections,
        'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
        'parent_location': course.location,
        'new_section_template': Location('i4x', 'edx', 'templates', 'chapter', 'Empty'),
        'new_subsection_template': Location('i4x', 'edx', 'templates', 'sequential', 'Empty'),  # for now they are the same, but the could be different at some point...
        'upload_asset_callback_url': upload_asset_callback_url,
        'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty')
    })
Exemple #35
0
def edit_subsection(request, location):
    # check that we have permissions to edit this item
    course = get_course_for_item(location)
    if not has_access(request.user, course.location):
        raise PermissionDenied()

    item = modulestore().get_item(location, depth=1)

    lms_link = get_lms_link_for_item(
        location, course_id=course.location.course_id)
    preview_link = get_lms_link_for_item(
        location, course_id=course.location.course_id, preview=True)

    # make sure that location references a 'sequential', otherwise return
    # BadRequest
    if item.location.category != 'sequential':
        return HttpResponseBadRequest()

    parent_locs = modulestore().get_parent_locations(location, None)

    # we're for now assuming a single parent
    if len(parent_locs) != 1:
        logging.error(
            'Multiple (or none) parents have been found for {0}'.format(location))

    # this should blow up if we don't find any parents, which would be
    # erroneous
    parent = modulestore().get_item(parent_locs[0])

    # remove all metadata from the generic dictionary that is presented in a
    # more normalized UI

    policy_metadata = dict(
        (field.name, field.read_from(item))
        for field
        in item.fields
        if field.name not in ['display_name', 'start', 'due', 'format'] and field.scope == Scope.settings
    )

    can_view_live = False
    subsection_units = item.get_children()
    for unit in subsection_units:
        state = compute_unit_state(unit)
        if state == UnitState.public or state == UnitState.draft:
            can_view_live = True
            break

    return render_to_response('edit_subsection.html',
                              {'subsection': item,
                               'context_course': course,
                               'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
                               'lms_link': lms_link,
                               'preview_link': preview_link,
                               'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
                               'parent_location': course.location,
                               'parent_item': parent,
                               'policy_metadata': policy_metadata,
                               'subsection_units': subsection_units,
                               'can_view_live': can_view_live
                               })
    def test_update_from_json(self):
        test_grader = CourseGradingModel.fetch(self.course.location)
        altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
        self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "Noop update")

        test_grader.graders[0]["weight"] = test_grader.graders[0].get("weight") * 2
        altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
        self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "Weight[0] * 2")

        test_grader.grade_cutoffs["D"] = 0.3
        altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
        self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "cutoff add D")

        test_grader.grace_period = {"hours": 4, "minutes": 5, "seconds": 0}
        altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
        self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "4 hour grace period")
Exemple #37
0
def course_index(request, org, course, name):
    """
    Display an editable course overview.

    org, course, name: Attributes of the Location for the item to edit
    """
    location = get_location_and_verify_access(request, org, course, name)

    lms_link = get_lms_link_for_item(location)

    upload_asset_callback_url = reverse("upload_asset", kwargs={"org": org, "course": course, "coursename": name})

    course = modulestore().get_item(location, depth=3)
    sections = course.get_children()

    return render_to_response(
        "overview.html",
        {
            "context_course": course,
            "lms_link": lms_link,
            "sections": sections,
            "course_graders": json.dumps(CourseGradingModel.fetch(course.location).graders),
            "parent_location": course.location,
            "new_section_category": "chapter",
            "new_subsection_category": "sequential",
            "upload_asset_callback_url": upload_asset_callback_url,
            "new_unit_category": "vertical",
            "category": "vertical",
        },
    )
Exemple #38
0
def course_index(request, course_id, branch, version_guid, block):
    """
    Display an editable course overview.

    org, course, name: Attributes of the Location for the item to edit
    """
    location = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block)
    # TODO: when converting to split backend, if location does not have a usage_id,
    # we'll need to get the course's root block_id
    if not has_access(request.user, location):
        raise PermissionDenied()


    old_location = loc_mapper().translate_locator_to_location(location)

    lms_link = get_lms_link_for_item(old_location)

    course = modulestore().get_item(old_location, depth=3)
    sections = course.get_children()

    return render_to_response('overview.html', {
        'context_course': course,
        'lms_link': lms_link,
        'sections': sections,
        'course_graders': json.dumps(
            CourseGradingModel.fetch(course.location).graders
        ),
        'parent_location': course.location,
        'new_section_category': 'chapter',
        'new_subsection_category': 'sequential',
        'new_unit_category': 'vertical',
        'category': 'vertical'
    })
    def test_fetch_grader(self):
        test_grader = CourseGradingModel.fetch(self.course.location.url())
        self.assertEqual(self.course.location, test_grader.course_location, "Course locations")
        self.assertIsNotNone(test_grader.graders, "No graders")
        self.assertIsNotNone(test_grader.grade_cutoffs, "No cutoffs")

        test_grader = CourseGradingModel.fetch(self.course.location)
        self.assertEqual(self.course.location, test_grader.course_location, "Course locations")
        self.assertIsNotNone(test_grader.graders, "No graders")
        self.assertIsNotNone(test_grader.grade_cutoffs, "No cutoffs")

        for i, grader in enumerate(test_grader.graders):
            subgrader = CourseGradingModel.fetch_grader(self.course.location, i)
            self.assertDictEqual(grader, subgrader, str(i) + "th graders not equal")

        subgrader = CourseGradingModel.fetch_grader(self.course.location.list(), 0)
        self.assertDictEqual(test_grader.graders[0], subgrader, "failed with location as list")
    def test_update_cutoffs_from_json(self):
        test_grader = CourseGradingModel.fetch(self.course.id)
        CourseGradingModel.update_cutoffs_from_json(self.course.id, test_grader.grade_cutoffs, self.user)
        # 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.
        altered_grader = CourseGradingModel.fetch(self.course.id)
        self.assertDictEqual(test_grader.grade_cutoffs, altered_grader.grade_cutoffs, "Noop update")

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

        test_grader.grade_cutoffs['Pass'] = 0.75
        CourseGradingModel.update_cutoffs_from_json(self.course.id, test_grader.grade_cutoffs, self.user)
        altered_grader = CourseGradingModel.fetch(self.course.id)
        self.assertDictEqual(test_grader.grade_cutoffs, altered_grader.grade_cutoffs, "cutoff change 'Pass'")
Exemple #41
0
def subsection_handler(request, usage_key_string):
    """
    The restful handler for subsection-specific requests.

    GET
        html: return html page for editing a subsection
        json: not currently supported
    """
    if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'):
        usage_key = UsageKey.from_string(usage_key_string)
        try:
            course, item, lms_link = _get_item_in_course(request, usage_key)
        except ItemNotFoundError:
            return HttpResponseBadRequest()

        preview_link = get_lms_link_for_item(item.location, preview=True)

        # make sure that location references a 'sequential', otherwise return
        # BadRequest
        if item.location.category != 'sequential':
            return HttpResponseBadRequest()

        parent = get_parent_xblock(item)

        # remove all metadata from the generic dictionary that is presented in a
        # more normalized UI. We only want to display the XBlocks fields, not
        # the fields from any mixins that have been added
        fields = getattr(item, 'unmixed_class', item.__class__).fields

        policy_metadata = dict(
            (field.name, field.read_from(item))
            for field
            in fields.values()
            if field.name not in ['display_name', 'start', 'due', 'format'] and field.scope == Scope.settings
        )

        can_view_live = False
        subsection_units = item.get_children()
        can_view_live = any([modulestore().has_published_version(unit) for unit in subsection_units])

        return render_to_response(
            'edit_subsection.html',
            {
                'subsection': item,
                'context_course': course,
                'new_unit_category': 'vertical',
                'lms_link': lms_link,
                'preview_link': preview_link,
                'course_graders': json.dumps(CourseGradingModel.fetch(item.location.course_key).graders),
                'parent_item': parent,
                'locator': item.location,
                'policy_metadata': policy_metadata,
                'subsection_units': subsection_units,
                'can_view_live': can_view_live
            }
        )
    else:
        return HttpResponseBadRequest("Only supports html requests")
    def test_update_section_grader_type(self):
        # Get the descriptor and the section_grader_type and assert they are the default values
        descriptor = modulestore().get_item(self.course.location)
        section_grader_type = CourseGradingModel.get_section_grader_type(self.course.location)

        self.assertEqual('notgraded', section_grader_type['graderType'])
        self.assertEqual(None, descriptor.format)
        self.assertEqual(False, descriptor.graded)

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

        self.assertEqual('Homework', section_grader_type['graderType'])
        self.assertEqual('Homework', descriptor.format)
        self.assertEqual(True, descriptor.graded)

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

        self.assertEqual('notgraded', section_grader_type['graderType'])
        self.assertEqual(None, descriptor.format)
        self.assertEqual(False, descriptor.graded)
Exemple #43
0
    def test_update_section_grader_type(self):
        # Get the descriptor and the section_grader_type and assert they are the default values
        descriptor = get_modulestore(self.course.location).get_item(
            self.course.location)
        section_grader_type = CourseGradingModel.get_section_grader_type(
            self.course_locator)

        self.assertEqual('notgraded', section_grader_type['graderType'])
        self.assertEqual(None, descriptor.format)
        self.assertEqual(False, descriptor.graded)

        # Change the default grader type to Homework, which should also mark the section as graded
        CourseGradingModel.update_section_grader_type(self.course, 'Homework',
                                                      self.user)
        descriptor = get_modulestore(self.course.location).get_item(
            self.course.location)
        section_grader_type = CourseGradingModel.get_section_grader_type(
            self.course_locator)

        self.assertEqual('Homework', section_grader_type['graderType'])
        self.assertEqual('Homework', descriptor.format)
        self.assertEqual(True, descriptor.graded)

        # Change the grader type back to notgraded, which should also unmark the section as graded
        CourseGradingModel.update_section_grader_type(self.course, 'notgraded',
                                                      self.user)
        descriptor = get_modulestore(self.course.location).get_item(
            self.course.location)
        section_grader_type = CourseGradingModel.get_section_grader_type(
            self.course_locator)

        self.assertEqual('notgraded', section_grader_type['graderType'])
        self.assertEqual(None, descriptor.format)
        self.assertEqual(False, descriptor.graded)
    def test_update_section_grader_type(self):
        # Get the descriptor and the section_grader_type and assert they are the default values
        descriptor = get_modulestore(self.course.location).get_item(self.course.location)
        section_grader_type = CourseGradingModel.get_section_grader_type(self.course.location)

        self.assertEqual("Not Graded", section_grader_type["graderType"])
        self.assertEqual(None, descriptor.lms.format)
        self.assertEqual(False, descriptor.lms.graded)

        # Change the default grader type to Homework, which should also mark the section as graded
        CourseGradingModel.update_section_grader_type(self.course.location, {"graderType": "Homework"})
        descriptor = get_modulestore(self.course.location).get_item(self.course.location)
        section_grader_type = CourseGradingModel.get_section_grader_type(self.course.location)

        self.assertEqual("Homework", section_grader_type["graderType"])
        self.assertEqual("Homework", descriptor.lms.format)
        self.assertEqual(True, descriptor.lms.graded)

        # Change the grader type back to Not Graded, which should also unmark the section as graded
        CourseGradingModel.update_section_grader_type(self.course.location, {"graderType": "Not Graded"})
        descriptor = get_modulestore(self.course.location).get_item(self.course.location)
        section_grader_type = CourseGradingModel.get_section_grader_type(self.course.location)

        self.assertEqual("Not Graded", section_grader_type["graderType"])
        self.assertEqual(None, descriptor.lms.format)
        self.assertEqual(False, descriptor.lms.graded)
Exemple #45
0
def grading_handler(request,
                    tag=None,
                    package_id=None,
                    branch=None,
                    version_guid=None,
                    block=None,
                    grader_index=None):
    """
    Course Grading policy configuration
    GET
        html: get the page
        json no grader_index: get the CourseGrading model (graceperiod, cutoffs, and graders)
        json w/ grader_index: get the specific grader
    PUT
        json no grader_index: update the Course through the CourseGrading model
        json w/ grader_index: create or update the specific grader (create if index out of range)
    """
    locator, course_module = _get_locator_and_course(package_id, branch,
                                                     version_guid, block,
                                                     request.user)

    if 'text/html' in request.META.get('HTTP_ACCEPT',
                                       '') and request.method == 'GET':
        course_details = CourseGradingModel.fetch(locator)

        return render_to_response(
            'settings_graders.html', {
                'context_course':
                course_module,
                'course_locator':
                locator,
                'course_details':
                json.dumps(course_details, cls=CourseSettingsEncoder),
                'grading_url':
                locator.url_reverse('/settings/grading/'),
            })
    elif 'application/json' in request.META.get('HTTP_ACCEPT', ''):
        if request.method == 'GET':
            if grader_index is None:
                return JsonResponse(
                    CourseGradingModel.fetch(locator),
                    # encoder serializes dates, old locations, and instances
                    encoder=CourseSettingsEncoder)
            else:
                return JsonResponse(
                    CourseGradingModel.fetch_grader(locator, grader_index))
        elif request.method in ('POST', 'PUT'):  # post or put, doesn't matter.
            # None implies update the whole model (cutoffs, graceperiod, and graders) not a specific grader
            if grader_index is None:
                return JsonResponse(CourseGradingModel.update_from_json(
                    locator, request.json, request.user),
                                    encoder=CourseSettingsEncoder)
            else:
                return JsonResponse(
                    CourseGradingModel.update_grader_from_json(
                        locator, request.json, request.user))
        elif request.method == "DELETE" and grader_index is not None:
            CourseGradingModel.delete_grader(locator, grader_index,
                                             request.user)
            return JsonResponse()
    def test_delete_grace_period(self):
        test_grader = CourseGradingModel.fetch(self.course.location)
        CourseGradingModel.update_grace_period_from_json(test_grader.course_location, test_grader.grace_period)
        # update_grace_period_from_json doesn't return anything, so query the db for its contents.
        altered_grader = CourseGradingModel.fetch(self.course.location)
        self.assertEqual(test_grader.grace_period, altered_grader.grace_period, "Noop update")

        test_grader.grace_period = {'hours': 15, 'minutes': 5, 'seconds': 30}
        CourseGradingModel.update_grace_period_from_json(test_grader.course_location, test_grader.grace_period)
        altered_grader = CourseGradingModel.fetch(self.course.location)
        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
        CourseGradingModel.delete_grace_period(test_grader.course_location)
        # update_grace_period_from_json doesn't return anything, so query the db for its contents.
        altered_grader = CourseGradingModel.fetch(self.course.location)
        # Once deleted, the grace period should simply be None
        self.assertEqual(None, altered_grader.grace_period, "Delete grace period")
    def test_delete_grace_period(self):
        test_grader = CourseGradingModel.fetch(self.course.id)
        CourseGradingModel.update_grace_period_from_json(self.course.id, test_grader.grace_period, self.user)
        # update_grace_period_from_json doesn't return anything, so query the db for its contents.
        altered_grader = CourseGradingModel.fetch(self.course.id)
        self.assertEqual(test_grader.grace_period, altered_grader.grace_period, "Noop update")

        test_grader.grace_period = {"hours": 15, "minutes": 5, "seconds": 30}
        CourseGradingModel.update_grace_period_from_json(self.course.id, test_grader.grace_period, self.user)
        altered_grader = CourseGradingModel.fetch(self.course.id)
        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
        CourseGradingModel.delete_grace_period(self.course.id, self.user)
        # update_grace_period_from_json doesn't return anything, so query the db for its contents.
        altered_grader = CourseGradingModel.fetch(self.course.id)
        # Once deleted, the grace period should simply be None
        self.assertEqual(None, altered_grader.grace_period, "Delete grace period")
Exemple #48
0
    def test_contentstore_views_entrance_exam_delete(self):
        """
        Unit Test: test_contentstore_views_entrance_exam_delete
        """
        resp = self.client.post(self.exam_url, {},
                                http_accept='application/json')
        self.assertEqual(resp.status_code, 201)
        resp = self.client.get(self.exam_url)
        self.assertEqual(resp.status_code, 200)
        resp = self.client.delete(self.exam_url)
        self.assertEqual(resp.status_code, 204)
        resp = self.client.get(self.exam_url)
        self.assertEqual(resp.status_code, 404)

        user = User.objects.create(
            username='******',
            email='*****@*****.**',
            is_active=True,
        )
        user.set_password('test')
        user.save()
        milestones = milestones_helpers.get_course_milestones(
            six.text_type(self.course_key))
        self.assertEqual(len(milestones), 1)
        milestone_key = '{}.{}'.format(milestones[0]['namespace'],
                                       milestones[0]['name'])
        paths = milestones_helpers.get_course_milestones_fulfillment_paths(
            six.text_type(self.course_key),
            milestones_helpers.serialize_user(user))

        # What we have now is a course milestone requirement and no valid fulfillment
        # paths for the specified user.  The LMS is going to have to ignore this situation,
        # because we can't confidently prevent it from occuring at some point in the future.
        # milestone_key_1 =
        self.assertEqual(len(paths[milestone_key]), 0)

        # Re-adding an entrance exam to the course should fix the missing link
        # It wipes out any old entrance exam artifacts and inserts a new exam course chapter/module
        resp = self.client.post(self.exam_url, {},
                                http_accept='application/json')
        self.assertEqual(resp.status_code, 201)
        resp = self.client.get(self.exam_url)
        self.assertEqual(resp.status_code, 200)

        # Confirm that we have only one Entrance Exam grader after re-adding the exam (validates SOL-475)
        graders = CourseGradingModel.fetch(self.course_key).graders
        count = 0
        for grader in graders:
            if grader['type'] == GRADER_TYPES['ENTRANCE_EXAM']:
                count += 1
        self.assertEqual(count, 1)
Exemple #49
0
 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)
     current_graders = CourseGradingModel.fetch(self.course_locator).graders
     self.assertEqual(len(self.starting_graders), len(current_graders))
Exemple #50
0
def course_config_graders_page(request, org, course, name):
    """
    Send models and views as well as html for editing the course settings to the client.

    org, course, name: Attributes of the Location for the item to edit
    """
    location = get_location_and_verify_access(request, org, course, name)

    course_module = modulestore().get_item(location)
    course_details = CourseGradingModel.fetch(location)

    return render_to_response('settings_graders.html', {
        'context_course': course_module,
        'course_location': location,
        'course_details': json.dumps(course_details, cls=CourseSettingsEncoder)
    })
 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.
     grader = {
         "type": "manual",
         "min_count": 5,
         "drop_count": 10,
         "short_label": "yo momma",
         "weight": 17.3,
     }
     resp = self.client.ajax_post('{}/{}'.format(self.url, len(self.starting_graders) + 1), grader)
     self.assertEqual(resp.status_code, 200)
     obj = json.loads(resp.content)
     self.assertEqual(obj['id'], len(self.starting_graders))
     del obj['id']
     self.assertEqual(obj, grader)
     current_graders = CourseGradingModel.fetch(self.course.id).graders
     self.assertEqual(len(self.starting_graders) + 1, len(current_graders))