def visit(node, depth=1): """ Recursive generator function which yields CCX schedule nodes. We convert dates to string to get them ready for use by the js date widgets, which use text inputs. Visits students visible nodes only; nodes children of hidden ones are skipped as well. """ for child in node.get_children(): # in case the children are visible to staff only, skip them if child.visible_to_staff_only: continue start = get_override_for_ccx(ccx, child, "start", None) if start: start = str(start)[:-9] due = get_override_for_ccx(ccx, child, "due", None) if due: due = str(due)[:-9] hidden = get_override_for_ccx(ccx, child, "visible_to_staff_only", child.visible_to_staff_only) visited = { "location": str(child.location), "display_name": child.display_name, "category": child.category, "start": start, "due": due, "hidden": hidden, } if depth < 3: children = tuple(visit(child, depth + 1)) if children: visited["children"] = children yield visited else: yield visited
def override_fields(parent, data, graded, earliest=None, ccx_ids_to_delete=None): """ Recursively apply CCX schedule data to CCX by overriding the `visible_to_staff_only`, `start` and `due` fields for units in the course. """ if ccx_ids_to_delete is None: ccx_ids_to_delete = [] blocks = { str(child.location): child for child in parent.get_children()} for unit in data: block = blocks[unit['location']] override_field_for_ccx( ccx, block, 'visible_to_staff_only', unit['hidden']) start = parse_date(unit['start']) if start: if not earliest or start < earliest: earliest = start override_field_for_ccx(ccx, block, 'start', start) else: ccx_ids_to_delete.append(get_override_for_ccx(ccx, block, 'start_id')) clear_ccx_field_info_from_ccx_map(ccx, block, 'start') # Only subsection (aka sequential) and unit (aka vertical) have due dates. if 'due' in unit: # checking that the key (due) exist in dict (unit). due = parse_date(unit['due']) if due: override_field_for_ccx(ccx, block, 'due', due) else: ccx_ids_to_delete.append(get_override_for_ccx(ccx, block, 'due_id')) clear_ccx_field_info_from_ccx_map(ccx, block, 'due') else: # In case of section aka chapter we do not have due date. ccx_ids_to_delete.append(get_override_for_ccx(ccx, block, 'due_id')) clear_ccx_field_info_from_ccx_map(ccx, block, 'due') if not unit['hidden'] and block.graded: graded[block.format] = graded.get(block.format, 0) + 1 children = unit.get('children', None) # For a vertical, override start and due dates of all its problems. if unit.get('category', None) == u'vertical': for component in block.get_children(): # override start and due date of problem (Copy dates of vertical into problems) if start: override_field_for_ccx(ccx, component, 'start', start) if due: override_field_for_ccx(ccx, component, 'due', due) if children: override_fields(block, children, graded, earliest, ccx_ids_to_delete) return earliest, ccx_ids_to_delete
def override_fields(parent, data, graded, earliest=None, ccx_ids_to_delete=None): """ Recursively apply CCX schedule data to CCX by overriding the `visible_to_staff_only`, `start` and `due` fields for units in the course. """ if ccx_ids_to_delete is None: ccx_ids_to_delete = [] blocks = { str(child.location): child for child in parent.get_children()} for unit in data: block = blocks[unit['location']] override_field_for_ccx( ccx, block, 'visible_to_staff_only', unit['hidden']) start = parse_date(unit['start']) if start: if not earliest or start < earliest: earliest = start override_field_for_ccx(ccx, block, 'start', start) else: ccx_ids_to_delete.append(get_override_for_ccx(ccx, block, 'start_id')) clear_ccx_field_info_from_ccx_map(ccx, block, 'start') # Only subsection (aka sequential) and unit (aka vertical) have due dates. if 'due' in unit: # checking that the key (due) exist in dict (unit). due = parse_date(unit['due']) if due: override_field_for_ccx(ccx, block, 'due', due) else: ccx_ids_to_delete.append(get_override_for_ccx(ccx, block, 'due_id')) clear_ccx_field_info_from_ccx_map(ccx, block, 'due') else: # In case of section aka chapter we do not have due date. ccx_ids_to_delete.append(get_override_for_ccx(ccx, block, 'due_id')) clear_ccx_field_info_from_ccx_map(ccx, block, 'due') if not unit['hidden'] and block.graded: graded[block.format] = graded.get(block.format, 0) + 1 children = unit.get('children', None) # For a vertical, override start and due dates of all its problems. if unit.get('category', None) == u'vertical': for component in block.get_children(): # override start and due date of problem (Copy dates of vertical into problems) if start: override_field_for_ccx(ccx, component, 'start', start) if due: override_field_for_ccx(ccx, component, 'due', due) if children: override_fields(block, children, graded, earliest, ccx_ids_to_delete) return earliest, ccx_ids_to_delete
def dashboard(request, course, ccx=None): """ Display the CCX Coach Dashboard. """ # right now, we can only have one ccx per user and course # so, if no ccx is passed in, we can sefely redirect to that if ccx is None: ccx = get_ccx_for_coach(course, request.user) if ccx: url = reverse( "ccx_coach_dashboard", kwargs={"course_id": CCXLocator.from_course_locator(course.id, ccx.id)} ) return redirect(url) context = {"course": course, "ccx": ccx} if ccx: ccx_locator = CCXLocator.from_course_locator(course.id, ccx.id) schedule = get_ccx_schedule(course, ccx) grading_policy = get_override_for_ccx(ccx, course, "grading_policy", course.grading_policy) context["schedule"] = json.dumps(schedule, indent=4) context["save_url"] = reverse("save_ccx", kwargs={"course_id": ccx_locator}) context["ccx_members"] = CourseEnrollment.objects.filter(course_id=ccx_locator, is_active=True) context["gradebook_url"] = reverse("ccx_gradebook", kwargs={"course_id": ccx_locator}) context["grades_csv_url"] = reverse("ccx_grades_csv", kwargs={"course_id": ccx_locator}) context["grading_policy"] = json.dumps(grading_policy, indent=4) context["grading_policy_url"] = reverse("ccx_set_grading_policy", kwargs={"course_id": ccx_locator}) else: context["create_ccx_url"] = reverse("create_ccx", kwargs={"course_id": course.id}) return render_to_response("ccx/coach_dashboard.html", context)
def visit(node, depth=1): """ Recursive generator function which yields CCX schedule nodes. We convert dates to string to get them ready for use by the js date widgets, which use text inputs. Visits students visible nodes only; nodes children of hidden ones are skipped as well. Dates: Only start date is applicable to a section. If ccx coach did not override start date then getting it from the master course. Both start and due dates are applicable to a subsection (aka sequential). If ccx coach did not override these dates then getting these dates from corresponding subsection in master course. Unit inherits start date and due date from its subsection. If ccx coach did not override these dates then getting them from corresponding subsection in master course. """ for child in node.get_children(): # in case the children are visible to staff only, skip them if child.visible_to_staff_only: continue hidden = get_override_for_ccx(ccx, child, 'visible_to_staff_only', child.visible_to_staff_only) start = get_date(ccx, child, 'start') if depth > 1: # Subsection has both start and due dates and unit inherit dates from their subsections if depth == 2: due = get_date(ccx, child, 'due') elif depth == 3: # Get start and due date of subsection in case unit has not override dates. due = get_date(ccx, child, 'due', node) start = get_date(ccx, child, 'start', node) visited = { 'location': str(child.location), 'display_name': child.display_name, 'category': child.category, 'start': start, 'due': due, 'hidden': hidden, } else: visited = { 'location': str(child.location), 'display_name': child.display_name, 'category': child.category, 'start': start, 'hidden': hidden, } if depth < 3: children = tuple(visit(child, depth + 1)) if children: visited['children'] = children yield visited elif return_xblocks and depth == 3: visited['children'] = child.children yield visited else: yield visited
def get_date(ccx, node, date_type=None, parent_node=None): """ This returns override or master date for section, subsection or a unit. :param ccx: ccx instance :param node: chapter, subsection or unit :param date_type: start or due :param parent_node: parent of node :return: start or due date """ date = get_override_for_ccx(ccx, node, date_type, None) if date_type == "start": master_date = node.start else: master_date = node.due if date is not None: # Setting override date [start or due] date = date.strftime('%Y-%m-%d %H:%M') elif not parent_node and master_date is not None: # Setting date from master course date = master_date.strftime('%Y-%m-%d %H:%M') elif parent_node is not None: # Set parent date (vertical has same dates as subsections) date = get_date(ccx, node=parent_node, date_type=date_type) return date
def test_create_ccx(self): """ Create CCX. Follow redirect to coach dashboard, confirm we see the coach dashboard for the new CCX. """ self.make_coach() url = reverse('create_ccx', kwargs={'course_id': unicode(self.course.id)}) response = self.client.post(url, {'name': 'New CCX'}) self.assertEqual(response.status_code, 302) url = response.get('location') # pylint: disable=no-member response = self.client.get(url) self.assertEqual(response.status_code, 200) # Get the ccx_key path = urlparse.urlparse(url).path resolver = resolve(path) ccx_key = resolver.kwargs['course_id'] course_key = CourseKey.from_string(ccx_key) self.assertTrue(CourseEnrollment.is_enrolled(self.coach, course_key)) self.assertTrue(re.search('id="ccx-schedule"', response.content)) # check if the max amount of student that can be enrolled has been overridden ccx = CustomCourseForEdX.objects.get() course_enrollments = get_override_for_ccx( ccx, self.course, 'max_student_enrollments_allowed') self.assertEqual(course_enrollments, settings.CCX_MAX_STUDENTS_ALLOWED) # assert ccx creator has role=ccx_coach role = CourseCcxCoachRole(course_key) self.assertTrue(role.has_user(self.coach, refresh=True))
def test_save_without_min_count(self): """ POST grading policy without min_count field. """ self.make_coach() ccx = self.make_ccx() course_id = CCXLocator.from_course_locator(self.course.id, ccx.id) save_policy_url = reverse("ccx_set_grading_policy", kwargs={"course_id": course_id}) # This policy doesn't include a min_count field policy = { "GRADE_CUTOFFS": {"Pass": 0.5}, "GRADER": [{"weight": 0.15, "type": "Homework", "drop_count": 2, "short_label": "HW"}], } response = self.client.post(save_policy_url, {"policy": json.dumps(policy)}) self.assertEqual(response.status_code, 302) ccx = CustomCourseForEdX.objects.get() # Make sure grading policy adjusted policy = get_override_for_ccx(ccx, self.course, "grading_policy", self.course.grading_policy) self.assertEqual(len(policy["GRADER"]), 1) self.assertEqual(policy["GRADER"][0]["type"], "Homework") self.assertNotIn("min_count", policy["GRADER"][0]) save_ccx_url = reverse("save_ccx", kwargs={"course_id": course_id}) coach_dashboard_url = reverse("ccx_coach_dashboard", kwargs={"course_id": course_id}) response = self.client.get(coach_dashboard_url) schedule = json.loads(response.mako_context["schedule"]) # pylint: disable=no-member response = self.client.post(save_ccx_url, json.dumps(schedule), content_type="application/json") self.assertEqual(response.status_code, 200)
def get_date(ccx, node, date_type=None, parent_node=None): """ This returns override or master date for section, subsection or a unit. :param ccx: ccx instance :param node: chapter, subsection or unit :param date_type: start or due :param parent_node: parent of node :return: start or due date """ date = get_override_for_ccx(ccx, node, date_type, None) if date_type == "start": master_date = node.start else: master_date = node.due if date is not None: # Setting override date [start or due] date = date.strftime('%Y-%m-%d %H:%M') elif not parent_node and master_date is not None: # Setting date from master course date = master_date.strftime('%Y-%m-%d %H:%M') elif parent_node is not None: # Set parent date (vertical has same dates as subsections) date = get_date(ccx, node=parent_node, date_type=date_type) return date
def test_create_ccx(self): """ Create CCX. Follow redirect to coach dashboard, confirm we see the coach dashboard for the new CCX. """ self.make_coach() url = reverse( 'create_ccx', kwargs={'course_id': unicode(self.course.id)}) response = self.client.post(url, {'name': 'New CCX'}) self.assertEqual(response.status_code, 302) url = response.get('location') # pylint: disable=no-member response = self.client.get(url) self.assertEqual(response.status_code, 200) # Get the ccx_key path = urlparse.urlparse(url).path resolver = resolve(path) ccx_key = resolver.kwargs['course_id'] course_key = CourseKey.from_string(ccx_key) self.assertTrue(CourseEnrollment.is_enrolled(self.coach, course_key)) self.assertTrue(re.search('id="ccx-schedule"', response.content)) # check if the max amount of student that can be enrolled has been overridden ccx = CustomCourseForEdX.objects.get() course_enrollments = get_override_for_ccx(ccx, self.course, 'max_student_enrollments_allowed') self.assertEqual(course_enrollments, settings.CCX_MAX_STUDENTS_ALLOWED)
def override_fields(parent, data, graded, earliest=None, ccx_ids_to_delete=None): """ Recursively apply CCX schedule data to CCX by overriding the `visible_to_staff_only`, `start` and `due` fields for units in the course. """ if ccx_ids_to_delete is None: ccx_ids_to_delete = [] blocks = {str(child.location): child for child in parent.get_children()} for unit in data: block = blocks[unit["location"]] override_field_for_ccx(ccx, block, "visible_to_staff_only", unit["hidden"]) start = parse_date(unit["start"]) if start: if not earliest or start < earliest: earliest = start override_field_for_ccx(ccx, block, "start", start) else: ccx_ids_to_delete.append(get_override_for_ccx(ccx, block, "start_id")) clear_ccx_field_info_from_ccx_map(ccx, block, "start") due = parse_date(unit["due"]) if due: override_field_for_ccx(ccx, block, "due", due) else: ccx_ids_to_delete.append(get_override_for_ccx(ccx, block, "due_id")) clear_ccx_field_info_from_ccx_map(ccx, block, "due") if not unit["hidden"] and block.graded: graded[block.format] = graded.get(block.format, 0) + 1 children = unit.get("children", None) # For a vertical, override start and due dates of all its problems. if unit.get("category", None) == u"vertical": for component in block.get_children(): # override start and due date of problem (Copy dates of vertical into problems) if start: override_field_for_ccx(ccx, component, "start", start) if due: override_field_for_ccx(ccx, component, "due", due) if children: override_fields(block, children, graded, earliest, ccx_ids_to_delete) return earliest, ccx_ids_to_delete
def visit(node, depth=1): """ Recursive generator function which yields CCX schedule nodes. We convert dates to string to get them ready for use by the js date widgets, which use text inputs. Visits students visible nodes only; nodes children of hidden ones are skipped as well. Dates: Only start date is applicable to a section. If ccx coach did not override start date then getting it from the master course. Both start and due dates are applicable to a subsection (aka sequential). If ccx coach did not override these dates then getting these dates from corresponding subsection in master course. Unit inherits start date and due date from its subsection. If ccx coach did not override these dates then getting them from corresponding subsection in master course. """ for child in node.get_children(): # in case the children are visible to staff only, skip them if child.visible_to_staff_only: continue hidden = get_override_for_ccx( ccx, child, 'visible_to_staff_only', child.visible_to_staff_only) start = get_date(ccx, child, 'start') if depth > 1: # Subsection has both start and due dates and unit inherit dates from their subsections if depth == 2: due = get_date(ccx, child, 'due') elif depth == 3: # Get start and due date of subsection in case unit has not override dates. due = get_date(ccx, child, 'due', node) start = get_date(ccx, child, 'start', node) visited = { 'location': str(child.location), 'display_name': child.display_name, 'category': child.category, 'start': start, 'due': due, 'hidden': hidden, } else: visited = { 'location': str(child.location), 'display_name': child.display_name, 'category': child.category, 'start': start, 'hidden': hidden, } if depth < 3: children = tuple(visit(child, depth + 1)) if children: visited['children'] = children yield visited else: yield visited
def override_fields(parent, data, graded, earliest=None, ccx_ids_to_delete=None): """ Recursively apply CCX schedule data to CCX by overriding the `visible_to_staff_only`, `start` and `due` fields for units in the course. """ if ccx_ids_to_delete is None: ccx_ids_to_delete = [] blocks = { str(child.location): child for child in parent.get_children()} for unit in data: block = blocks[unit['location']] override_field_for_ccx( ccx, block, 'visible_to_staff_only', unit['hidden']) start = parse_date(unit['start']) if start: if not earliest or start < earliest: earliest = start override_field_for_ccx(ccx, block, 'start', start) else: ccx_ids_to_delete.append(get_override_for_ccx(ccx, block, 'start_id')) clear_ccx_field_info_from_ccx_map(ccx, block, 'start') due = parse_date(unit['due']) if due: override_field_for_ccx(ccx, block, 'due', due) else: ccx_ids_to_delete.append(get_override_for_ccx(ccx, block, 'due_id')) clear_ccx_field_info_from_ccx_map(ccx, block, 'due') if not unit['hidden'] and block.graded: graded[block.format] = graded.get(block.format, 0) + 1 children = unit.get('children', None) if children: override_fields(block, children, graded, earliest, ccx_ids_to_delete) return earliest, ccx_ids_to_delete
def test_save_without_min_count(self): """ POST grading policy without min_count field. """ self.make_coach() ccx = self.make_ccx() course_id = CCXLocator.from_course_locator(self.course.id, ccx.id) save_policy_url = reverse( 'ccx_set_grading_policy', kwargs={'course_id': course_id}) # This policy doesn't include a min_count field policy = { "GRADE_CUTOFFS": { "Pass": 0.5 }, "GRADER": [ { "weight": 0.15, "type": "Homework", "drop_count": 2, "short_label": "HW" } ] } response = self.client.post( save_policy_url, {"policy": json.dumps(policy)} ) self.assertEqual(response.status_code, 302) ccx = CustomCourseForEdX.objects.get() # Make sure grading policy adjusted policy = get_override_for_ccx( ccx, self.course, 'grading_policy', self.course.grading_policy ) self.assertEqual(len(policy['GRADER']), 1) self.assertEqual(policy['GRADER'][0]['type'], 'Homework') self.assertNotIn('min_count', policy['GRADER'][0]) save_ccx_url = reverse('save_ccx', kwargs={'course_id': course_id}) coach_dashboard_url = reverse( 'ccx_coach_dashboard', kwargs={'course_id': course_id} ) response = self.client.get(coach_dashboard_url) schedule = json.loads(response.mako_context['schedule']) # pylint: disable=no-member response = self.client.post( save_ccx_url, json.dumps(schedule), content_type='application/json' ) self.assertEqual(response.status_code, 200)
def dashboard(request, course, ccx=None): """ Display the CCX Coach Dashboard. """ # right now, we can only have one ccx per user and course # so, if no ccx is passed in, we can sefely redirect to that if ccx is None: ccx = get_ccx_for_coach(course, request.user) if ccx: url = reverse('ccx_coach_dashboard', kwargs={ 'course_id': CCXLocator.from_course_locator( course.id, ccx.id) }) return redirect(url) context = { 'course': course, 'ccx': ccx, } context.update(get_ccx_creation_dict(course)) if ccx: ccx_locator = CCXLocator.from_course_locator(course.id, unicode(ccx.id)) # At this point we are done with verification that current user is ccx coach. assign_coach_role_to_ccx(ccx_locator, request.user, course.id) schedule = get_ccx_schedule(course, ccx) grading_policy = get_override_for_ccx(ccx, course, 'grading_policy', course.grading_policy) context['schedule'] = json.dumps(schedule, indent=4) context['save_url'] = reverse('save_ccx', kwargs={'course_id': ccx_locator}) context['ccx_members'] = CourseEnrollment.objects.filter( course_id=ccx_locator, is_active=True) context['gradebook_url'] = reverse('ccx_gradebook', kwargs={'course_id': ccx_locator}) context['grades_csv_url'] = reverse('ccx_grades_csv', kwargs={'course_id': ccx_locator}) context['grading_policy'] = json.dumps(grading_policy, indent=4) context['grading_policy_url'] = reverse( 'ccx_set_grading_policy', kwargs={'course_id': ccx_locator}) with ccx_course(ccx_locator) as course: context['course'] = course else: context['create_ccx_url'] = reverse('create_ccx', kwargs={'course_id': course.id}) return render_to_response('ccx/coach_dashboard.html', context)
def dashboard(request, course, ccx=None): """ Display the CCX Coach Dashboard. """ # right now, we can only have one ccx per user and course # so, if no ccx is passed in, we can sefely redirect to that if ccx is None: ccx = get_ccx_for_coach(course, request.user) if ccx: url = reverse( 'ccx_coach_dashboard', kwargs={ 'course_id': CCXLocator.from_course_locator(course.id, unicode(ccx.id)) }) return redirect(url) context = { 'course': course, 'ccx': ccx, } context.update(get_ccx_creation_dict(course)) if ccx: ccx_locator = CCXLocator.from_course_locator(course.id, unicode(ccx.id)) # At this point we are done with verification that current user is ccx coach. assign_staff_role_to_ccx(ccx_locator, request.user, course.id) schedule = get_ccx_schedule(course, ccx) grading_policy = get_override_for_ccx(ccx, course, 'grading_policy', course.grading_policy) context['schedule'] = json.dumps(schedule, indent=4) context['save_url'] = reverse( 'save_ccx', kwargs={'course_id': ccx_locator}) context['ccx_members'] = CourseEnrollment.objects.filter( course_id=ccx_locator, is_active=True) context['gradebook_url'] = reverse( 'ccx_gradebook', kwargs={'course_id': ccx_locator}) context['grades_csv_url'] = reverse( 'ccx_grades_csv', kwargs={'course_id': ccx_locator}) context['grading_policy'] = json.dumps(grading_policy, indent=4) context['grading_policy_url'] = reverse( 'ccx_set_grading_policy', kwargs={'course_id': ccx_locator}) with ccx_course(ccx_locator) as course: context['course'] = course else: context['create_ccx_url'] = reverse( 'create_ccx', kwargs={'course_id': course.id}) return render_to_response('ccx/coach_dashboard.html', context)
def edit_ccx_context(course, ccx, user, **kwargs): ccx_locator = CCXLocator.from_course_locator(course.id, unicode(ccx.pk)) schedule = get_ccx_schedule(course, ccx) grading_policy = get_override_for_ccx(ccx, course, 'grading_policy', course.grading_policy) context = {} context['ccx_locator'] = ccx_locator context['modify_access_url'] = reverse('modify_access', kwargs={'course_id': ccx_locator}) context['schedule'] = json.dumps(schedule, indent=4) context['save_url'] = reverse('save_ccx', kwargs={'course_id': ccx_locator}) non_student_user_ids = CourseAccessRole.objects.filter( course_id=ccx_locator).values_list('user_id', flat=True) ccx_student_enrollments = CourseEnrollment.objects.filter( course_id=ccx_locator, is_active=True).exclude(user_id__in=non_student_user_ids) context['ccx_student_enrollments'] = ccx_student_enrollments context['gradebook_url'] = reverse('ccx_gradebook', kwargs={'course_id': ccx_locator}) context['grades_csv_url'] = reverse('ccx_grades_csv', kwargs={'course_id': ccx_locator}) context['grading_policy'] = json.dumps(grading_policy, indent=4) context['grading_policy_url'] = reverse('ccx_set_grading_policy', kwargs={'course_id': ccx_locator}) context['STATE_CHOICES'] = STATE_CHOICES all_facilitators = get_facilitators(ccx.affiliate) added_facilitator_user_ids = CourseAccessRole.objects.filter( course_id=ccx_locator, role=AffiliateMembership.CCX_COACH).values_list('user_id', flat=True) context['added_facilitators'] = all_facilitators.filter( id__in=added_facilitator_user_ids) context['not_added_facilitators'] = all_facilitators.exclude( id__in=added_facilitator_user_ids) with ccx_course(ccx_locator) as course: context['course'] = course context['edit_ccx_url'] = reverse('edit_ccx', kwargs={'course_id': ccx_locator}) context['edit_ccx_dasboard_url'] = reverse( 'ccx_edit_course_view', kwargs={'course_id': ccx_locator}) return context
def visit(node, depth=1): """ Recursive generator function which yields CCX schedule nodes. We convert dates to string to get them ready for use by the js date widgets, which use text inputs. Visits students visible nodes only; nodes children of hidden ones are skipped as well. """ for child in node.get_children(): # in case the children are visible to staff only, skip them if child.visible_to_staff_only: continue start = get_override_for_ccx(ccx, child, 'start', None) if start: start = str(start)[:-9] due = get_override_for_ccx(ccx, child, 'due', None) if due: due = str(due)[:-9] hidden = get_override_for_ccx( ccx, child, 'visible_to_staff_only', child.visible_to_staff_only) visited = { 'location': str(child.location), 'display_name': child.display_name, 'category': child.category, 'start': start, 'due': due, 'hidden': hidden, } if depth < 3: children = tuple(visit(child, depth + 1)) if children: visited['children'] = children yield visited else: yield visited
def test_create_ccx(self, ccx_name='New CCX'): """ Create CCX. Follow redirect to coach dashboard, confirm we see the coach dashboard for the new CCX. """ self.make_coach() url = reverse( 'create_ccx', kwargs={'course_id': unicode(self.course.id)}) response = self.client.post(url, {'name': ccx_name}) self.assertEqual(response.status_code, 302) url = response.get('location') # pylint: disable=no-member response = self.client.get(url) self.assertEqual(response.status_code, 200) # Get the ccx_key path = urlparse.urlparse(url).path resolver = resolve(path) ccx_key = resolver.kwargs['course_id'] course_key = CourseKey.from_string(ccx_key) self.assertTrue(CourseEnrollment.is_enrolled(self.coach, course_key)) self.assertTrue(re.search('id="ccx-schedule"', response.content)) # check if the max amount of student that can be enrolled has been overridden ccx = CustomCourseForEdX.objects.get() course_enrollments = get_override_for_ccx(ccx, self.course, 'max_student_enrollments_allowed') self.assertEqual(course_enrollments, settings.CCX_MAX_STUDENTS_ALLOWED) # assert ccx creator has role=ccx_coach role = CourseCcxCoachRole(course_key) self.assertTrue(role.has_user(self.coach)) # assert that staff and instructors of master course has staff and instructor roles on ccx list_staff_master_course = list_with_level(self.course, 'staff') list_instructor_master_course = list_with_level(self.course, 'instructor') with ccx_course(course_key) as course_ccx: list_staff_ccx_course = list_with_level(course_ccx, 'staff') self.assertEqual(len(list_staff_master_course), len(list_staff_ccx_course)) self.assertEqual(list_staff_master_course[0].email, list_staff_ccx_course[0].email) list_instructor_ccx_course = list_with_level(course_ccx, 'instructor') self.assertEqual(len(list_instructor_ccx_course), len(list_instructor_master_course)) self.assertEqual(list_instructor_ccx_course[0].email, list_instructor_master_course[0].email)
def save_ccx(request, course, ccx=None): """ Save changes to CCX. """ if not ccx: raise Http404 def override_fields(parent, data, graded, earliest=None, ccx_ids_to_delete=None): """ Recursively apply CCX schedule data to CCX by overriding the `visible_to_staff_only`, `start` and `due` fields for units in the course. """ if ccx_ids_to_delete is None: ccx_ids_to_delete = [] blocks = {str(child.location): child for child in parent.get_children()} for unit in data: block = blocks[unit["location"]] override_field_for_ccx(ccx, block, "visible_to_staff_only", unit["hidden"]) start = parse_date(unit["start"]) if start: if not earliest or start < earliest: earliest = start override_field_for_ccx(ccx, block, "start", start) else: ccx_ids_to_delete.append(get_override_for_ccx(ccx, block, "start_id")) clear_ccx_field_info_from_ccx_map(ccx, block, "start") due = parse_date(unit["due"]) if due: override_field_for_ccx(ccx, block, "due", due) else: ccx_ids_to_delete.append(get_override_for_ccx(ccx, block, "due_id")) clear_ccx_field_info_from_ccx_map(ccx, block, "due") if not unit["hidden"] and block.graded: graded[block.format] = graded.get(block.format, 0) + 1 children = unit.get("children", None) # For a vertical, override start and due dates of all its problems. if unit.get("category", None) == u"vertical": for component in block.get_children(): # override start and due date of problem (Copy dates of vertical into problems) if start: override_field_for_ccx(ccx, component, "start", start) if due: override_field_for_ccx(ccx, component, "due", due) if children: override_fields(block, children, graded, earliest, ccx_ids_to_delete) return earliest, ccx_ids_to_delete graded = {} earliest, ccx_ids_to_delete = override_fields(course, json.loads(request.body), graded, []) bulk_delete_ccx_override_fields(ccx, ccx_ids_to_delete) if earliest: override_field_for_ccx(ccx, course, "start", earliest) # Attempt to automatically adjust grading policy changed = False policy = get_override_for_ccx(ccx, course, "grading_policy", course.grading_policy) policy = deepcopy(policy) grader = policy["GRADER"] for section in grader: count = graded.get(section.get("type"), 0) if count < section.get("min_count", 0): changed = True section["min_count"] = count if changed: override_field_for_ccx(ccx, course, "grading_policy", policy) return HttpResponse( json.dumps({"schedule": get_ccx_schedule(course, ccx), "grading_policy": json.dumps(policy, indent=4)}), content_type="application/json", )
def save_ccx(request, course, ccx=None): """ Save changes to CCX. """ if not ccx: raise Http404 def override_fields(parent, data, graded, earliest=None, ccx_ids_to_delete=None): """ Recursively apply CCX schedule data to CCX by overriding the `visible_to_staff_only`, `start` and `due` fields for units in the course. """ if ccx_ids_to_delete is None: ccx_ids_to_delete = [] blocks = { str(child.location): child for child in parent.get_children()} for unit in data: block = blocks[unit['location']] override_field_for_ccx( ccx, block, 'visible_to_staff_only', unit['hidden']) start = parse_date(unit['start']) if start: if not earliest or start < earliest: earliest = start override_field_for_ccx(ccx, block, 'start', start) else: ccx_ids_to_delete.append(get_override_for_ccx(ccx, block, 'start_id')) clear_ccx_field_info_from_ccx_map(ccx, block, 'start') # Only subsection (aka sequential) and unit (aka vertical) have due dates. if 'due' in unit: # checking that the key (due) exist in dict (unit). due = parse_date(unit['due']) if due: override_field_for_ccx(ccx, block, 'due', due) else: ccx_ids_to_delete.append(get_override_for_ccx(ccx, block, 'due_id')) clear_ccx_field_info_from_ccx_map(ccx, block, 'due') else: # In case of section aka chapter we do not have due date. ccx_ids_to_delete.append(get_override_for_ccx(ccx, block, 'due_id')) clear_ccx_field_info_from_ccx_map(ccx, block, 'due') if not unit['hidden'] and block.graded: graded[block.format] = graded.get(block.format, 0) + 1 children = unit.get('children', None) # For a vertical, override start and due dates of all its problems. if unit.get('category', None) == u'vertical': for component in block.get_children(): # override start and due date of problem (Copy dates of vertical into problems) if start: override_field_for_ccx(ccx, component, 'start', start) if due: override_field_for_ccx(ccx, component, 'due', due) if children: override_fields(block, children, graded, earliest, ccx_ids_to_delete) return earliest, ccx_ids_to_delete graded = {} earliest, ccx_ids_to_delete = override_fields(course, json.loads(request.body), graded, []) bulk_delete_ccx_override_fields(ccx, ccx_ids_to_delete) if earliest: override_field_for_ccx(ccx, course, 'start', earliest) # Attempt to automatically adjust grading policy changed = False policy = get_override_for_ccx( ccx, course, 'grading_policy', course.grading_policy ) policy = deepcopy(policy) grader = policy['GRADER'] for section in grader: count = graded.get(section.get('type'), 0) if count < section.get('min_count', 0): changed = True section['min_count'] = count if changed: override_field_for_ccx(ccx, course, 'grading_policy', policy) return HttpResponse( json.dumps({ 'schedule': get_ccx_schedule(course, ccx), 'grading_policy': json.dumps(policy, indent=4)}), content_type='application/json', )
def test_edit_schedule(self, today): """ Get CCX schedule, modify it, save it. """ today.return_value = datetime.datetime(2014, 11, 25, tzinfo=pytz.UTC) self.make_coach() ccx = self.make_ccx() url = reverse( 'ccx_coach_dashboard', kwargs={'course_id': CCXLocator.from_course_locator(self.course.id, ccx.id)}) response = self.client.get(url) schedule = json.loads(response.mako_context['schedule']) # pylint: disable=no-member self.assertEqual(len(schedule), 2) self.assertEqual(schedule[0]['hidden'], False) # If a coach does not override dates, then dates will be imported from master course. self.assertEqual( schedule[0]['start'], self.chapters[0].start.strftime('%Y-%m-%d %H:%M') ) self.assertEqual( schedule[0]['children'][0]['start'], self.sequentials[0].start.strftime('%Y-%m-%d %H:%M') ) if self.sequentials[0].due: expected_due = self.sequentials[0].due.strftime('%Y-%m-%d %H:%M') else: expected_due = None self.assertEqual(schedule[0]['children'][0]['due'], expected_due) url = reverse( 'save_ccx', kwargs={'course_id': CCXLocator.from_course_locator(self.course.id, ccx.id)}) def unhide(unit): """ Recursively unhide a unit and all of its children in the CCX schedule. """ unit['hidden'] = False for child in unit.get('children', ()): unhide(child) unhide(schedule[0]) schedule[0]['start'] = u'2014-11-20 00:00' schedule[0]['children'][0]['due'] = u'2014-12-25 00:00' # what a jerk! schedule[0]['children'][0]['children'][0]['start'] = u'2014-12-20 00:00' schedule[0]['children'][0]['children'][0]['due'] = u'2014-12-25 00:00' response = self.client.post( url, json.dumps(schedule), content_type='application/json' ) schedule = json.loads(response.content)['schedule'] self.assertEqual(schedule[0]['hidden'], False) self.assertEqual(schedule[0]['start'], u'2014-11-20 00:00') self.assertEqual( schedule[0]['children'][0]['due'], u'2014-12-25 00:00' ) self.assertEqual( schedule[0]['children'][0]['children'][0]['due'], u'2014-12-25 00:00' ) self.assertEqual( schedule[0]['children'][0]['children'][0]['start'], u'2014-12-20 00:00' ) # Make sure start date set on course, follows start date of earliest # scheduled chapter ccx = CustomCourseForEdX.objects.get() course_start = get_override_for_ccx(ccx, self.course, 'start') self.assertEqual(str(course_start)[:-9], self.chapters[0].start.strftime('%Y-%m-%d %H:%M')) # Make sure grading policy adjusted policy = get_override_for_ccx(ccx, self.course, 'grading_policy', self.course.grading_policy) self.assertEqual(policy['GRADER'][0]['type'], 'Homework') self.assertEqual(policy['GRADER'][0]['min_count'], 8) self.assertEqual(policy['GRADER'][1]['type'], 'Lab') self.assertEqual(policy['GRADER'][1]['min_count'], 0) self.assertEqual(policy['GRADER'][2]['type'], 'Midterm Exam') self.assertEqual(policy['GRADER'][2]['min_count'], 0) self.assertEqual(policy['GRADER'][3]['type'], 'Final Exam') self.assertEqual(policy['GRADER'][3]['min_count'], 0)
def test_edit_schedule(self, today): """ Get CCX schedule, modify it, save it. """ today.return_value = datetime.datetime(2014, 11, 25, tzinfo=UTC) self.make_coach() ccx = self.make_ccx() url = reverse( 'ccx_coach_dashboard', kwargs={'course_id': CCXLocator.from_course_locator(self.course.id, ccx.id)}) response = self.client.get(url) schedule = json.loads(response.mako_context['schedule']) assert len(schedule) == 2 assert schedule[0]['hidden'] is False # If a coach does not override dates, then dates will be imported from master course. assert schedule[0]['start'] == self.chapters[0].start.strftime('%Y-%m-%d %H:%M') assert schedule[0]['children'][0]['start'] == self.sequentials[0].start.strftime('%Y-%m-%d %H:%M') if self.sequentials[0].due: expected_due = self.sequentials[0].due.strftime('%Y-%m-%d %H:%M') else: expected_due = None assert schedule[0]['children'][0]['due'] == expected_due url = reverse( 'save_ccx', kwargs={'course_id': CCXLocator.from_course_locator(self.course.id, ccx.id)}) unhide(schedule[0]) schedule[0]['start'] = '2014-11-20 00:00' schedule[0]['children'][0]['due'] = '2014-12-25 00:00' # what a jerk! schedule[0]['children'][0]['children'][0]['start'] = '2014-12-20 00:00' schedule[0]['children'][0]['children'][0]['due'] = '2014-12-25 00:00' response = self.client.post( url, json.dumps(schedule), content_type='application/json' ) schedule = json.loads(response.content.decode('utf-8'))['schedule'] assert schedule[0]['hidden'] is False assert schedule[0]['start'] == '2014-11-20 00:00' assert schedule[0]['children'][0]['due'] == '2014-12-25 00:00' assert schedule[0]['children'][0]['children'][0]['due'] == '2014-12-25 00:00' assert schedule[0]['children'][0]['children'][0]['start'] == '2014-12-20 00:00' # Make sure start date set on course, follows start date of earliest # scheduled chapter ccx = CustomCourseForEdX.objects.get() course_start = get_override_for_ccx(ccx, self.course, 'start') assert str(course_start)[:(- 9)] == self.chapters[0].start.strftime('%Y-%m-%d %H:%M') # Make sure grading policy adjusted policy = get_override_for_ccx(ccx, self.course, 'grading_policy', self.course.grading_policy) assert policy['GRADER'][0]['type'] == 'Homework' assert policy['GRADER'][0]['min_count'] == 8 assert policy['GRADER'][1]['type'] == 'Lab' assert policy['GRADER'][1]['min_count'] == 0 assert policy['GRADER'][2]['type'] == 'Midterm Exam' assert policy['GRADER'][2]['min_count'] == 0 assert policy['GRADER'][3]['type'] == 'Final Exam' assert policy['GRADER'][3]['min_count'] == 0
def test_edit_schedule(self, today): """ Get CCX schedule, modify it, save it. """ today.return_value = datetime.datetime(2014, 11, 25, tzinfo=pytz.UTC) self.make_coach() ccx = self.make_ccx() url = reverse( 'ccx_coach_dashboard', kwargs={'course_id': CCXLocator.from_course_locator(self.course.id, ccx.id)}) response = self.client.get(url) schedule = json.loads(response.mako_context['schedule']) # pylint: disable=no-member self.assertEqual(len(schedule), 2) self.assertEqual(schedule[0]['hidden'], False) # If a coach does not override dates, then dates will be imported from master course. self.assertEqual( schedule[0]['start'], self.chapters[0].start.strftime('%Y-%m-%d %H:%M') ) self.assertEqual( schedule[0]['children'][0]['start'], self.sequentials[0].start.strftime('%Y-%m-%d %H:%M') ) if self.sequentials[0].due: expected_due = self.sequentials[0].due.strftime('%Y-%m-%d %H:%M') else: expected_due = None self.assertEqual(schedule[0]['children'][0]['due'], expected_due) url = reverse( 'save_ccx', kwargs={'course_id': CCXLocator.from_course_locator(self.course.id, ccx.id)}) def unhide(unit): """ Recursively unhide a unit and all of its children in the CCX schedule. """ unit['hidden'] = False for child in unit.get('children', ()): unhide(child) unhide(schedule[0]) schedule[0]['start'] = u'2014-11-20 00:00' schedule[0]['children'][0]['due'] = u'2014-12-25 00:00' # what a jerk! schedule[0]['children'][0]['children'][0]['start'] = u'2014-12-20 00:00' schedule[0]['children'][0]['children'][0]['due'] = u'2014-12-25 00:00' response = self.client.post( url, json.dumps(schedule), content_type='application/json' ) schedule = json.loads(response.content)['schedule'] self.assertEqual(schedule[0]['hidden'], False) self.assertEqual(schedule[0]['start'], u'2014-11-20 00:00') self.assertEqual( schedule[0]['children'][0]['due'], u'2014-12-25 00:00' ) self.assertEqual( schedule[0]['children'][0]['children'][0]['due'], u'2014-12-25 00:00' ) self.assertEqual( schedule[0]['children'][0]['children'][0]['start'], u'2014-12-20 00:00' ) # Make sure start date set on course, follows start date of earliest # scheduled chapter ccx = CustomCourseForEdX.objects.get() course_start = get_override_for_ccx(ccx, self.course, 'start') self.assertEqual(str(course_start)[:-9], self.chapters[0].start.strftime('%Y-%m-%d %H:%M')) # Make sure grading policy adjusted policy = get_override_for_ccx(ccx, self.course, 'grading_policy', self.course.grading_policy) self.assertEqual(policy['GRADER'][0]['type'], 'Homework') self.assertEqual(policy['GRADER'][0]['min_count'], 8) self.assertEqual(policy['GRADER'][1]['type'], 'Lab') self.assertEqual(policy['GRADER'][1]['min_count'], 0) self.assertEqual(policy['GRADER'][2]['type'], 'Midterm Exam') self.assertEqual(policy['GRADER'][2]['min_count'], 0) self.assertEqual(policy['GRADER'][3]['type'], 'Final Exam') self.assertEqual(policy['GRADER'][3]['min_count'], 0)
def test_create_ccx(self, ccx_name='New CCX'): """ Create CCX. Follow redirect to coach dashboard, confirm we see the coach dashboard for the new CCX. """ self.make_coach() url = reverse( 'create_ccx', kwargs={'course_id': str(self.course.id)}) response = self.client.post(url, {'name': ccx_name}) assert response.status_code == 302 url = response.get('location') response = self.client.get(url) assert response.status_code == 200 # Get the ccx_key path = six.moves.urllib.parse.urlparse(url).path resolver = resolve(path) ccx_key = resolver.kwargs['course_id'] course_key = CourseKey.from_string(ccx_key) assert CourseEnrollment.is_enrolled(self.coach, course_key) assert re.search('id="ccx-schedule"', response.content.decode('utf-8')) # check if the max amount of student that can be enrolled has been overridden ccx = CustomCourseForEdX.objects.get() course_enrollments = get_override_for_ccx(ccx, self.course, 'max_student_enrollments_allowed') assert course_enrollments == settings.CCX_MAX_STUDENTS_ALLOWED # check if the course display name is properly set course_display_name = get_override_for_ccx(ccx, self.course, 'display_name') assert course_display_name == ccx_name # check if the course display name is properly set in modulestore course_display_name = self.mstore.get_course(ccx.locator).display_name assert course_display_name == ccx_name # assert ccx creator has role=staff role = CourseStaffRole(course_key) assert role.has_user(self.coach) # assert that staff and instructors of master course has staff and instructor roles on ccx list_staff_master_course = list_with_level(self.course, 'staff') list_instructor_master_course = list_with_level(self.course, 'instructor') # assert that forum roles are seeded assert are_permissions_roles_seeded(course_key) assert has_forum_access(self.coach.username, course_key, FORUM_ROLE_ADMINISTRATOR) with ccx_course(course_key) as course_ccx: list_staff_ccx_course = list_with_level(course_ccx, 'staff') # The "Coach" in the parent course becomes "Staff" on the CCX, so the CCX should have 1 "Staff" # user more than the parent course assert (len(list_staff_master_course) + 1) == len(list_staff_ccx_course) assert list_staff_master_course[0].email in [ccx_staff.email for ccx_staff in list_staff_ccx_course] # Make sure the "Coach" on the parent course is "Staff" on the CCX assert self.coach in list_staff_ccx_course list_instructor_ccx_course = list_with_level(course_ccx, 'instructor') assert len(list_instructor_ccx_course) == len(list_instructor_master_course) assert list_instructor_ccx_course[0].email == list_instructor_master_course[0].email
def test_edit_schedule(self, today): """ Get CCX schedule, modify it, save it. """ today.return_value = datetime.datetime(2014, 11, 25, tzinfo=pytz.UTC) self.make_coach() ccx = self.make_ccx() url = reverse( "ccx_coach_dashboard", kwargs={"course_id": CCXLocator.from_course_locator(self.course.id, ccx.id)} ) response = self.client.get(url) schedule = json.loads(response.mako_context["schedule"]) # pylint: disable=no-member self.assertEqual(len(schedule), 2) self.assertEqual(schedule[0]["hidden"], False) # If a coach does not override dates, then dates will be imported from master course. self.assertEqual(schedule[0]["start"], self.chapters[0].start.strftime("%Y-%m-%d %H:%M")) self.assertEqual(schedule[0]["children"][0]["start"], self.sequentials[0].start.strftime("%Y-%m-%d %H:%M")) if self.sequentials[0].due: expected_due = self.sequentials[0].due.strftime("%Y-%m-%d %H:%M") else: expected_due = None self.assertEqual(schedule[0]["children"][0]["due"], expected_due) url = reverse("save_ccx", kwargs={"course_id": CCXLocator.from_course_locator(self.course.id, ccx.id)}) def unhide(unit): """ Recursively unhide a unit and all of its children in the CCX schedule. """ unit["hidden"] = False for child in unit.get("children", ()): unhide(child) unhide(schedule[0]) schedule[0]["start"] = u"2014-11-20 00:00" schedule[0]["children"][0]["due"] = u"2014-12-25 00:00" # what a jerk! schedule[0]["children"][0]["children"][0]["start"] = u"2014-12-20 00:00" schedule[0]["children"][0]["children"][0]["due"] = u"2014-12-25 00:00" response = self.client.post(url, json.dumps(schedule), content_type="application/json") schedule = json.loads(response.content)["schedule"] self.assertEqual(schedule[0]["hidden"], False) self.assertEqual(schedule[0]["start"], u"2014-11-20 00:00") self.assertEqual(schedule[0]["children"][0]["due"], u"2014-12-25 00:00") self.assertEqual(schedule[0]["children"][0]["children"][0]["due"], u"2014-12-25 00:00") self.assertEqual(schedule[0]["children"][0]["children"][0]["start"], u"2014-12-20 00:00") # Make sure start date set on course, follows start date of earliest # scheduled chapter ccx = CustomCourseForEdX.objects.get() course_start = get_override_for_ccx(ccx, self.course, "start") self.assertEqual(str(course_start)[:-9], self.chapters[0].start.strftime("%Y-%m-%d %H:%M")) # Make sure grading policy adjusted policy = get_override_for_ccx(ccx, self.course, "grading_policy", self.course.grading_policy) self.assertEqual(policy["GRADER"][0]["type"], "Homework") self.assertEqual(policy["GRADER"][0]["min_count"], 8) self.assertEqual(policy["GRADER"][1]["type"], "Lab") self.assertEqual(policy["GRADER"][1]["min_count"], 0) self.assertEqual(policy["GRADER"][2]["type"], "Midterm Exam") self.assertEqual(policy["GRADER"][2]["min_count"], 0) self.assertEqual(policy["GRADER"][3]["type"], "Final Exam") self.assertEqual(policy["GRADER"][3]["min_count"], 0)
def save_ccx(request, course, ccx=None): # lint-amnesty, pylint: disable=too-many-statements """ Save changes to CCX. """ if not ccx: raise Http404 def override_fields(parent, data, graded, earliest=None, ccx_ids_to_delete=None): """ Recursively apply CCX schedule data to CCX by overriding the `visible_to_staff_only`, `start` and `due` fields for units in the course. """ if ccx_ids_to_delete is None: ccx_ids_to_delete = [] blocks = { str(child.location): child for child in parent.get_children() } for unit in data: block = blocks[unit['location']] override_field_for_ccx(ccx, block, 'visible_to_staff_only', unit['hidden']) start = parse_date(unit['start']) if start: if not earliest or start < earliest: earliest = start override_field_for_ccx(ccx, block, 'start', start) else: ccx_ids_to_delete.append( get_override_for_ccx(ccx, block, 'start_id')) clear_ccx_field_info_from_ccx_map(ccx, block, 'start') # Only subsection (aka sequential) and unit (aka vertical) have due dates. if 'due' in unit: # checking that the key (due) exist in dict (unit). due = parse_date(unit['due']) if due: override_field_for_ccx(ccx, block, 'due', due) else: ccx_ids_to_delete.append( get_override_for_ccx(ccx, block, 'due_id')) clear_ccx_field_info_from_ccx_map(ccx, block, 'due') else: # In case of section aka chapter we do not have due date. ccx_ids_to_delete.append( get_override_for_ccx(ccx, block, 'due_id')) clear_ccx_field_info_from_ccx_map(ccx, block, 'due') if not unit['hidden'] and block.graded: graded[block.format] = graded.get(block.format, 0) + 1 children = unit.get('children', None) # For a vertical, override start and due dates of all its problems. if unit.get('category', None) == 'vertical': for component in block.get_children(): # override start and due date of problem (Copy dates of vertical into problems) if start: override_field_for_ccx(ccx, component, 'start', start) if due: override_field_for_ccx(ccx, component, 'due', due) if children: override_fields(block, children, graded, earliest, ccx_ids_to_delete) return earliest, ccx_ids_to_delete graded = {} earliest, ccx_ids_to_delete = override_fields( course, json.loads(request.body.decode('utf8')), graded, []) bulk_delete_ccx_override_fields(ccx, ccx_ids_to_delete) if earliest: override_field_for_ccx(ccx, course, 'start', earliest) # Attempt to automatically adjust grading policy changed = False policy = get_override_for_ccx(ccx, course, 'grading_policy', course.grading_policy) policy = deepcopy(policy) grader = policy['GRADER'] for section in grader: count = graded.get(section.get('type'), 0) if count < section.get('min_count', 0): changed = True section['min_count'] = count if changed: override_field_for_ccx(ccx, course, 'grading_policy', policy) # using CCX object as sender here. responses = SignalHandler.course_published.send( sender=ccx, course_key=CCXLocator.from_course_locator(course.id, str(ccx.id))) for rec, response in responses: log.info( 'Signal fired when course is published. Receiver: %s. Response: %s', rec, response) return HttpResponse( # lint-amnesty, pylint: disable=http-response-with-content-type-json, http-response-with-json-dumps json.dumps({ 'schedule': get_ccx_schedule(course, ccx), 'grading_policy': json.dumps(policy, indent=4) }), content_type='application/json', )
def save_ccx(request, course, ccx=None): """ Save changes to CCX. """ if not ccx: raise Http404 def override_fields(parent, data, graded, earliest=None, ccx_ids_to_delete=None): """ Recursively apply CCX schedule data to CCX by overriding the `visible_to_staff_only`, `start` and `due` fields for units in the course. """ if ccx_ids_to_delete is None: ccx_ids_to_delete = [] blocks = { str(child.location): child for child in parent.get_children()} for unit in data: block = blocks[unit['location']] override_field_for_ccx( ccx, block, 'visible_to_staff_only', unit['hidden']) start = parse_date(unit['start']) if start: if not earliest or start < earliest: earliest = start override_field_for_ccx(ccx, block, 'start', start) else: ccx_ids_to_delete.append(get_override_for_ccx(ccx, block, 'start_id')) clear_ccx_field_info_from_ccx_map(ccx, block, 'start') due = parse_date(unit['due']) if due: override_field_for_ccx(ccx, block, 'due', due) else: ccx_ids_to_delete.append(get_override_for_ccx(ccx, block, 'due_id')) clear_ccx_field_info_from_ccx_map(ccx, block, 'due') if not unit['hidden'] and block.graded: graded[block.format] = graded.get(block.format, 0) + 1 children = unit.get('children', None) # For a vertical, override start and due dates of all its problems. if unit.get('category', None) == u'vertical': for component in block.get_children(): # override start and due date of problem (Copy dates of vertical into problems) if start: override_field_for_ccx(ccx, component, 'start', start) if due: override_field_for_ccx(ccx, component, 'due', due) if children: override_fields(block, children, graded, earliest, ccx_ids_to_delete) return earliest, ccx_ids_to_delete graded = {} earliest, ccx_ids_to_delete = override_fields(course, json.loads(request.body), graded, []) bulk_delete_ccx_override_fields(ccx, ccx_ids_to_delete) if earliest: override_field_for_ccx(ccx, course, 'start', earliest) # Attempt to automatically adjust grading policy changed = False policy = get_override_for_ccx( ccx, course, 'grading_policy', course.grading_policy ) policy = deepcopy(policy) grader = policy['GRADER'] for section in grader: count = graded.get(section.get('type'), 0) if count < section.get('min_count', 0): changed = True section['min_count'] = count if changed: override_field_for_ccx(ccx, course, 'grading_policy', policy) return HttpResponse( json.dumps({ 'schedule': get_ccx_schedule(course, ccx), 'grading_policy': json.dumps(policy, indent=4)}), content_type='application/json', )