Пример #1
0
    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)
Пример #3
0
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)
Пример #4
0
    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)
Пример #5
0
    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)
Пример #6
0
 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
Пример #7
0
 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
Пример #8
0
    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
Пример #9
0
    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)
Пример #10
0
    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
Пример #11
0
 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)
Пример #12
0
 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)
Пример #13
0
 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)
Пример #14
0
 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)
Пример #15
0
 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)
Пример #16
0
 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)
Пример #17
0
    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
Пример #18
0
    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)
Пример #20
0
 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)
Пример #21
0
    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)
Пример #22
0
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)
Пример #23
0
 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)
Пример #24
0
 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)
Пример #25
0
 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)
Пример #26
0
 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)
Пример #27
0
 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)
Пример #28
0
 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)
Пример #29
0
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)
Пример #30
0
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)
Пример #31
0
    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
Пример #32
0
    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
        )
Пример #33
0
    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
Пример #34
0
    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, )
Пример #35
0
    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)
Пример #36
0
    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,
        )
Пример #37
0
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',
    )
Пример #39
0
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)
Пример #40
0
    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
        )
Пример #41
0
    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,
        )
Пример #42
0
    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
        )
Пример #43
0
    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)
Пример #44
0
    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)
Пример #45
0
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)
Пример #46
0
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',
    )
Пример #47
0
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)
Пример #49
0
    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)
Пример #50
0
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)
Пример #51
0
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',
    )