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 grading_handler(request, tag=None, course_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 = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block) if not has_access(request.user, locator): raise PermissionDenied() if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET': course_old_location = loc_mapper().translate_locator_to_location(locator) course_module = modulestore().get_item(course_old_location) 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), encoder=CourseSettingsEncoder ) else: return JsonResponse( CourseGradingModel.update_grader_from_json(locator, request.json) ) elif request.method == "DELETE" and grader_index is not None: CourseGradingModel.delete_grader(locator, grader_index) return JsonResponse()
def grading_handler(request, course_key_string, 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) """ course_key = CourseKey.from_string(course_key_string) course_module = _get_course_module(course_key, request.user) if "text/html" in request.META.get("HTTP_ACCEPT", "") and request.method == "GET": course_details = CourseGradingModel.fetch(course_key) return render_to_response( "settings_graders.html", { "context_course": course_module, "course_locator": course_key, "course_details": json.dumps(course_details, cls=CourseSettingsEncoder), "grading_url": reverse_course_url("grading_handler", course_key), }, ) elif "application/json" in request.META.get("HTTP_ACCEPT", ""): if request.method == "GET": if grader_index is None: return JsonResponse( CourseGradingModel.fetch(course_key), # encoder serializes dates, old locations, and instances encoder=CourseSettingsEncoder, ) else: return JsonResponse(CourseGradingModel.fetch_grader(course_key, 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(course_key, request.json, request.user), encoder=CourseSettingsEncoder, ) else: return JsonResponse(CourseGradingModel.update_grader_from_json(course_key, request.json, request.user)) elif request.method == "DELETE" and grader_index is not None: CourseGradingModel.delete_grader(course_key, grader_index, request.user) return JsonResponse()
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 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 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 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_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")
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_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 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, })
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' })
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_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_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'")
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_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")
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_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_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")
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")
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)
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)
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_locator).graders self.assertEqual(len(self.starting_graders) + 1, len(current_graders))
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(unicode(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( unicode(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)
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_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")
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.id).graders self.assertEqual(len(self.starting_graders), len(current_graders))
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 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 create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=False, include_child_info=False, course_outline=False, include_children_predicate=NEVER, parent_xblock=None, graders=None): """ Creates the information needed for client-side XBlockInfo. If data or metadata are not specified, their information will not be added (regardless of whether or not the xblock actually has data or metadata). There are three optional boolean parameters: include_ancestor_info - if true, ancestor info is added to the response include_child_info - if true, direct child info is included in the response course_outline - if true, the xblock is being rendered on behalf of the course outline. There are certain expensive computations that do not need to be included in this case. In addition, an optional include_children_predicate argument can be provided to define whether or not a particular xblock should have its children included. """ def safe_get_username(user_id): """ Guard against bad user_ids, like the infamous "**replace_user**". Note that this will ignore our special known IDs (ModuleStoreEnum.UserID). We should consider adding special handling for those values. :param user_id: the user id to get the username of :return: username, or None if the user does not exist or user_id is None """ if user_id: try: return User.objects.get(id=user_id).username except: # pylint: disable=bare-except pass return None is_xblock_unit = is_unit(xblock, parent_xblock) has_changes = modulestore().has_changes(xblock) if graders is None: graders = CourseGradingModel.fetch(xblock.location.course_key).graders # Compute the child info first so it can be included in aggregate information for the parent should_visit_children = include_child_info and ( course_outline and not is_xblock_unit or not course_outline) if should_visit_children and xblock.has_children: child_info = _create_xblock_child_info( xblock, course_outline, graders, include_children_predicate=include_children_predicate, ) else: child_info = None # Treat DEFAULT_START_DATE as a magic number that means the release date has not been set release_date = get_default_time_display( xblock.start) if xblock.start != DEFAULT_START_DATE else None if xblock.category != 'course': visibility_state = _compute_visibility_state( xblock, child_info, is_xblock_unit and has_changes) else: visibility_state = None published = modulestore().has_published_version(xblock) xblock_info = { "id": unicode(xblock.location), "display_name": xblock.display_name_with_default, "category": xblock.category, "edited_on": get_default_time_display(xblock.subtree_edited_on) if xblock.subtree_edited_on else None, "published": published, "published_on": get_default_time_display(xblock.published_on) if xblock.published_on else None, "studio_url": xblock_studio_url(xblock, parent_xblock), "released_to_students": datetime.now(UTC) > xblock.start, "release_date": release_date, "visibility_state": visibility_state, "has_explicit_staff_lock": xblock.fields['visible_to_staff_only'].is_set_on(xblock), "start": xblock.fields['start'].to_json(xblock.start), "graded": xblock.graded, "due_date": get_default_time_display(xblock.due), "due": xblock.fields['due'].to_json(xblock.due), "format": xblock.format, "course_graders": json.dumps([grader.get('type') for grader in graders]), "has_changes": has_changes, } if data is not None: xblock_info["data"] = data if metadata is not None: xblock_info["metadata"] = metadata if include_ancestor_info: xblock_info['ancestor_info'] = _create_xblock_ancestor_info( xblock, course_outline) if child_info: xblock_info['child_info'] = child_info if visibility_state == VisibilityState.staff_only: xblock_info["ancestor_has_staff_lock"] = ancestor_has_staff_lock( xblock, parent_xblock) else: xblock_info["ancestor_has_staff_lock"] = False # Currently, 'edited_by', 'published_by', and 'release_date_from' are only used by the # container page when rendering a unit. Since they are expensive to compute, only include them for units # that are not being rendered on the course outline. if is_xblock_unit and not course_outline: xblock_info["edited_by"] = safe_get_username(xblock.subtree_edited_by) xblock_info["published_by"] = safe_get_username(xblock.published_by) xblock_info[ "currently_visible_to_students"] = is_currently_visible_to_students( xblock) if release_date: xblock_info["release_date_from"] = _get_release_date_from(xblock) if visibility_state == VisibilityState.staff_only: xblock_info["staff_lock_from"] = _get_staff_lock_from(xblock) else: xblock_info["staff_lock_from"] = None if course_outline: if xblock_info["has_explicit_staff_lock"]: xblock_info["staff_only_message"] = True elif child_info and child_info["children"]: xblock_info["staff_only_message"] = all([ child["staff_only_message"] for child in child_info["children"] ]) else: xblock_info["staff_only_message"] = False return xblock_info
def edit_subsection(request, location): "Edit the subsection of a course" # check that we have permissions to edit this item try: course = get_course_for_item(location) except InvalidLocationError: return HttpResponseBadRequest() if not has_access(request.user, course.location): raise PermissionDenied() try: item = modulestore().get_item(location, depth=1) except ItemNotFoundError: return HttpResponseBadRequest() 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 %', 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, 'new_unit_category': 'vertical', '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 subsection_handler(request, tag=None, package_id=None, branch=None, version_guid=None, block=None): """ 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'): locator = BlockUsageLocator(package_id=package_id, branch=branch, version_guid=version_guid, block_id=block) try: old_location, course, item, lms_link = _get_item_in_course( request, locator) except ItemNotFoundError: return HttpResponseBadRequest() preview_link = get_lms_link_for_item( old_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 = 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() for unit in subsection_units: state = compute_publish_state(unit) if state in (PublishState.public, PublishState.draft): can_view_live = True break course_locator = loc_mapper().translate_location( course.location.course_id, course.location, False, True) 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(course_locator).graders), 'parent_item': parent, 'locator': locator, 'policy_metadata': policy_metadata, 'subsection_units': subsection_units, 'can_view_live': can_view_live }) else: return HttpResponseBadRequest("Only supports html requests")
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, preview_link = _get_item_in_course( request, usage_key) except ItemNotFoundError: return HttpResponseBadRequest() # 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 create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=False, include_child_info=False, course_outline=False, include_children_predicate=NEVER, parent_xblock=None, graders=None): """ Creates the information needed for client-side XBlockInfo. If data or metadata are not specified, their information will not be added (regardless of whether or not the xblock actually has data or metadata). There are three optional boolean parameters: include_ancestor_info - if true, ancestor info is added to the response include_child_info - if true, direct child info is included in the response course_outline - if true, the xblock is being rendered on behalf of the course outline. There are certain expensive computations that do not need to be included in this case. In addition, an optional include_children_predicate argument can be provided to define whether or not a particular xblock should have its children included. """ is_library_block = isinstance(xblock.location, LibraryUsageLocator) is_xblock_unit = is_unit(xblock, parent_xblock) # this should not be calculated for Sections and Subsections on Unit page or for library blocks has_changes = None if (is_xblock_unit or course_outline) and not is_library_block: has_changes = modulestore().has_changes(xblock) if graders is None: if not is_library_block: graders = CourseGradingModel.fetch( xblock.location.course_key).graders else: graders = [] # Filter the graders data as needed graders = _filter_entrance_exam_grader(graders) # Compute the child info first so it can be included in aggregate information for the parent should_visit_children = include_child_info and ( course_outline and not is_xblock_unit or not course_outline) if should_visit_children and xblock.has_children: child_info = _create_xblock_child_info( xblock, course_outline, graders, include_children_predicate=include_children_predicate, ) else: child_info = None if xblock.category != 'course': visibility_state = _compute_visibility_state( xblock, child_info, is_xblock_unit and has_changes) else: visibility_state = None published = modulestore().has_published_version( xblock) if not is_library_block else None # defining the default value 'True' for delete, drag and add new child actions in xblock_actions for each xblock. xblock_actions = { 'deletable': True, 'draggable': True, 'childAddable': True } explanatory_message = None # is_entrance_exam is inherited metadata. if xblock.category == 'chapter' and getattr(xblock, "is_entrance_exam", None): # Entrance exam section should not be deletable, draggable and not have 'New Subsection' button. xblock_actions['deletable'] = xblock_actions[ 'childAddable'] = xblock_actions['draggable'] = False if parent_xblock is None: parent_xblock = get_parent_xblock(xblock) explanatory_message = _( 'Students must score {score}% or higher to access course materials.' ).format(score=int(parent_xblock.entrance_exam_minimum_score_pct * 100)) xblock_info = { "id": unicode(xblock.location), "display_name": xblock.display_name_with_default, "category": xblock.category, "edited_on": get_default_time_display(xblock.subtree_edited_on) if xblock.subtree_edited_on else None, "published": published, "published_on": get_default_time_display(xblock.published_on) if published and xblock.published_on else None, "studio_url": xblock_studio_url(xblock, parent_xblock), "released_to_students": datetime.now(UTC) > xblock.start, "release_date": _get_release_date(xblock), "visibility_state": visibility_state, "has_explicit_staff_lock": xblock.fields['visible_to_staff_only'].is_set_on(xblock), "start": xblock.fields['start'].to_json(xblock.start), "graded": xblock.graded, "due_date": get_default_time_display(xblock.due), "due": xblock.fields['due'].to_json(xblock.due), "format": xblock.format, "course_graders": json.dumps([grader.get('type') for grader in graders]), "has_changes": has_changes, "actions": xblock_actions, "explanatory_message": explanatory_message } # Entrance exam subsection should be hidden. in_entrance_exam is inherited metadata, all children will have it. if xblock.category == 'sequential' and getattr(xblock, "in_entrance_exam", False): xblock_info["is_header_visible"] = False if data is not None: xblock_info["data"] = data if metadata is not None: xblock_info["metadata"] = metadata if include_ancestor_info: xblock_info['ancestor_info'] = _create_xblock_ancestor_info( xblock, course_outline) if child_info: xblock_info['child_info'] = child_info if visibility_state == VisibilityState.staff_only: xblock_info["ancestor_has_staff_lock"] = ancestor_has_staff_lock( xblock, parent_xblock) else: xblock_info["ancestor_has_staff_lock"] = False if course_outline: if xblock_info["has_explicit_staff_lock"]: xblock_info["staff_only_message"] = True elif child_info and child_info["children"]: xblock_info["staff_only_message"] = all([ child["staff_only_message"] for child in child_info["children"] ]) else: xblock_info["staff_only_message"] = False return xblock_info
def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=False, include_child_info=False, course_outline=False, include_children_predicate=NEVER, parent_xblock=None, graders=None): """ Creates the information needed for client-side XBlockInfo. If data or metadata are not specified, their information will not be added (regardless of whether or not the xblock actually has data or metadata). There are three optional boolean parameters: include_ancestor_info - if true, ancestor info is added to the response include_child_info - if true, direct child info is included in the response course_outline - if true, the xblock is being rendered on behalf of the course outline. There are certain expensive computations that do not need to be included in this case. In addition, an optional include_children_predicate argument can be provided to define whether or not a particular xblock should have its children included. """ is_library_block = isinstance(xblock.location, LibraryUsageLocator) is_xblock_unit = is_unit(xblock, parent_xblock) # this should not be calculated for Sections and Subsections on Unit page or for library blocks has_changes = None if (is_xblock_unit or course_outline) and not is_library_block: has_changes = modulestore().has_changes(xblock) if graders is None: if not is_library_block: graders = CourseGradingModel.fetch( xblock.location.course_key).graders else: graders = [] # Filter the graders data as needed graders = _filter_entrance_exam_grader(graders) # Compute the child info first so it can be included in aggregate information for the parent should_visit_children = include_child_info and ( course_outline and not is_xblock_unit or not course_outline) if should_visit_children and xblock.has_children: child_info = _create_xblock_child_info( xblock, course_outline, graders, include_children_predicate=include_children_predicate, ) else: child_info = None if xblock.category != 'course': visibility_state = _compute_visibility_state( xblock, child_info, is_xblock_unit and has_changes) else: visibility_state = None published = modulestore().has_published_version( xblock) if not is_library_block else None #instead of adding a new feature directly into xblock-info, we should add them into override_type. override_type = {} if getattr(xblock, "is_entrance_exam", None): override_type['is_entrance_exam'] = xblock.is_entrance_exam xblock_info = { "id": unicode(xblock.location), "display_name": xblock.display_name_with_default, "category": xblock.category, "edited_on": get_default_time_display(xblock.subtree_edited_on) if xblock.subtree_edited_on else None, "published": published, "published_on": get_default_time_display(xblock.published_on) if published and xblock.published_on else None, "studio_url": xblock_studio_url(xblock, parent_xblock), "released_to_students": datetime.now(UTC) > xblock.start, "release_date": _get_release_date(xblock), "visibility_state": visibility_state, "has_explicit_staff_lock": xblock.fields['visible_to_staff_only'].is_set_on(xblock), "start": xblock.fields['start'].to_json(xblock.start), "graded": xblock.graded, "due_date": get_default_time_display(xblock.due), "due": xblock.fields['due'].to_json(xblock.due), "format": xblock.format, "course_graders": json.dumps([grader.get('type') for grader in graders]), "has_changes": has_changes, "override_type": override_type, } if data is not None: xblock_info["data"] = data if metadata is not None: xblock_info["metadata"] = metadata if include_ancestor_info: xblock_info['ancestor_info'] = _create_xblock_ancestor_info( xblock, course_outline) if child_info: xblock_info['child_info'] = child_info if visibility_state == VisibilityState.staff_only: xblock_info["ancestor_has_staff_lock"] = ancestor_has_staff_lock( xblock, parent_xblock) else: xblock_info["ancestor_has_staff_lock"] = False if course_outline: if xblock_info["has_explicit_staff_lock"]: xblock_info["staff_only_message"] = True elif child_info and child_info["children"]: xblock_info["staff_only_message"] = all([ child["staff_only_message"] for child in child_info["children"] ]) else: xblock_info["staff_only_message"] = False return xblock_info