def make_ccx(self, max_students_allowed=200): """ Overridden method to replicate (part of) the actual creation of ccx courses """ ccx = super().make_ccx(max_students_allowed=max_students_allowed) ccx.structure_json = json.dumps(self.master_course_chapters) ccx.save() override_field_for_ccx(ccx, self.course, 'start', now()) override_field_for_ccx(ccx, self.course, 'due', None) # Hide anything that can show up in the schedule hidden = 'visible_to_staff_only' for chapter in self.course.get_children(): override_field_for_ccx(ccx, chapter, hidden, True) for sequential in chapter.get_children(): override_field_for_ccx(ccx, sequential, hidden, True) for vertical in sequential.get_children(): override_field_for_ccx(ccx, vertical, hidden, True) # enroll the coach in the CCX ccx_course_key = CCXLocator.from_course_locator(self.course.id, ccx.id) email_params = get_email_params( self.course, auto_enroll=True, course_key=ccx_course_key, display_name=ccx.display_name ) enroll_email( course_id=ccx_course_key, student_email=self.coach.email, auto_enroll=True, email_students=False, email_params=email_params, ) return ccx
def set_grading_policy(request, course, ccx=None): """ Set grading policy for the CCX. """ if not ccx: raise Http404 override_field_for_ccx(ccx, course, 'grading_policy', json.loads(request.POST['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) url = reverse('ccx_coach_dashboard', kwargs={ 'course_id': CCXLocator.from_course_locator(course.id, str(ccx.id)) }) return redirect(url)
def set_grading_policy(request, course, ccx=None): """ Set grading policy for the CCX. """ if not ccx: raise Http404 override_field_for_ccx(ccx, course, 'grading_policy', json.loads(request.POST['policy'])) # using CCX object as sender here. responses = SignalHandler.course_published.send( sender=ccx, course_key=CCXLocator.from_course_locator(course.id, unicode(ccx.id))) for rec, response in responses: log.info( 'Signal fired when course is published. Receiver: %s. Response: %s', rec, response) url = reverse( 'ccx_coach_dashboard', kwargs={ 'course_id': CCXLocator.from_course_locator( course.id, unicode(ccx.id)) }) return redirect(url)
def setUp(self): """ Set up courses and enrollments. """ super(TestStudentDashboardWithCCX, self).setUp() # Create a Draft Mongo and a Split Mongo course and enroll a student user in them. self.student_password = "******" self.student = UserFactory.create(username="******", password=self.student_password, is_staff=False) self.draft_course = CourseFactory.create(default_store=ModuleStoreEnum.Type.mongo) self.split_course = CourseFactory.create(default_store=ModuleStoreEnum.Type.split) CourseEnrollment.enroll(self.student, self.draft_course.id) CourseEnrollment.enroll(self.student, self.split_course.id) # Create a CCX coach. self.coach = AdminFactory.create() role = CourseCcxCoachRole(self.split_course.id) role.add_users(self.coach) # Create a CCX course and enroll the user in it. self.ccx = CcxFactory(course_id=self.split_course.id, coach=self.coach) last_week = datetime.datetime.now(UTC()) - datetime.timedelta(days=7) override_field_for_ccx(self.ccx, self.split_course, 'start', last_week) # Required by self.ccx.has_started(). course_key = CCXLocator.from_course_locator(self.split_course.id, self.ccx.id) CourseEnrollment.enroll(self.student, course_key)
def setUp(self): """ Set up courses and enrollments. """ super(TestStudentViewsWithCCX, self).setUp() # Create a Draft Mongo and a Split Mongo course and enroll a student user in them. self.student_password = "******" self.student = UserFactory.create(username="******", password=self.student_password, is_staff=False) self.draft_course = SampleCourseFactory.create(default_store=ModuleStoreEnum.Type.mongo) self.split_course = SampleCourseFactory.create(default_store=ModuleStoreEnum.Type.split) CourseEnrollment.enroll(self.student, self.draft_course.id) CourseEnrollment.enroll(self.student, self.split_course.id) # Create a CCX coach. self.coach = AdminFactory.create() role = CourseCcxCoachRole(self.split_course.id) role.add_users(self.coach) # Create a CCX course and enroll the user in it. self.ccx = CcxFactory(course_id=self.split_course.id, coach=self.coach) last_week = datetime.datetime.now(UTC()) - datetime.timedelta(days=7) override_field_for_ccx(self.ccx, self.split_course, 'start', last_week) # Required by self.ccx.has_started(). self.ccx_course_key = CCXLocator.from_course_locator(self.split_course.id, self.ccx.id) CourseEnrollment.enroll(self.student, self.ccx_course_key)
def make_ccx(self, max_students_allowed=settings.CCX_MAX_STUDENTS_ALLOWED): """ create ccx """ ccx = CcxFactory(course_id=self.course.id, coach=self.coach) override_field_for_ccx(ccx, self.course, 'max_student_enrollments_allowed', max_students_allowed) return ccx
def make_ccx(self, max_students_allowed=settings.CCX_MAX_STUDENTS_ALLOWED): """ create ccx """ ccx = CcxFactory(course_id=self.course.id, coach=self.coach) override_field_for_ccx(ccx, self.course, 'max_student_enrollments_allowed', max_students_allowed) return ccx
def make_ccx(self, max_students_allowed=200): """ Overridden method to replicate (part of) the actual creation of ccx courses """ ccx = super(CcxDetailTest, self).make_ccx(max_students_allowed=max_students_allowed) today = datetime.datetime.today() start = today.replace(tzinfo=pytz.UTC) override_field_for_ccx(ccx, self.course, 'start', start) override_field_for_ccx(ccx, self.course, 'due', None) # Hide anything that can show up in the schedule hidden = 'visible_to_staff_only' for chapter in self.course.get_children(): override_field_for_ccx(ccx, chapter, hidden, True) for sequential in chapter.get_children(): override_field_for_ccx(ccx, sequential, hidden, True) for vertical in sequential.get_children(): override_field_for_ccx(ccx, vertical, hidden, True) # enroll the coach in the CCX ccx_course_key = CCXLocator.from_course_locator(self.course.id, ccx.id) email_params = get_email_params( self.course, auto_enroll=True, course_key=ccx_course_key, display_name=ccx.display_name ) enroll_email( course_id=ccx_course_key, student_email=self.coach.email, auto_enroll=True, email_students=False, email_params=email_params, ) return ccx
def setUp(self): """ Set up tests """ super(TestCCXGrades, self).setUp() # Create instructor account self.coach = coach = AdminFactory.create() self.client.login(username=coach.username, password="******") # Create CCX role = CourseCcxCoachRole(self._course.id) role.add_users(coach) ccx = CcxFactory(course_id=self._course.id, coach=self.coach) # override course grading policy and make last section invisible to students override_field_for_ccx( ccx, self._course, "grading_policy", { "GRADER": [{"drop_count": 0, "min_count": 2, "short_label": "HW", "type": "Homework", "weight": 1}], "GRADE_CUTOFFS": {"Pass": 0.75}, }, ) override_field_for_ccx(ccx, self.sections[-1], "visible_to_staff_only", True) # create a ccx locator and retrieve the course structure using that key # which emulates how a student would get access. self.ccx_key = CCXLocator.from_course_locator(self._course.id, ccx.id) self.course = get_course_by_id(self.ccx_key, depth=None) setup_students_and_grades(self) self.client.login(username=coach.username, password="******") self.addCleanup(RequestCache.clear_request_cache)
def make_ccx(self, max_students_allowed=200): """ Overridden method to replicate (part of) the actual creation of ccx courses """ ccx = super(CcxDetailTest, self).make_ccx(max_students_allowed=max_students_allowed) today = datetime.datetime.today() start = today.replace(tzinfo=pytz.UTC) override_field_for_ccx(ccx, self.course, 'start', start) override_field_for_ccx(ccx, self.course, 'due', None) # Hide anything that can show up in the schedule hidden = 'visible_to_staff_only' for chapter in self.course.get_children(): override_field_for_ccx(ccx, chapter, hidden, True) for sequential in chapter.get_children(): override_field_for_ccx(ccx, sequential, hidden, True) for vertical in sequential.get_children(): override_field_for_ccx(ccx, vertical, hidden, True) # enroll the coach in the CCX ccx_course_key = CCXLocator.from_course_locator(self.course.id, ccx.id) email_params = get_email_params(self.course, auto_enroll=True, course_key=ccx_course_key, display_name=ccx.display_name) enroll_email( course_id=ccx_course_key, student_email=self.coach.email, auto_enroll=True, email_students=False, email_params=email_params, ) return ccx
def test_override_start(self): """ Test that overriding start date on a chapter works. """ ccx_start = datetime.datetime(2014, 12, 25, 00, 00, tzinfo=pytz.UTC) chapter = self.ccx_course.get_children()[0] override_field_for_ccx(self.ccx, chapter, 'start', ccx_start) self.assertEquals(chapter.start, ccx_start)
def test_override_start(self): """ Test that overriding start date on a chapter works. """ ccx_start = datetime.datetime(2014, 12, 25, 00, 00, tzinfo=pytz.UTC) chapter = self.ccx_course.get_children()[0] override_field_for_ccx(self.ccx, chapter, 'start', ccx_start) self.assertEquals(chapter.start, ccx_start)
def test_override_num_queries_field_value_not_changed(self): """ Test that if value of field does not changed no query execute. """ ccx_start = datetime.datetime(2014, 12, 25, 00, 00, tzinfo=pytz.UTC) chapter = self.ccx_course.get_children()[0] override_field_for_ccx(self.ccx, chapter, 'start', ccx_start) with self.assertNumQueries(2): # 2 savepoints override_field_for_ccx(self.ccx, chapter, 'start', ccx_start)
def test_override_num_queries_field_value_not_changed(self): """ Test that if value of field does not changed no query execute. """ ccx_start = datetime.datetime(2014, 12, 25, 00, 00, tzinfo=pytz.UTC) chapter = self.ccx_course.get_children()[0] override_field_for_ccx(self.ccx, chapter, 'start', ccx_start) with self.assertNumQueries(2): # 2 savepoints override_field_for_ccx(self.ccx, chapter, 'start', ccx_start)
def test_override_is_inherited(self): """ Test that sequentials inherit overridden start date from chapter. """ ccx_start = datetime.datetime(2014, 12, 25, 00, 00, tzinfo=pytz.UTC) chapter = self.ccx_course.get_children()[0] override_field_for_ccx(self.ccx, chapter, 'start', ccx_start) self.assertEquals(chapter.get_children()[0].start, ccx_start) self.assertEquals(chapter.get_children()[1].start, ccx_start)
def test_override_is_inherited(self): """ Test that sequentials inherit overridden start date from chapter. """ ccx_start = datetime.datetime(2014, 12, 25, 00, 00, tzinfo=pytz.UTC) chapter = self.ccx_course.get_children()[0] override_field_for_ccx(self.ccx, chapter, 'start', ccx_start) self.assertEquals(chapter.get_children()[0].start, ccx_start) self.assertEquals(chapter.get_children()[1].start, ccx_start)
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 test_override_num_queries_update_existing_field(self): """ Test that overriding existing field executed create, fetch and update queries. """ ccx_start = datetime.datetime(2014, 12, 25, 00, 00, tzinfo=pytz.UTC) new_ccx_start = datetime.datetime(2015, 12, 25, 00, 00, tzinfo=pytz.UTC) chapter = self.ccx_course.get_children()[0] override_field_for_ccx(self.ccx, chapter, 'start', ccx_start) with self.assertNumQueries(3): override_field_for_ccx(self.ccx, chapter, 'start', new_ccx_start)
def test_override_num_queries_update_existing_field(self): """ Test that overriding existing field executed create, fetch and update queries. """ ccx_start = datetime.datetime(2014, 12, 25, 00, 00, tzinfo=pytz.UTC) new_ccx_start = datetime.datetime(2015, 12, 25, 00, 00, tzinfo=pytz.UTC) chapter = self.ccx_course.get_children()[0] override_field_for_ccx(self.ccx, chapter, 'start', ccx_start) with self.assertNumQueries(3): override_field_for_ccx(self.ccx, chapter, 'start', new_ccx_start)
def setUp(self): """ Set up tests """ super(TestCCXGrades, self).setUp() # Create instructor account self.coach = coach = AdminFactory.create() self.client.login(username=coach.username, password="******") # Create CCX role = CourseCcxCoachRole(self._course.id) role.add_users(coach) ccx = CcxFactory(course_id=self._course.id, coach=self.coach) # override course grading policy and make last section invisible to students override_field_for_ccx(ccx, self._course, 'grading_policy', { 'GRADER': [ {'drop_count': 0, 'min_count': 2, 'short_label': 'HW', 'type': 'Homework', 'weight': 1} ], 'GRADE_CUTOFFS': {'Pass': 0.75}, }) override_field_for_ccx( ccx, self.sections[-1], 'visible_to_staff_only', True ) # create a ccx locator and retrieve the course structure using that key # which emulates how a student would get access. self.ccx_key = CCXLocator.from_course_locator(self._course.id, ccx.id) self.course = get_course_by_id(self.ccx_key, depth=None) self.student = student = UserFactory.create() CourseEnrollmentFactory.create(user=student, course_id=self.course.id) # create grades for self.student as if they'd submitted the ccx for chapter in self.course.get_children(): for i, section in enumerate(chapter.get_children()): for j, problem in enumerate(section.get_children()): # if not problem.visible_to_staff_only: StudentModuleFactory.create( grade=1 if i < j else 0, max_grade=1, student=self.student, course_id=self.course.id, module_state_key=problem.location ) self.client.login(username=coach.username, password="******") self.addCleanup(RequestCache.clear_request_cache)
def set_grading_policy(request, course, ccx=None): """ Set grading policy for the CCX. """ if not ccx: raise Http404 override_field_for_ccx(ccx, course, "grading_policy", json.loads(request.POST["policy"])) url = reverse("ccx_coach_dashboard", kwargs={"course_id": CCXLocator.from_course_locator(course.id, ccx.id)}) return redirect(url)
def test_override_is_inherited_even_if_set_in_mooc(self): """ Test that a due date set on a chapter is inherited by grandchildren (verticals) even if a due date is set explicitly on grandchildren in the mooc. """ ccx_due = datetime.datetime(2015, 1, 1, 00, 00, tzinfo=pytz.UTC) chapter = self.ccx_course.get_children()[0] chapter.display_name = 'itsme!' override_field_for_ccx(self.ccx, chapter, 'due', ccx_due) vertical = chapter.get_children()[0].get_children()[0] self.assertEqual(vertical.due, ccx_due)
def test_override_is_inherited_even_if_set_in_mooc(self): """ Test that a due date set on a chapter is inherited by grandchildren (verticals) even if a due date is set explicitly on grandchildren in the mooc. """ ccx_due = datetime.datetime(2015, 1, 1, 00, 00, tzinfo=pytz.UTC) chapter = self.ccx_course.get_children()[0] chapter.display_name = 'itsme!' override_field_for_ccx(self.ccx, chapter, 'due', ccx_due) vertical = chapter.get_children()[0].get_children()[0] self.assertEqual(vertical.due, ccx_due)
def test_override_num_queries_new_field(self): """ Test that for creating new field executed only create query """ ccx_start = datetime.datetime(2014, 12, 25, 00, 00, tzinfo=pytz.UTC) chapter = self.ccx_course.get_children()[0] # One outer SAVEPOINT/RELEASE SAVEPOINT pair around everything caused by the # transaction.atomic decorator wrapping override_field_for_ccx. # One SELECT and one INSERT. # One inner SAVEPOINT/RELEASE SAVEPOINT pair around the INSERT caused by the # transaction.atomic down in Django's get_or_create()/_create_object_from_params(). with self.assertNumQueries(6): override_field_for_ccx(self.ccx, chapter, 'start', ccx_start)
def test_overriden_field_access_produces_no_extra_queries(self): """ Test no extra queries when accessing an overriden field more than once. """ ccx_start = datetime.datetime(2014, 12, 25, 00, 00, tzinfo=pytz.UTC) chapter = self.ccx_course.get_children()[0] # One outer SAVEPOINT/RELEASE SAVEPOINT pair around everything caused by the # transaction.atomic decorator wrapping override_field_for_ccx. # One SELECT and one INSERT. # One inner SAVEPOINT/RELEASE SAVEPOINT pair around the INSERT caused by the # transaction.atomic down in Django's get_or_create()/_create_object_from_params(). with self.assertNumQueries(6): override_field_for_ccx(self.ccx, chapter, 'start', ccx_start)
def test_override_num_queries_new_field(self): """ Test that for creating new field executed only create query """ ccx_start = datetime.datetime(2014, 12, 25, 00, 00, tzinfo=pytz.UTC) chapter = self.ccx_course.get_children()[0] # One outer SAVEPOINT/RELEASE SAVEPOINT pair around everything caused by the # transaction.atomic decorator wrapping override_field_for_ccx. # One SELECT and one INSERT. # One inner SAVEPOINT/RELEASE SAVEPOINT pair around the INSERT caused by the # transaction.atomic down in Django's get_or_create()/_create_object_from_params(). with self.assertNumQueries(6): override_field_for_ccx(self.ccx, chapter, 'start', ccx_start)
def test_overriden_field_access_produces_no_extra_queries(self): """ Test no extra queries when accessing an overriden field more than once. """ ccx_start = datetime.datetime(2014, 12, 25, 00, 00, tzinfo=pytz.UTC) chapter = self.ccx_course.get_children()[0] # One outer SAVEPOINT/RELEASE SAVEPOINT pair around everything caused by the # transaction.atomic decorator wrapping override_field_for_ccx. # One SELECT and one INSERT. # One inner SAVEPOINT/RELEASE SAVEPOINT pair around the INSERT caused by the # transaction.atomic down in Django's get_or_create()/_create_object_from_params(). with self.assertNumQueries(6): override_field_for_ccx(self.ccx, chapter, 'start', ccx_start)
def create_ccx(request, course, ccx=None): """ Create a new CCX """ name = request.POST.get('name') # prevent CCX objects from being created for deprecated course ids. if course.id.deprecated: messages.error(request, _( "You cannot create a CCX from a course using a deprecated id. " "Please create a rerun of this course in the studio to allow " "this action.")) url = reverse('ccx_coach_dashboard', kwargs={'course_id': course.id}) return redirect(url) ccx = CustomCourseForEdX( course_id=course.id, coach=request.user, display_name=name) ccx.save() # Make sure start/due are overridden for entire course start = TODAY().replace(tzinfo=pytz.UTC) override_field_for_ccx(ccx, course, 'start', start) override_field_for_ccx(ccx, course, 'due', None) # Hide anything that can show up in the schedule hidden = 'visible_to_staff_only' for chapter in course.get_children(): override_field_for_ccx(ccx, chapter, hidden, True) for sequential in chapter.get_children(): override_field_for_ccx(ccx, sequential, hidden, True) for vertical in sequential.get_children(): override_field_for_ccx(ccx, vertical, hidden, True) ccx_id = CCXLocator.from_course_locator(course.id, ccx.id) # pylint: disable=no-member url = reverse('ccx_coach_dashboard', kwargs={'course_id': ccx_id}) # Enroll the coach in the course email_params = get_email_params(course, auto_enroll=True, course_key=ccx_id, display_name=ccx.display_name) enroll_email( course_id=ccx_id, student_email=request.user.email, auto_enroll=True, email_students=True, email_params=email_params, ) return redirect(url)
def set_grading_policy(request, course, ccx=None): """ Set grading policy for the CCX. """ if not ccx: raise Http404 override_field_for_ccx( ccx, course, 'grading_policy', json.loads(request.POST['policy'])) url = reverse( 'ccx_coach_dashboard', kwargs={'course_id': CCXLocator.from_course_locator(course.id, ccx.id)} ) return redirect(url)
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 setUp(self): """ Set up tests """ super().setUp() # Create instructor account self.coach = coach = AdminFactory.create() self.client.login(username=coach.username, password="******") # Create CCX role = CourseCcxCoachRole(self._course.id) role.add_users(coach) ccx = CcxFactory(course_id=self._course.id, coach=self.coach) # override course grading policy and make last section invisible to students override_field_for_ccx(ccx, self._course, 'grading_policy', { 'GRADER': [ {'drop_count': 0, 'min_count': 2, 'short_label': 'HW', 'type': 'Homework', 'weight': 1} ], 'GRADE_CUTOFFS': {'Pass': 0.75}, }) override_field_for_ccx( ccx, self.sections[-1], 'visible_to_staff_only', True ) # create a ccx locator and retrieve the course structure using that key # which emulates how a student would get access. self.ccx_key = CCXLocator.from_course_locator(self._course.id, str(ccx.id)) self.course = get_course_by_id(self.ccx_key, depth=None) CourseOverview.load_from_module_store(self.course.id) setup_students_and_grades(self) self.client.login(username=coach.username, password="******") self.addCleanup(RequestCache.clear_all_namespaces) from xmodule.modulestore.django import SignalHandler # using CCX object as sender here. SignalHandler.course_published.send( sender=ccx, course_key=self.ccx_key )
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 patch(self, request, ccx_course_id=None): """ Modifies a CCX course. Args: request (Request): Django request object. ccx_course_id (string): URI element specifying the CCX course location. """ ccx_course_object, ccx_course_key, error_code, http_status = self.get_object( ccx_course_id, is_ccx=True) if ccx_course_object is None: return Response(status=http_status, data={'error_code': error_code}) master_course_id = request.data.get('master_course_id') if master_course_id is not None and str( ccx_course_object.course_id) != master_course_id: return Response( status=status.HTTP_403_FORBIDDEN, data={'error_code': 'master_course_id_change_not_allowed'}) valid_input, field_errors = get_valid_input(request.data, ignore_missing=True) if field_errors: return Response(status=status.HTTP_400_BAD_REQUEST, data={'field_errors': field_errors}) # get the master course key and master course object master_course_object, master_course_key, _, _ = get_valid_course( str(ccx_course_object.course_id)) with transaction.atomic(): # update the display name if 'display_name' in valid_input: ccx_course_object.display_name = valid_input['display_name'] # check if the coach has changed and in case update it old_coach = None if 'coach_email' in valid_input: try: coach = User.objects.get(email=valid_input['coach_email']) except User.DoesNotExist: return Response( status=status.HTTP_404_NOT_FOUND, data={'error_code': 'coach_user_does_not_exist'}) if ccx_course_object.coach.id != coach.id: old_coach = ccx_course_object.coach ccx_course_object.coach = coach if 'course_modules' in valid_input: if valid_input.get('course_modules'): if not valid_course_modules(valid_input['course_modules'], master_course_key): return Response( status=status.HTTP_400_BAD_REQUEST, data={ 'error_code': 'course_module_list_not_belonging_to_master_course' }) # course_modules to be stored in a json stringified field ccx_course_object.structure_json = json.dumps( valid_input.get('course_modules')) ccx_course_object.save() # update the overridden field for the maximum amount of students if 'max_students_allowed' in valid_input: override_field_for_ccx(ccx_course_object, ccx_course_object.course, 'max_student_enrollments_allowed', valid_input['max_students_allowed']) # if the coach has changed, update the permissions if old_coach is not None: # make the new ccx coach a coach on the master course make_user_coach(coach, master_course_key) # enroll the coach in the ccx email_params = get_email_params( master_course_object, auto_enroll=True, course_key=ccx_course_key, display_name=ccx_course_object.display_name) enroll_email( course_id=ccx_course_key, student_email=coach.email, auto_enroll=True, email_students=True, email_params=email_params, ) # make the new coach staff on the CCX assign_staff_role_to_ccx(ccx_course_key, coach, master_course_object.id) # using CCX object as sender here. responses = SignalHandler.course_published.send( sender=ccx_course_object, course_key=ccx_course_key) for rec, response in responses: log.info( 'Signal fired when course is published. Receiver: %s. Response: %s', rec, response) return Response(status=status.HTTP_204_NO_CONTENT, )
def post(self, request): """ Creates a new CCX course for a given Master Course. Args: request (Request): Django request object. Return: A JSON serialized representation a newly created CCX course. """ master_course_id = request.data.get('master_course_id') master_course_object, master_course_key, error_code, http_status = get_valid_course( master_course_id, advanced_course_check=True) if master_course_object is None: return Response(status=http_status, data={'error_code': error_code}) # validating the rest of the input valid_input, field_errors = get_valid_input(request.data) if field_errors: return Response(status=status.HTTP_400_BAD_REQUEST, data={'field_errors': field_errors}) try: # Retired users should effectively appear to not exist when # attempts are made to modify them, so a direct User model email # lookup is sufficient here. This corner case relies on the fact # that we scramble emails immediately during user lock-out. Of # course, the normal cases are that the email just never existed, # or it is currently associated with an active account. coach = User.objects.get(email=valid_input['coach_email']) except User.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND, data={'error_code': 'coach_user_does_not_exist'}) if valid_input.get('course_modules'): if not valid_course_modules(valid_input['course_modules'], master_course_key): return Response( status=status.HTTP_400_BAD_REQUEST, data={ 'error_code': 'course_module_list_not_belonging_to_master_course' }) # prepare the course_modules to be stored in a json stringified field course_modules_json = json.dumps(valid_input.get('course_modules')) with transaction.atomic(): ccx_course_object = CustomCourseForEdX( course_id=master_course_object.id, coach=coach, display_name=valid_input['display_name'], structure_json=course_modules_json) ccx_course_object.save() # Make sure start/due are overridden for entire course start = TODAY().replace(tzinfo=pytz.UTC) override_field_for_ccx(ccx_course_object, master_course_object, 'start', start) override_field_for_ccx(ccx_course_object, master_course_object, 'due', None) # Enforce a static limit for the maximum amount of students that can be enrolled override_field_for_ccx(ccx_course_object, master_course_object, 'max_student_enrollments_allowed', valid_input['max_students_allowed']) # Hide anything that can show up in the schedule hidden = 'visible_to_staff_only' for chapter in master_course_object.get_children(): override_field_for_ccx(ccx_course_object, chapter, hidden, True) for sequential in chapter.get_children(): override_field_for_ccx(ccx_course_object, sequential, hidden, True) for vertical in sequential.get_children(): override_field_for_ccx(ccx_course_object, vertical, hidden, True) # make the coach user a coach on the master course make_user_coach(coach, master_course_key) # pull the ccx course key ccx_course_key = CCXLocator.from_course_locator( master_course_object.id, str(ccx_course_object.id)) # enroll the coach in the newly created ccx email_params = get_email_params( master_course_object, auto_enroll=True, course_key=ccx_course_key, display_name=ccx_course_object.display_name) enroll_email( course_id=ccx_course_key, student_email=coach.email, auto_enroll=True, email_students=True, email_params=email_params, ) # assign staff role for the coach to the newly created ccx assign_staff_role_to_ccx(ccx_course_key, coach, master_course_object.id) # assign staff role for all the staff and instructor of the master course to the newly created ccx add_master_course_staff_to_ccx(master_course_object, ccx_course_key, ccx_course_object.display_name, send_email=False) serializer = self.get_serializer(ccx_course_object) # using CCX object as sender here. responses = SignalHandler.course_published.send( sender=ccx_course_object, course_key=ccx_course_key) for rec, response in responses: log.info( 'Signal fired when course is published. Receiver: %s. Response: %s', rec, response) return Response(status=status.HTTP_201_CREATED, data=serializer.data)
def patch(self, request, ccx_course_id=None): """ Modifies a CCX course. Args: request (Request): Django request object. ccx_course_id (string): URI element specifying the CCX course location. """ ccx_course_object, ccx_course_key, error_code, http_status = self.get_object(ccx_course_id, is_ccx=True) if ccx_course_object is None: return Response( status=http_status, data={ 'error_code': error_code } ) master_course_id = request.data.get('master_course_id') if master_course_id is not None and unicode(ccx_course_object.course_id) != master_course_id: return Response( status=status.HTTP_403_FORBIDDEN, data={ 'error_code': 'master_course_id_change_not_allowed' } ) valid_input, field_errors = get_valid_input(request.data, ignore_missing=True) if field_errors: return Response( status=status.HTTP_400_BAD_REQUEST, data={ 'field_errors': field_errors } ) with transaction.atomic(): # update the display name if 'display_name' in valid_input: ccx_course_object.display_name = valid_input['display_name'] # check if the coach has changed and in case update it old_coach = None if 'coach_email' in valid_input: try: coach = User.objects.get(email=valid_input['coach_email']) except User.DoesNotExist: return Response( status=status.HTTP_404_NOT_FOUND, data={ 'error_code': 'coach_user_does_not_exist' } ) if ccx_course_object.coach.id != coach.id: old_coach = ccx_course_object.coach ccx_course_object.coach = coach ccx_course_object.save() # update the overridden field for the maximum amount of students if 'max_students_allowed' in valid_input: override_field_for_ccx( ccx_course_object, ccx_course_object.course, 'max_student_enrollments_allowed', valid_input['max_students_allowed'] ) # if the coach has changed, update the permissions if old_coach is not None: # get the master course key and master course object master_course_object, master_course_key, _, _ = get_valid_course(unicode(ccx_course_object.course_id)) # make the new ccx coach a coach on the master course make_user_coach(coach, master_course_key) # enroll the coach in the ccx email_params = get_email_params( master_course_object, auto_enroll=True, course_key=ccx_course_key, display_name=ccx_course_object.display_name ) enroll_email( course_id=ccx_course_key, student_email=coach.email, auto_enroll=True, email_students=True, email_params=email_params, ) # enroll the coach to the newly created ccx assign_coach_role_to_ccx(ccx_course_key, coach, master_course_object.id) return Response( status=status.HTTP_204_NO_CONTENT, )
def create_ccx(request, course, ccx=None): """ Create a new CCX """ name = request.POST.get('name') # prevent CCX objects from being created for deprecated course ids. if course.id.deprecated: messages.error(request, _( "You cannot create a CCX from a course using a deprecated id. " "Please create a rerun of this course in the studio to allow " "this action.")) url = reverse('ccx_coach_dashboard', kwargs={'course_id': course.id}) return redirect(url) ccx = CustomCourseForEdX( course_id=course.id, coach=request.user, display_name=name) ccx.save() # Make sure start/due are overridden for entire course start = TODAY().replace(tzinfo=pytz.UTC) override_field_for_ccx(ccx, course, 'start', start) override_field_for_ccx(ccx, course, 'due', None) # Enforce a static limit for the maximum amount of students that can be enrolled override_field_for_ccx(ccx, course, 'max_student_enrollments_allowed', settings.CCX_MAX_STUDENTS_ALLOWED) # Hide anything that can show up in the schedule hidden = 'visible_to_staff_only' for chapter in course.get_children(): override_field_for_ccx(ccx, chapter, hidden, True) for sequential in chapter.get_children(): override_field_for_ccx(ccx, sequential, hidden, True) for vertical in sequential.get_children(): override_field_for_ccx(ccx, vertical, hidden, True) ccx_id = CCXLocator.from_course_locator(course.id, ccx.id) url = reverse('ccx_coach_dashboard', kwargs={'course_id': ccx_id}) # Enroll the coach in the course email_params = get_email_params(course, auto_enroll=True, course_key=ccx_id, display_name=ccx.display_name) enroll_email( course_id=ccx_id, student_email=request.user.email, auto_enroll=True, email_students=True, email_params=email_params, ) return redirect(url)
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 create_ccx(request, course, ccx=None): """ Create a new CCX """ name = request.POST.get("name") # prevent CCX objects from being created for deprecated course ids. if course.id.deprecated: messages.error( request, _( "You cannot create a CCX from a course using a deprecated id. " "Please create a rerun of this course in the studio to allow " "this action." ), ) url = reverse("ccx_coach_dashboard", kwargs={"course_id": course.id}) return redirect(url) ccx = CustomCourseForEdX(course_id=course.id, coach=request.user, display_name=name) ccx.save() # Make sure start/due are overridden for entire course start = TODAY().replace(tzinfo=pytz.UTC) override_field_for_ccx(ccx, course, "start", start) override_field_for_ccx(ccx, course, "due", None) # Enforce a static limit for the maximum amount of students that can be enrolled override_field_for_ccx(ccx, course, "max_student_enrollments_allowed", settings.CCX_MAX_STUDENTS_ALLOWED) # Hide anything that can show up in the schedule hidden = "visible_to_staff_only" for chapter in course.get_children(): override_field_for_ccx(ccx, chapter, hidden, True) for sequential in chapter.get_children(): override_field_for_ccx(ccx, sequential, hidden, True) for vertical in sequential.get_children(): override_field_for_ccx(ccx, vertical, hidden, True) ccx_id = CCXLocator.from_course_locator(course.id, ccx.id) url = reverse("ccx_coach_dashboard", kwargs={"course_id": ccx_id}) # Enroll the coach in the course email_params = get_email_params(course, auto_enroll=True, course_key=ccx_id, display_name=ccx.display_name) enroll_email( course_id=ccx_id, student_email=request.user.email, auto_enroll=True, email_students=True, email_params=email_params, ) assign_coach_role_to_ccx(ccx_id, request.user, course.id) add_master_course_staff_to_ccx(course, ccx_id, ccx.display_name) return redirect(url)
def post(self, request): """ Creates a new CCX course for a given Master Course. Args: request (Request): Django request object. Return: A JSON serialized representation a newly created CCX course. """ master_course_id = request.data.get('master_course_id') master_course_object, master_course_key, error_code, http_status = get_valid_course( master_course_id, advanced_course_check=True ) if master_course_object is None: return Response( status=http_status, data={ 'error_code': error_code } ) # validating the rest of the input valid_input, field_errors = get_valid_input(request.data) if field_errors: return Response( status=status.HTTP_400_BAD_REQUEST, data={ 'field_errors': field_errors } ) try: coach = User.objects.get(email=valid_input['coach_email']) except User.DoesNotExist: return Response( status=status.HTTP_404_NOT_FOUND, data={ 'error_code': 'coach_user_does_not_exist' } ) if valid_input.get('course_modules'): if not valid_course_modules(valid_input['course_modules'], master_course_key): return Response( status=status.HTTP_400_BAD_REQUEST, data={ 'error_code': 'course_module_list_not_belonging_to_master_course' } ) # prepare the course_modules to be stored in a json stringified field course_modules_json = json.dumps(valid_input.get('course_modules')) with transaction.atomic(): ccx_course_object = CustomCourseForEdX( course_id=master_course_object.id, coach=coach, display_name=valid_input['display_name'], structure_json=course_modules_json ) ccx_course_object.save() # Make sure start/due are overridden for entire course start = TODAY().replace(tzinfo=pytz.UTC) override_field_for_ccx(ccx_course_object, master_course_object, 'start', start) override_field_for_ccx(ccx_course_object, master_course_object, 'due', None) # Enforce a static limit for the maximum amount of students that can be enrolled override_field_for_ccx( ccx_course_object, master_course_object, 'max_student_enrollments_allowed', valid_input['max_students_allowed'] ) # Hide anything that can show up in the schedule hidden = 'visible_to_staff_only' for chapter in master_course_object.get_children(): override_field_for_ccx(ccx_course_object, chapter, hidden, True) for sequential in chapter.get_children(): override_field_for_ccx(ccx_course_object, sequential, hidden, True) for vertical in sequential.get_children(): override_field_for_ccx(ccx_course_object, vertical, hidden, True) # make the coach user a coach on the master course make_user_coach(coach, master_course_key) # pull the ccx course key ccx_course_key = CCXLocator.from_course_locator(master_course_object.id, ccx_course_object.id) # enroll the coach in the newly created ccx email_params = get_email_params( master_course_object, auto_enroll=True, course_key=ccx_course_key, display_name=ccx_course_object.display_name ) enroll_email( course_id=ccx_course_key, student_email=coach.email, auto_enroll=True, email_students=True, email_params=email_params, ) # assign coach role for the coach to the newly created ccx assign_coach_role_to_ccx(ccx_course_key, coach, master_course_object.id) serializer = self.get_serializer(ccx_course_object) return Response( status=status.HTTP_201_CREATED, data=serializer.data )
def patch(self, request, ccx_course_id=None): """ Modifies a CCX course. Args: request (Request): Django request object. ccx_course_id (string): URI element specifying the CCX course location. """ ccx_course_object, ccx_course_key, error_code, http_status = self.get_object(ccx_course_id, is_ccx=True) if ccx_course_object is None: return Response( status=http_status, data={ 'error_code': error_code } ) master_course_id = request.data.get('master_course_id') if master_course_id is not None and unicode(ccx_course_object.course_id) != master_course_id: return Response( status=status.HTTP_403_FORBIDDEN, data={ 'error_code': 'master_course_id_change_not_allowed' } ) valid_input, field_errors = get_valid_input(request.data, ignore_missing=True) if field_errors: return Response( status=status.HTTP_400_BAD_REQUEST, data={ 'field_errors': field_errors } ) # get the master course key and master course object master_course_object, master_course_key, _, _ = get_valid_course(unicode(ccx_course_object.course_id)) with transaction.atomic(): # update the display name if 'display_name' in valid_input: ccx_course_object.display_name = valid_input['display_name'] # check if the coach has changed and in case update it old_coach = None if 'coach_email' in valid_input: try: coach = User.objects.get(email=valid_input['coach_email']) except User.DoesNotExist: return Response( status=status.HTTP_404_NOT_FOUND, data={ 'error_code': 'coach_user_does_not_exist' } ) if ccx_course_object.coach.id != coach.id: old_coach = ccx_course_object.coach ccx_course_object.coach = coach if 'course_modules' in valid_input: if valid_input.get('course_modules'): if not valid_course_modules(valid_input['course_modules'], master_course_key): return Response( status=status.HTTP_400_BAD_REQUEST, data={ 'error_code': 'course_module_list_not_belonging_to_master_course' } ) # course_modules to be stored in a json stringified field ccx_course_object.structure_json = json.dumps(valid_input.get('course_modules')) ccx_course_object.save() # update the overridden field for the maximum amount of students if 'max_students_allowed' in valid_input: override_field_for_ccx( ccx_course_object, ccx_course_object.course, 'max_student_enrollments_allowed', valid_input['max_students_allowed'] ) # if the coach has changed, update the permissions if old_coach is not None: # make the new ccx coach a coach on the master course make_user_coach(coach, master_course_key) # enroll the coach in the ccx email_params = get_email_params( master_course_object, auto_enroll=True, course_key=ccx_course_key, display_name=ccx_course_object.display_name ) enroll_email( course_id=ccx_course_key, student_email=coach.email, auto_enroll=True, email_students=True, email_params=email_params, ) # make the new coach staff on the CCX assign_staff_role_to_ccx(ccx_course_key, coach, master_course_object.id) # using CCX object as sender here. responses = SignalHandler.course_published.send( sender=ccx_course_object, course_key=ccx_course_key ) for rec, response in responses: log.info('Signal fired when course is published. Receiver: %s. Response: %s', rec, response) return Response( status=status.HTTP_204_NO_CONTENT, )
def post(self, request): """ Creates a new CCX course for a given Master Course. Args: request (Request): Django request object. Return: A JSON serialized representation a newly created CCX course. """ master_course_id = request.data.get('master_course_id') master_course_object, master_course_key, error_code, http_status = get_valid_course( master_course_id, advanced_course_check=True ) if master_course_object is None: return Response( status=http_status, data={ 'error_code': error_code } ) # validating the rest of the input valid_input, field_errors = get_valid_input(request.data) if field_errors: return Response( status=status.HTTP_400_BAD_REQUEST, data={ 'field_errors': field_errors } ) try: # Retired users should effectively appear to not exist when # attempts are made to modify them, so a direct User model email # lookup is sufficient here. This corner case relies on the fact # that we scramble emails immediately during user lock-out. Of # course, the normal cases are that the email just never existed, # or it is currently associated with an active account. coach = User.objects.get(email=valid_input['coach_email']) except User.DoesNotExist: return Response( status=status.HTTP_404_NOT_FOUND, data={ 'error_code': 'coach_user_does_not_exist' } ) if valid_input.get('course_modules'): if not valid_course_modules(valid_input['course_modules'], master_course_key): return Response( status=status.HTTP_400_BAD_REQUEST, data={ 'error_code': 'course_module_list_not_belonging_to_master_course' } ) # prepare the course_modules to be stored in a json stringified field course_modules_json = json.dumps(valid_input.get('course_modules')) with transaction.atomic(): ccx_course_object = CustomCourseForEdX( course_id=master_course_object.id, coach=coach, display_name=valid_input['display_name'], structure_json=course_modules_json ) ccx_course_object.save() # Make sure start/due are overridden for entire course start = TODAY().replace(tzinfo=pytz.UTC) override_field_for_ccx(ccx_course_object, master_course_object, 'start', start) override_field_for_ccx(ccx_course_object, master_course_object, 'due', None) # Enforce a static limit for the maximum amount of students that can be enrolled override_field_for_ccx( ccx_course_object, master_course_object, 'max_student_enrollments_allowed', valid_input['max_students_allowed'] ) # Hide anything that can show up in the schedule hidden = 'visible_to_staff_only' for chapter in master_course_object.get_children(): override_field_for_ccx(ccx_course_object, chapter, hidden, True) for sequential in chapter.get_children(): override_field_for_ccx(ccx_course_object, sequential, hidden, True) for vertical in sequential.get_children(): override_field_for_ccx(ccx_course_object, vertical, hidden, True) # make the coach user a coach on the master course make_user_coach(coach, master_course_key) # pull the ccx course key ccx_course_key = CCXLocator.from_course_locator(master_course_object.id, unicode(ccx_course_object.id)) # enroll the coach in the newly created ccx email_params = get_email_params( master_course_object, auto_enroll=True, course_key=ccx_course_key, display_name=ccx_course_object.display_name ) enroll_email( course_id=ccx_course_key, student_email=coach.email, auto_enroll=True, email_students=True, email_params=email_params, ) # assign staff role for the coach to the newly created ccx assign_staff_role_to_ccx(ccx_course_key, coach, master_course_object.id) # assign staff role for all the staff and instructor of the master course to the newly created ccx add_master_course_staff_to_ccx( master_course_object, ccx_course_key, ccx_course_object.display_name, send_email=False ) serializer = self.get_serializer(ccx_course_object) # using CCX object as sender here. responses = SignalHandler.course_published.send( sender=ccx_course_object, course_key=ccx_course_key ) for rec, response in responses: log.info('Signal fired when course is published. Receiver: %s. Response: %s', rec, response) return Response( status=status.HTTP_201_CREATED, data=serializer.data )
def setUp(self): """ Set up tests """ super(TestCCXGrades, self).setUp() self._course = CourseFactory.create(enable_ccx=True) # Create a course outline self.start = datetime.datetime(2010, 5, 12, 2, 42, tzinfo=pytz.UTC) chapter = ItemFactory.create(start=self.start, parent=self._course, category='sequential') self.sections = [ ItemFactory.create(parent=chapter, category="sequential", metadata={ 'graded': True, 'format': 'Homework' }) for _ in xrange(4) ] # making problems available at class level for possible future use in tests self.problems = [[ ItemFactory.create( parent=section, category="problem", data=StringResponseXMLFactory().build_xml(answer='foo'), metadata={'rerandomize': 'always'}) for _ in xrange(4) ] for section in self.sections] # Create instructor account self.coach = coach = AdminFactory.create() self.client.login(username=coach.username, password="******") # Create CCX role = CourseCcxCoachRole(self._course.id) role.add_users(coach) ccx = CcxFactory(course_id=self._course.id, coach=self.coach) # override course grading policy and make last section invisible to students override_field_for_ccx( ccx, self._course, 'grading_policy', { 'GRADER': [{ 'drop_count': 0, 'min_count': 2, 'short_label': 'HW', 'type': 'Homework', 'weight': 1 }], 'GRADE_CUTOFFS': { 'Pass': 0.75 }, }) override_field_for_ccx(ccx, self.sections[-1], 'visible_to_staff_only', True) # create a ccx locator and retrieve the course structure using that key # which emulates how a student would get access. self.ccx_key = CCXLocator.from_course_locator(self._course.id, ccx.id) self.course = get_course_by_id(self.ccx_key, depth=None) setup_students_and_grades(self) self.client.login(username=coach.username, password="******") self.addCleanup(RequestCache.clear_request_cache)
def post(self, request): """ Creates a new CCX course for a given Master Course. Args: request (Request): Django request object. Return: A JSON serialized representation a newly created CCX course. """ master_course_id = request.data.get('master_course_id') master_course_object, master_course_key, error_code, http_status = get_valid_course( master_course_id, advanced_course_check=True) if master_course_object is None: return Response(status=http_status, data={'error_code': error_code}) # validating the rest of the input valid_input, field_errors = get_valid_input(request.data) if field_errors: return Response(status=status.HTTP_400_BAD_REQUEST, data={'field_errors': field_errors}) try: coach = User.objects.get(email=valid_input['coach_email']) except User.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND, data={'error_code': 'coach_user_does_not_exist'}) if valid_input.get('course_modules'): if not valid_course_modules(valid_input['course_modules'], master_course_key): return Response( status=status.HTTP_400_BAD_REQUEST, data={ 'error_code': 'course_module_list_not_belonging_to_master_course' }) # prepare the course_modules to be stored in a json stringified field course_modules_json = json.dumps(valid_input.get('course_modules')) with transaction.atomic(): ccx_course_object = CustomCourseForEdX( course_id=master_course_object.id, coach=coach, display_name=valid_input['display_name'], structure_json=course_modules_json) ccx_course_object.save() # Make sure start/due are overridden for entire course start = TODAY().replace(tzinfo=pytz.UTC) override_field_for_ccx(ccx_course_object, master_course_object, 'start', start) override_field_for_ccx(ccx_course_object, master_course_object, 'due', None) # Enforce a static limit for the maximum amount of students that can be enrolled override_field_for_ccx(ccx_course_object, master_course_object, 'max_student_enrollments_allowed', valid_input['max_students_allowed']) # Hide anything that can show up in the schedule hidden = 'visible_to_staff_only' for chapter in master_course_object.get_children(): override_field_for_ccx(ccx_course_object, chapter, hidden, True) for sequential in chapter.get_children(): override_field_for_ccx(ccx_course_object, sequential, hidden, True) for vertical in sequential.get_children(): override_field_for_ccx(ccx_course_object, vertical, hidden, True) # make the coach user a coach on the master course make_user_coach(coach, master_course_key) # pull the ccx course key ccx_course_key = CCXLocator.from_course_locator( master_course_object.id, ccx_course_object.id) # enroll the coach in the newly created ccx email_params = get_email_params( master_course_object, auto_enroll=True, course_key=ccx_course_key, display_name=ccx_course_object.display_name) enroll_email( course_id=ccx_course_key, student_email=coach.email, auto_enroll=True, email_students=True, email_params=email_params, ) # assign coach role for the coach to the newly created ccx assign_coach_role_to_ccx(ccx_course_key, coach, master_course_object.id) # assign staff role for all the staff and instructor of the master course to the newly created ccx add_master_course_staff_to_ccx(master_course_object, ccx_course_key, ccx_course_object.display_name, send_email=False) serializer = self.get_serializer(ccx_course_object) return Response(status=status.HTTP_201_CREATED, data=serializer.data)
def create_ccx(request, course, ccx=None): """ Create a new CCX """ name = request.POST.get('name') if hasattr(course, 'ccx_connector') and course.ccx_connector: # if ccx connector url is set in course settings then inform user that he can # only create ccx by using ccx connector url. context = get_ccx_creation_dict(course) messages.error(request, context['use_ccx_con_error_message']) return render_to_response('ccx/coach_dashboard.html', context) # prevent CCX objects from being created for deprecated course ids. if course.id.deprecated: messages.error(request, _( "You cannot create a CCX from a course using a deprecated id. " "Please create a rerun of this course in the studio to allow " "this action.")) url = reverse('ccx_coach_dashboard', kwargs={'course_id': course.id}) return redirect(url) ccx = CustomCourseForEdX( course_id=course.id, coach=request.user, display_name=name) ccx.save() # Make sure start/due are overridden for entire course start = TODAY().replace(tzinfo=pytz.UTC) override_field_for_ccx(ccx, course, 'start', start) override_field_for_ccx(ccx, course, 'due', None) # Enforce a static limit for the maximum amount of students that can be enrolled override_field_for_ccx(ccx, course, 'max_student_enrollments_allowed', settings.CCX_MAX_STUDENTS_ALLOWED) # Hide anything that can show up in the schedule hidden = 'visible_to_staff_only' for chapter in course.get_children(): override_field_for_ccx(ccx, chapter, hidden, True) for sequential in chapter.get_children(): override_field_for_ccx(ccx, sequential, hidden, True) for vertical in sequential.get_children(): override_field_for_ccx(ccx, vertical, hidden, True) ccx_id = CCXLocator.from_course_locator(course.id, unicode(ccx.id)) # Create forum roles seed_permissions_roles(ccx_id) # Assign administrator forum role to CCX coach assign_role(ccx_id, request.user, FORUM_ROLE_ADMINISTRATOR) url = reverse('ccx_coach_dashboard', kwargs={'course_id': ccx_id}) # Enroll the coach in the course email_params = get_email_params(course, auto_enroll=True, course_key=ccx_id, display_name=ccx.display_name) enroll_email( course_id=ccx_id, student_email=request.user.email, auto_enroll=True, email_students=True, email_params=email_params, ) assign_staff_role_to_ccx(ccx_id, request.user, course.id) add_master_course_staff_to_ccx(course, ccx_id, ccx.display_name) # using CCX object as sender here. responses = SignalHandler.course_published.send( sender=ccx, course_key=CCXLocator.from_course_locator(course.id, unicode(ccx.id)) ) for rec, response in responses: log.info('Signal fired when course is published. Receiver: %s. Response: %s', rec, response) return redirect(url)
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 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 create_ccx(request, course, ccx=None): """ Create a new CCX """ name = request.POST.get('name') if hasattr(course, 'ccx_connector') and course.ccx_connector: # if ccx connector url is set in course settings then inform user that he can # only create ccx by using ccx connector url. context = get_ccx_creation_dict(course) messages.error(request, context['use_ccx_con_error_message']) return render_to_response('ccx/coach_dashboard.html', context) # prevent CCX objects from being created for deprecated course ids. if course.id.deprecated: messages.error( request, _("You cannot create a CCX from a course using a deprecated id. " "Please create a rerun of this course in the studio to allow " "this action.")) url = reverse('ccx_coach_dashboard', kwargs={'course_id': course.id}) return redirect(url) ccx = CustomCourseForEdX(course_id=course.id, coach=request.user, display_name=name) ccx.save() # Make sure start/due are overridden for entire course start = TODAY().replace(tzinfo=pytz.UTC) override_field_for_ccx(ccx, course, 'start', start) override_field_for_ccx(ccx, course, 'due', None) # Enforce a static limit for the maximum amount of students that can be enrolled override_field_for_ccx(ccx, course, 'max_student_enrollments_allowed', settings.CCX_MAX_STUDENTS_ALLOWED) # Save display name explicitly override_field_for_ccx(ccx, course, 'display_name', name) # Hide anything that can show up in the schedule hidden = 'visible_to_staff_only' for chapter in course.get_children(): override_field_for_ccx(ccx, chapter, hidden, True) for sequential in chapter.get_children(): override_field_for_ccx(ccx, sequential, hidden, True) for vertical in sequential.get_children(): override_field_for_ccx(ccx, vertical, hidden, True) ccx_id = CCXLocator.from_course_locator(course.id, str(ccx.id)) # Create forum roles seed_permissions_roles(ccx_id) # Assign administrator forum role to CCX coach assign_role(ccx_id, request.user, FORUM_ROLE_ADMINISTRATOR) url = reverse('ccx_coach_dashboard', kwargs={'course_id': ccx_id}) # Enroll the coach in the course email_params = get_email_params(course, auto_enroll=True, course_key=ccx_id, display_name=ccx.display_name) enroll_email( course_id=ccx_id, student_email=request.user.email, auto_enroll=True, email_students=True, email_params=email_params, ) assign_staff_role_to_ccx(ccx_id, request.user, course.id) add_master_course_staff_to_ccx(course, ccx_id, ccx.display_name) # 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 redirect(url)
def setUp(self): """ Set up tests """ super(TestCCXGrades, self).setUp() self._course = CourseFactory.create(enable_ccx=True) # Create a course outline self.start = datetime.datetime( 2010, 5, 12, 2, 42, tzinfo=pytz.UTC ) chapter = ItemFactory.create( start=self.start, parent=self._course, category='sequential' ) self.sections = [ ItemFactory.create( parent=chapter, category="sequential", metadata={'graded': True, 'format': 'Homework'}) for _ in xrange(4) ] # making problems available at class level for possible future use in tests self.problems = [ [ ItemFactory.create( parent=section, category="problem", data=StringResponseXMLFactory().build_xml(answer='foo'), metadata={'rerandomize': 'always'} ) for _ in xrange(4) ] for section in self.sections ] # Create instructor account self.coach = coach = AdminFactory.create() self.client.login(username=coach.username, password="******") # Create CCX role = CourseCcxCoachRole(self._course.id) role.add_users(coach) ccx = CcxFactory(course_id=self._course.id, coach=self.coach) # override course grading policy and make last section invisible to students override_field_for_ccx(ccx, self._course, 'grading_policy', { 'GRADER': [ {'drop_count': 0, 'min_count': 2, 'short_label': 'HW', 'type': 'Homework', 'weight': 1} ], 'GRADE_CUTOFFS': {'Pass': 0.75}, }) override_field_for_ccx( ccx, self.sections[-1], 'visible_to_staff_only', True ) # create a ccx locator and retrieve the course structure using that key # which emulates how a student would get access. self.ccx_key = CCXLocator.from_course_locator(self._course.id, ccx.id) self.course = get_course_by_id(self.ccx_key, depth=None) setup_students_and_grades(self) self.client.login(username=coach.username, password="******") self.addCleanup(RequestCache.clear_request_cache)
def create_ccx(request, course, ccx=None, **kwargs): """ Create a new CCX """ if not is_ccx_coach_on_master_course( request.user, course) or not request.user.profile.affiliate: return HttpResponseForbidden() affiliate_slug = request.POST.get('affiliate') name = request.POST.get('name') delivery_mode = request.POST.get('delivery_mode') location_city = request.POST.get('city') location_state = request.POST.get('state') location_postal_code = request.POST.get('postal_code') time = '{} {}Z'.format(request.POST.get('date'), request.POST.get('time')) enrollment_end_date = '{} {}Z'.format( request.POST.get('enrollment_end_date'), request.POST.get('enrollment_end_time')) end_date = '{} {}Z'.format(request.POST.get('end_date'), request.POST.get('end_time')) fee = request.POST.get('fee') course_description = request.POST.get('course_description') enrollment_type = request.POST.get('enrollment_type') facilitators = dict(request.POST).get('facilitators') context = get_ccx_creation_dict(course) if not affiliate_slug: messages.error(request, 'Affiliate not selected.') return render_to_response('ccx/coach_dashboard.html', context) if not facilitators: messages.error(request, 'No facilitators added.') return render_to_response('ccx/coach_dashboard.html', context) if hasattr(course, 'ccx_connector') and course.ccx_connector: # if ccx connector url is set in course settings then inform user that he can # only create ccx by using ccx connector url. messages.error(request, context['use_ccx_con_error_message']) return render_to_response('ccx/coach_dashboard.html', context) # prevent CCX objects from being created for deprecated course ids. if course.id.deprecated: messages.error( request, _("You cannot create a CCX from a course using a deprecated id. " "Please create a rerun of this course in the studio to allow " "this action.")) url = reverse('ccx_coach_dashboard', kwargs={'course_id': course.id}) return redirect(url) affiliate = AffiliateEntity.objects.get(slug=affiliate_slug) if not request.user.is_staff and not AffiliateMembership.objects.filter( member=request.user, affiliate=affiliate, role__in=AffiliateMembership.STAFF_ROLES).exists(): return HttpResponseForbidden() ccx = CustomCourseForEdX(affiliate=affiliate, course_id=course.id, coach=request.user, display_name=name, delivery_mode=delivery_mode, location_city=location_city, location_state=location_state, location_postal_code=location_postal_code, time=time, enrollment_end_date=enrollment_end_date, end_date=end_date, fee=ast.literal_eval(fee), enrollment_type=enrollment_type, course_description=course_description) ccx.save() # we need this for authorization ccx.save() # Make sure start/due are overridden for entire course start = TODAY().replace(tzinfo=pytz.UTC) override_field_for_ccx(ccx, course, 'start', start) override_field_for_ccx(ccx, course, 'due', None) # Enforce a static limit for the maximum amount of students that can be enrolled override_field_for_ccx(ccx, course, 'max_student_enrollments_allowed', settings.CCX_MAX_STUDENTS_ALLOWED) # Hide anything that can show up in the schedule hidden = 'visible_to_staff_only' for chapter in course.get_children(): override_field_for_ccx(ccx, chapter, hidden, True) for sequential in chapter.get_children(): override_field_for_ccx(ccx, sequential, hidden, True) for vertical in sequential.get_children(): override_field_for_ccx(ccx, vertical, hidden, True) ccx_id = CCXLocator.from_course_locator(course.id, ccx.pk) url = reverse('ccx_coach_dashboard', kwargs={'course_id': ccx_id}) # Enroll the coach in the course email_params = get_email_params(course, auto_enroll=True, course_key=ccx_id, display_name=ccx.display_name) enroll_email( course_id=ccx_id, student_email=request.user.email, auto_enroll=True, email_students=True, email_params=email_params, ) # Add facilitators if facilitators: course_obj = get_course_by_id(ccx.ccx_course_id, depth=None) facilitator_ids = [int(i) for i in facilitators] for user_id in facilitator_ids: user = User.objects.get(id=user_id) enroll_email(course_id=ccx_id, student_email=user.email, auto_enroll=True, email_students=True, email_params=email_params) allow_access(course_obj, user, AffiliateMembership.CCX_COACH, False) return redirect(url)
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', )