Example #1
0
 def visit(node, depth=1):
     """
     Recursive generator function which yields CCX schedule nodes.
     We convert dates to string to get them ready for use by the js date
     widgets, which use text inputs.
     Visits students visible nodes only; nodes children of hidden ones
     are skipped as well.
     """
     for child in node.get_children():
         # in case the children are visible to staff only, skip them
         if child.visible_to_staff_only:
             continue
         start = get_override_for_ccx(ccx, child, "start", None)
         if start:
             start = str(start)[:-9]
         due = get_override_for_ccx(ccx, child, "due", None)
         if due:
             due = str(due)[:-9]
         hidden = get_override_for_ccx(ccx, child, "visible_to_staff_only", child.visible_to_staff_only)
         visited = {
             "location": str(child.location),
             "display_name": child.display_name,
             "category": child.category,
             "start": start,
             "due": due,
             "hidden": hidden,
         }
         if depth < 3:
             children = tuple(visit(child, depth + 1))
             if children:
                 visited["children"] = children
                 yield visited
         else:
             yield visited
Example #2
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
Example #3
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
Example #4
0
def dashboard(request, course, ccx=None):
    """
    Display the CCX Coach Dashboard.
    """
    # right now, we can only have one ccx per user and course
    # so, if no ccx is passed in, we can sefely redirect to that
    if ccx is None:
        ccx = get_ccx_for_coach(course, request.user)
        if ccx:
            url = reverse(
                "ccx_coach_dashboard", kwargs={"course_id": CCXLocator.from_course_locator(course.id, ccx.id)}
            )
            return redirect(url)

    context = {"course": course, "ccx": ccx}

    if ccx:
        ccx_locator = CCXLocator.from_course_locator(course.id, ccx.id)
        schedule = get_ccx_schedule(course, ccx)
        grading_policy = get_override_for_ccx(ccx, course, "grading_policy", course.grading_policy)
        context["schedule"] = json.dumps(schedule, indent=4)
        context["save_url"] = reverse("save_ccx", kwargs={"course_id": ccx_locator})
        context["ccx_members"] = CourseEnrollment.objects.filter(course_id=ccx_locator, is_active=True)
        context["gradebook_url"] = reverse("ccx_gradebook", kwargs={"course_id": ccx_locator})
        context["grades_csv_url"] = reverse("ccx_grades_csv", kwargs={"course_id": ccx_locator})
        context["grading_policy"] = json.dumps(grading_policy, indent=4)
        context["grading_policy_url"] = reverse("ccx_set_grading_policy", kwargs={"course_id": ccx_locator})
    else:
        context["create_ccx_url"] = reverse("create_ccx", kwargs={"course_id": course.id})
    return render_to_response("ccx/coach_dashboard.html", context)
Example #5
0
    def visit(node, depth=1):
        """
        Recursive generator function which yields CCX schedule nodes.
        We convert dates to string to get them ready for use by the js date
        widgets, which use text inputs.
        Visits students visible nodes only; nodes children of hidden ones
        are skipped as well.

        Dates:
        Only start date is applicable to a section. If ccx coach did not override start date then
        getting it from the master course.
        Both start and due dates are applicable to a subsection (aka sequential). If ccx coach did not override
        these dates then getting these dates from corresponding subsection in master course.
        Unit inherits start date and due date from its subsection. If ccx coach did not override these dates
        then getting them from corresponding subsection in master course.
        """
        for child in node.get_children():
            # in case the children are visible to staff only, skip them
            if child.visible_to_staff_only:
                continue

            hidden = get_override_for_ccx(ccx, child, 'visible_to_staff_only',
                                          child.visible_to_staff_only)

            start = get_date(ccx, child, 'start')
            if depth > 1:
                # Subsection has both start and due dates and unit inherit dates from their subsections
                if depth == 2:
                    due = get_date(ccx, child, 'due')
                elif depth == 3:
                    # Get start and due date of subsection in case unit has not override dates.
                    due = get_date(ccx, child, 'due', node)
                    start = get_date(ccx, child, 'start', node)

                visited = {
                    'location': str(child.location),
                    'display_name': child.display_name,
                    'category': child.category,
                    'start': start,
                    'due': due,
                    'hidden': hidden,
                }
            else:
                visited = {
                    'location': str(child.location),
                    'display_name': child.display_name,
                    'category': child.category,
                    'start': start,
                    'hidden': hidden,
                }
            if depth < 3:
                children = tuple(visit(child, depth + 1))
                if children:
                    visited['children'] = children
                    yield visited
            elif return_xblocks and depth == 3:
                visited['children'] = child.children
                yield visited
            else:
                yield visited
Example #6
0
def get_date(ccx, node, date_type=None, parent_node=None):
    """
    This returns override or master date for section, subsection or a unit.

    :param ccx: ccx instance
    :param node: chapter, subsection or unit
    :param date_type: start or due
    :param parent_node: parent of node
    :return: start or due date
    """
    date = get_override_for_ccx(ccx, node, date_type, None)
    if date_type == "start":
        master_date = node.start
    else:
        master_date = node.due

    if date is not None:
        # Setting override date [start or due]
        date = date.strftime('%Y-%m-%d %H:%M')
    elif not parent_node and master_date is not None:
        # Setting date from master course
        date = master_date.strftime('%Y-%m-%d %H:%M')
    elif parent_node is not None:
        # Set parent date (vertical has same dates as subsections)
        date = get_date(ccx, node=parent_node, date_type=date_type)

    return date
Example #7
0
    def test_create_ccx(self):
        """
        Create CCX. Follow redirect to coach dashboard, confirm we see
        the coach dashboard for the new CCX.
        """

        self.make_coach()
        url = reverse('create_ccx',
                      kwargs={'course_id': unicode(self.course.id)})

        response = self.client.post(url, {'name': 'New CCX'})
        self.assertEqual(response.status_code, 302)
        url = response.get('location')  # pylint: disable=no-member
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)

        # Get the ccx_key
        path = urlparse.urlparse(url).path
        resolver = resolve(path)
        ccx_key = resolver.kwargs['course_id']

        course_key = CourseKey.from_string(ccx_key)

        self.assertTrue(CourseEnrollment.is_enrolled(self.coach, course_key))
        self.assertTrue(re.search('id="ccx-schedule"', response.content))

        # check if the max amount of student that can be enrolled has been overridden
        ccx = CustomCourseForEdX.objects.get()
        course_enrollments = get_override_for_ccx(
            ccx, self.course, 'max_student_enrollments_allowed')
        self.assertEqual(course_enrollments, settings.CCX_MAX_STUDENTS_ALLOWED)

        # assert ccx creator has role=ccx_coach
        role = CourseCcxCoachRole(course_key)
        self.assertTrue(role.has_user(self.coach, refresh=True))
Example #8
0
    def test_save_without_min_count(self):
        """
        POST grading policy without min_count field.
        """
        self.make_coach()
        ccx = self.make_ccx()

        course_id = CCXLocator.from_course_locator(self.course.id, ccx.id)
        save_policy_url = reverse("ccx_set_grading_policy", kwargs={"course_id": course_id})

        # This policy doesn't include a min_count field
        policy = {
            "GRADE_CUTOFFS": {"Pass": 0.5},
            "GRADER": [{"weight": 0.15, "type": "Homework", "drop_count": 2, "short_label": "HW"}],
        }

        response = self.client.post(save_policy_url, {"policy": json.dumps(policy)})
        self.assertEqual(response.status_code, 302)

        ccx = CustomCourseForEdX.objects.get()

        # Make sure grading policy adjusted
        policy = get_override_for_ccx(ccx, self.course, "grading_policy", self.course.grading_policy)
        self.assertEqual(len(policy["GRADER"]), 1)
        self.assertEqual(policy["GRADER"][0]["type"], "Homework")
        self.assertNotIn("min_count", policy["GRADER"][0])

        save_ccx_url = reverse("save_ccx", kwargs={"course_id": course_id})
        coach_dashboard_url = reverse("ccx_coach_dashboard", kwargs={"course_id": course_id})
        response = self.client.get(coach_dashboard_url)
        schedule = json.loads(response.mako_context["schedule"])  # pylint: disable=no-member
        response = self.client.post(save_ccx_url, json.dumps(schedule), content_type="application/json")
        self.assertEqual(response.status_code, 200)
Example #9
0
def get_date(ccx, node, date_type=None, parent_node=None):
    """
    This returns override or master date for section, subsection or a unit.

    :param ccx: ccx instance
    :param node: chapter, subsection or unit
    :param date_type: start or due
    :param parent_node: parent of node
    :return: start or due date
    """
    date = get_override_for_ccx(ccx, node, date_type, None)
    if date_type == "start":
        master_date = node.start
    else:
        master_date = node.due

    if date is not None:
        # Setting override date [start or due]
        date = date.strftime('%Y-%m-%d %H:%M')
    elif not parent_node and master_date is not None:
        # Setting date from master course
        date = master_date.strftime('%Y-%m-%d %H:%M')
    elif parent_node is not None:
        # Set parent date (vertical has same dates as subsections)
        date = get_date(ccx, node=parent_node, date_type=date_type)

    return date
Example #10
0
    def test_create_ccx(self):
        """
        Create CCX. Follow redirect to coach dashboard, confirm we see
        the coach dashboard for the new CCX.
        """

        self.make_coach()
        url = reverse(
            'create_ccx',
            kwargs={'course_id': unicode(self.course.id)})

        response = self.client.post(url, {'name': 'New CCX'})
        self.assertEqual(response.status_code, 302)
        url = response.get('location')  # pylint: disable=no-member
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)

        # Get the ccx_key
        path = urlparse.urlparse(url).path
        resolver = resolve(path)
        ccx_key = resolver.kwargs['course_id']

        course_key = CourseKey.from_string(ccx_key)

        self.assertTrue(CourseEnrollment.is_enrolled(self.coach, course_key))
        self.assertTrue(re.search('id="ccx-schedule"', response.content))

        # check if the max amount of student that can be enrolled has been overridden
        ccx = CustomCourseForEdX.objects.get()
        course_enrollments = get_override_for_ccx(ccx, self.course, 'max_student_enrollments_allowed')
        self.assertEqual(course_enrollments, settings.CCX_MAX_STUDENTS_ALLOWED)
Example #11
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
Example #12
0
    def visit(node, depth=1):
        """
        Recursive generator function which yields CCX schedule nodes.
        We convert dates to string to get them ready for use by the js date
        widgets, which use text inputs.
        Visits students visible nodes only; nodes children of hidden ones
        are skipped as well.

        Dates:
        Only start date is applicable to a section. If ccx coach did not override start date then
        getting it from the master course.
        Both start and due dates are applicable to a subsection (aka sequential). If ccx coach did not override
        these dates then getting these dates from corresponding subsection in master course.
        Unit inherits start date and due date from its subsection. If ccx coach did not override these dates
        then getting them from corresponding subsection in master course.
        """
        for child in node.get_children():
            # in case the children are visible to staff only, skip them
            if child.visible_to_staff_only:
                continue

            hidden = get_override_for_ccx(
                ccx, child, 'visible_to_staff_only',
                child.visible_to_staff_only)

            start = get_date(ccx, child, 'start')
            if depth > 1:
                # Subsection has both start and due dates and unit inherit dates from their subsections
                if depth == 2:
                    due = get_date(ccx, child, 'due')
                elif depth == 3:
                    # Get start and due date of subsection in case unit has not override dates.
                    due = get_date(ccx, child, 'due', node)
                    start = get_date(ccx, child, 'start', node)

                visited = {
                    'location': str(child.location),
                    'display_name': child.display_name,
                    'category': child.category,
                    'start': start,
                    'due': due,
                    'hidden': hidden,
                }
            else:
                visited = {
                    'location': str(child.location),
                    'display_name': child.display_name,
                    'category': child.category,
                    'start': start,
                    'hidden': hidden,
                }
            if depth < 3:
                children = tuple(visit(child, depth + 1))
                if children:
                    visited['children'] = children
                    yield visited
            else:
                yield visited
Example #13
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
Example #14
0
    def test_save_without_min_count(self):
        """
        POST grading policy without min_count field.
        """
        self.make_coach()
        ccx = self.make_ccx()

        course_id = CCXLocator.from_course_locator(self.course.id, ccx.id)
        save_policy_url = reverse(
            'ccx_set_grading_policy', kwargs={'course_id': course_id})

        # This policy doesn't include a min_count field
        policy = {
            "GRADE_CUTOFFS": {
                "Pass": 0.5
            },
            "GRADER": [
                {
                    "weight": 0.15,
                    "type": "Homework",
                    "drop_count": 2,
                    "short_label": "HW"
                }
            ]
        }

        response = self.client.post(
            save_policy_url, {"policy": json.dumps(policy)}
        )
        self.assertEqual(response.status_code, 302)

        ccx = CustomCourseForEdX.objects.get()

        # Make sure grading policy adjusted
        policy = get_override_for_ccx(
            ccx, self.course, 'grading_policy', self.course.grading_policy
        )
        self.assertEqual(len(policy['GRADER']), 1)
        self.assertEqual(policy['GRADER'][0]['type'], 'Homework')
        self.assertNotIn('min_count', policy['GRADER'][0])

        save_ccx_url = reverse('save_ccx', kwargs={'course_id': course_id})
        coach_dashboard_url = reverse(
            'ccx_coach_dashboard',
            kwargs={'course_id': course_id}
        )
        response = self.client.get(coach_dashboard_url)
        schedule = json.loads(response.mako_context['schedule'])  # pylint: disable=no-member
        response = self.client.post(
            save_ccx_url, json.dumps(schedule), content_type='application/json'
        )
        self.assertEqual(response.status_code, 200)
Example #15
0
def dashboard(request, course, ccx=None):
    """
    Display the CCX Coach Dashboard.
    """
    # right now, we can only have one ccx per user and course
    # so, if no ccx is passed in, we can sefely redirect to that
    if ccx is None:
        ccx = get_ccx_for_coach(course, request.user)
        if ccx:
            url = reverse('ccx_coach_dashboard',
                          kwargs={
                              'course_id':
                              CCXLocator.from_course_locator(
                                  course.id, ccx.id)
                          })
            return redirect(url)

    context = {
        'course': course,
        'ccx': ccx,
    }
    context.update(get_ccx_creation_dict(course))

    if ccx:
        ccx_locator = CCXLocator.from_course_locator(course.id,
                                                     unicode(ccx.id))
        # At this point we are done with verification that current user is ccx coach.
        assign_coach_role_to_ccx(ccx_locator, request.user, course.id)

        schedule = get_ccx_schedule(course, ccx)
        grading_policy = get_override_for_ccx(ccx, course, 'grading_policy',
                                              course.grading_policy)
        context['schedule'] = json.dumps(schedule, indent=4)
        context['save_url'] = reverse('save_ccx',
                                      kwargs={'course_id': ccx_locator})
        context['ccx_members'] = CourseEnrollment.objects.filter(
            course_id=ccx_locator, is_active=True)
        context['gradebook_url'] = reverse('ccx_gradebook',
                                           kwargs={'course_id': ccx_locator})
        context['grades_csv_url'] = reverse('ccx_grades_csv',
                                            kwargs={'course_id': ccx_locator})
        context['grading_policy'] = json.dumps(grading_policy, indent=4)
        context['grading_policy_url'] = reverse(
            'ccx_set_grading_policy', kwargs={'course_id': ccx_locator})

        with ccx_course(ccx_locator) as course:
            context['course'] = course

    else:
        context['create_ccx_url'] = reverse('create_ccx',
                                            kwargs={'course_id': course.id})
    return render_to_response('ccx/coach_dashboard.html', context)
Example #16
0
def dashboard(request, course, ccx=None):
    """
    Display the CCX Coach Dashboard.
    """
    # right now, we can only have one ccx per user and course
    # so, if no ccx is passed in, we can sefely redirect to that
    if ccx is None:
        ccx = get_ccx_for_coach(course, request.user)
        if ccx:
            url = reverse(
                'ccx_coach_dashboard',
                kwargs={
                    'course_id':
                    CCXLocator.from_course_locator(course.id, unicode(ccx.id))
                })
            return redirect(url)

    context = {
        'course': course,
        'ccx': ccx,
    }
    context.update(get_ccx_creation_dict(course))

    if ccx:
        ccx_locator = CCXLocator.from_course_locator(course.id,
                                                     unicode(ccx.id))
        # At this point we are done with verification that current user is ccx coach.
        assign_staff_role_to_ccx(ccx_locator, request.user, course.id)
        schedule = get_ccx_schedule(course, ccx)
        grading_policy = get_override_for_ccx(ccx, course, 'grading_policy',
                                              course.grading_policy)
        context['schedule'] = json.dumps(schedule, indent=4)
        context['save_url'] = reverse(
            'save_ccx', kwargs={'course_id': ccx_locator})
        context['ccx_members'] = CourseEnrollment.objects.filter(
            course_id=ccx_locator, is_active=True)
        context['gradebook_url'] = reverse(
            'ccx_gradebook', kwargs={'course_id': ccx_locator})
        context['grades_csv_url'] = reverse(
            'ccx_grades_csv', kwargs={'course_id': ccx_locator})
        context['grading_policy'] = json.dumps(grading_policy, indent=4)
        context['grading_policy_url'] = reverse(
            'ccx_set_grading_policy', kwargs={'course_id': ccx_locator})

        with ccx_course(ccx_locator) as course:
            context['course'] = course

    else:
        context['create_ccx_url'] = reverse(
            'create_ccx', kwargs={'course_id': course.id})
    return render_to_response('ccx/coach_dashboard.html', context)
Example #17
0
def edit_ccx_context(course, ccx, user, **kwargs):
    ccx_locator = CCXLocator.from_course_locator(course.id, unicode(ccx.pk))

    schedule = get_ccx_schedule(course, ccx)
    grading_policy = get_override_for_ccx(ccx, course, 'grading_policy',
                                          course.grading_policy)

    context = {}
    context['ccx_locator'] = ccx_locator
    context['modify_access_url'] = reverse('modify_access',
                                           kwargs={'course_id': ccx_locator})
    context['schedule'] = json.dumps(schedule, indent=4)
    context['save_url'] = reverse('save_ccx',
                                  kwargs={'course_id': ccx_locator})

    non_student_user_ids = CourseAccessRole.objects.filter(
        course_id=ccx_locator).values_list('user_id', flat=True)
    ccx_student_enrollments = CourseEnrollment.objects.filter(
        course_id=ccx_locator,
        is_active=True).exclude(user_id__in=non_student_user_ids)

    context['ccx_student_enrollments'] = ccx_student_enrollments
    context['gradebook_url'] = reverse('ccx_gradebook',
                                       kwargs={'course_id': ccx_locator})
    context['grades_csv_url'] = reverse('ccx_grades_csv',
                                        kwargs={'course_id': ccx_locator})
    context['grading_policy'] = json.dumps(grading_policy, indent=4)
    context['grading_policy_url'] = reverse('ccx_set_grading_policy',
                                            kwargs={'course_id': ccx_locator})
    context['STATE_CHOICES'] = STATE_CHOICES

    all_facilitators = get_facilitators(ccx.affiliate)
    added_facilitator_user_ids = CourseAccessRole.objects.filter(
        course_id=ccx_locator,
        role=AffiliateMembership.CCX_COACH).values_list('user_id', flat=True)
    context['added_facilitators'] = all_facilitators.filter(
        id__in=added_facilitator_user_ids)
    context['not_added_facilitators'] = all_facilitators.exclude(
        id__in=added_facilitator_user_ids)

    with ccx_course(ccx_locator) as course:
        context['course'] = course

    context['edit_ccx_url'] = reverse('edit_ccx',
                                      kwargs={'course_id': ccx_locator})
    context['edit_ccx_dasboard_url'] = reverse(
        'ccx_edit_course_view', kwargs={'course_id': ccx_locator})

    return context
Example #18
0
 def visit(node, depth=1):
     """
     Recursive generator function which yields CCX schedule nodes.
     We convert dates to string to get them ready for use by the js date
     widgets, which use text inputs.
     Visits students visible nodes only; nodes children of hidden ones
     are skipped as well.
     """
     for child in node.get_children():
         # in case the children are visible to staff only, skip them
         if child.visible_to_staff_only:
             continue
         start = get_override_for_ccx(ccx, child, 'start', None)
         if start:
             start = str(start)[:-9]
         due = get_override_for_ccx(ccx, child, 'due', None)
         if due:
             due = str(due)[:-9]
         hidden = get_override_for_ccx(
             ccx, child, 'visible_to_staff_only',
             child.visible_to_staff_only)
         visited = {
             'location': str(child.location),
             'display_name': child.display_name,
             'category': child.category,
             'start': start,
             'due': due,
             'hidden': hidden,
         }
         if depth < 3:
             children = tuple(visit(child, depth + 1))
             if children:
                 visited['children'] = children
                 yield visited
         else:
             yield visited
Example #19
0
    def test_create_ccx(self, ccx_name='New CCX'):
        """
        Create CCX. Follow redirect to coach dashboard, confirm we see
        the coach dashboard for the new CCX.
        """

        self.make_coach()
        url = reverse(
            'create_ccx',
            kwargs={'course_id': unicode(self.course.id)})

        response = self.client.post(url, {'name': ccx_name})
        self.assertEqual(response.status_code, 302)
        url = response.get('location')  # pylint: disable=no-member
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)

        # Get the ccx_key
        path = urlparse.urlparse(url).path
        resolver = resolve(path)
        ccx_key = resolver.kwargs['course_id']

        course_key = CourseKey.from_string(ccx_key)

        self.assertTrue(CourseEnrollment.is_enrolled(self.coach, course_key))
        self.assertTrue(re.search('id="ccx-schedule"', response.content))

        # check if the max amount of student that can be enrolled has been overridden
        ccx = CustomCourseForEdX.objects.get()
        course_enrollments = get_override_for_ccx(ccx, self.course, 'max_student_enrollments_allowed')
        self.assertEqual(course_enrollments, settings.CCX_MAX_STUDENTS_ALLOWED)

        # assert ccx creator has role=ccx_coach
        role = CourseCcxCoachRole(course_key)
        self.assertTrue(role.has_user(self.coach))

        # assert that staff and instructors of master course has staff and instructor roles on ccx
        list_staff_master_course = list_with_level(self.course, 'staff')
        list_instructor_master_course = list_with_level(self.course, 'instructor')

        with ccx_course(course_key) as course_ccx:
            list_staff_ccx_course = list_with_level(course_ccx, 'staff')
            self.assertEqual(len(list_staff_master_course), len(list_staff_ccx_course))
            self.assertEqual(list_staff_master_course[0].email, list_staff_ccx_course[0].email)

            list_instructor_ccx_course = list_with_level(course_ccx, 'instructor')
            self.assertEqual(len(list_instructor_ccx_course), len(list_instructor_master_course))
            self.assertEqual(list_instructor_ccx_course[0].email, list_instructor_master_course[0].email)
Example #20
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",
    )
Example #21
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',
    )
Example #22
0
    def test_edit_schedule(self, today):
        """
        Get CCX schedule, modify it, save it.
        """
        today.return_value = datetime.datetime(2014, 11, 25, tzinfo=pytz.UTC)
        self.make_coach()
        ccx = self.make_ccx()
        url = reverse(
            'ccx_coach_dashboard',
            kwargs={'course_id': CCXLocator.from_course_locator(self.course.id, ccx.id)})
        response = self.client.get(url)
        schedule = json.loads(response.mako_context['schedule'])  # pylint: disable=no-member

        self.assertEqual(len(schedule), 2)
        self.assertEqual(schedule[0]['hidden'], False)
        # If a coach does not override dates, then dates will be imported from master course.
        self.assertEqual(
            schedule[0]['start'],
            self.chapters[0].start.strftime('%Y-%m-%d %H:%M')
        )
        self.assertEqual(
            schedule[0]['children'][0]['start'],
            self.sequentials[0].start.strftime('%Y-%m-%d %H:%M')
        )

        if self.sequentials[0].due:
            expected_due = self.sequentials[0].due.strftime('%Y-%m-%d %H:%M')
        else:
            expected_due = None
        self.assertEqual(schedule[0]['children'][0]['due'], expected_due)

        url = reverse(
            'save_ccx',
            kwargs={'course_id': CCXLocator.from_course_locator(self.course.id, ccx.id)})

        def unhide(unit):
            """
            Recursively unhide a unit and all of its children in the CCX
            schedule.
            """
            unit['hidden'] = False
            for child in unit.get('children', ()):
                unhide(child)

        unhide(schedule[0])
        schedule[0]['start'] = u'2014-11-20 00:00'
        schedule[0]['children'][0]['due'] = u'2014-12-25 00:00'  # what a jerk!
        schedule[0]['children'][0]['children'][0]['start'] = u'2014-12-20 00:00'
        schedule[0]['children'][0]['children'][0]['due'] = u'2014-12-25 00:00'

        response = self.client.post(
            url, json.dumps(schedule), content_type='application/json'
        )

        schedule = json.loads(response.content)['schedule']
        self.assertEqual(schedule[0]['hidden'], False)
        self.assertEqual(schedule[0]['start'], u'2014-11-20 00:00')
        self.assertEqual(
            schedule[0]['children'][0]['due'], u'2014-12-25 00:00'
        )

        self.assertEqual(
            schedule[0]['children'][0]['children'][0]['due'], u'2014-12-25 00:00'
        )
        self.assertEqual(
            schedule[0]['children'][0]['children'][0]['start'], u'2014-12-20 00:00'
        )

        # Make sure start date set on course, follows start date of earliest
        # scheduled chapter
        ccx = CustomCourseForEdX.objects.get()
        course_start = get_override_for_ccx(ccx, self.course, 'start')
        self.assertEqual(str(course_start)[:-9], self.chapters[0].start.strftime('%Y-%m-%d %H:%M'))

        # Make sure grading policy adjusted
        policy = get_override_for_ccx(ccx, self.course, 'grading_policy',
                                      self.course.grading_policy)
        self.assertEqual(policy['GRADER'][0]['type'], 'Homework')
        self.assertEqual(policy['GRADER'][0]['min_count'], 8)
        self.assertEqual(policy['GRADER'][1]['type'], 'Lab')
        self.assertEqual(policy['GRADER'][1]['min_count'], 0)
        self.assertEqual(policy['GRADER'][2]['type'], 'Midterm Exam')
        self.assertEqual(policy['GRADER'][2]['min_count'], 0)
        self.assertEqual(policy['GRADER'][3]['type'], 'Final Exam')
        self.assertEqual(policy['GRADER'][3]['min_count'], 0)
Example #23
0
    def test_edit_schedule(self, today):
        """
        Get CCX schedule, modify it, save it.
        """
        today.return_value = datetime.datetime(2014, 11, 25, tzinfo=UTC)
        self.make_coach()
        ccx = self.make_ccx()
        url = reverse(
            'ccx_coach_dashboard',
            kwargs={'course_id': CCXLocator.from_course_locator(self.course.id, ccx.id)})
        response = self.client.get(url)
        schedule = json.loads(response.mako_context['schedule'])

        assert len(schedule) == 2
        assert schedule[0]['hidden'] is False
        # If a coach does not override dates, then dates will be imported from master course.
        assert schedule[0]['start'] == self.chapters[0].start.strftime('%Y-%m-%d %H:%M')
        assert schedule[0]['children'][0]['start'] == self.sequentials[0].start.strftime('%Y-%m-%d %H:%M')

        if self.sequentials[0].due:
            expected_due = self.sequentials[0].due.strftime('%Y-%m-%d %H:%M')
        else:
            expected_due = None
        assert schedule[0]['children'][0]['due'] == expected_due

        url = reverse(
            'save_ccx',
            kwargs={'course_id': CCXLocator.from_course_locator(self.course.id, ccx.id)})

        unhide(schedule[0])
        schedule[0]['start'] = '2014-11-20 00:00'
        schedule[0]['children'][0]['due'] = '2014-12-25 00:00'  # what a jerk!
        schedule[0]['children'][0]['children'][0]['start'] = '2014-12-20 00:00'
        schedule[0]['children'][0]['children'][0]['due'] = '2014-12-25 00:00'

        response = self.client.post(
            url, json.dumps(schedule), content_type='application/json'
        )

        schedule = json.loads(response.content.decode('utf-8'))['schedule']
        assert schedule[0]['hidden'] is False
        assert schedule[0]['start'] == '2014-11-20 00:00'
        assert schedule[0]['children'][0]['due'] == '2014-12-25 00:00'

        assert schedule[0]['children'][0]['children'][0]['due'] == '2014-12-25 00:00'
        assert schedule[0]['children'][0]['children'][0]['start'] == '2014-12-20 00:00'

        # Make sure start date set on course, follows start date of earliest
        # scheduled chapter
        ccx = CustomCourseForEdX.objects.get()
        course_start = get_override_for_ccx(ccx, self.course, 'start')
        assert str(course_start)[:(- 9)] == self.chapters[0].start.strftime('%Y-%m-%d %H:%M')

        # Make sure grading policy adjusted
        policy = get_override_for_ccx(ccx, self.course, 'grading_policy',
                                      self.course.grading_policy)
        assert policy['GRADER'][0]['type'] == 'Homework'
        assert policy['GRADER'][0]['min_count'] == 8
        assert policy['GRADER'][1]['type'] == 'Lab'
        assert policy['GRADER'][1]['min_count'] == 0
        assert policy['GRADER'][2]['type'] == 'Midterm Exam'
        assert policy['GRADER'][2]['min_count'] == 0
        assert policy['GRADER'][3]['type'] == 'Final Exam'
        assert policy['GRADER'][3]['min_count'] == 0
Example #24
0
    def test_edit_schedule(self, today):
        """
        Get CCX schedule, modify it, save it.
        """
        today.return_value = datetime.datetime(2014, 11, 25, tzinfo=pytz.UTC)
        self.make_coach()
        ccx = self.make_ccx()
        url = reverse(
            'ccx_coach_dashboard',
            kwargs={'course_id': CCXLocator.from_course_locator(self.course.id, ccx.id)})
        response = self.client.get(url)
        schedule = json.loads(response.mako_context['schedule'])  # pylint: disable=no-member

        self.assertEqual(len(schedule), 2)
        self.assertEqual(schedule[0]['hidden'], False)
        # If a coach does not override dates, then dates will be imported from master course.
        self.assertEqual(
            schedule[0]['start'],
            self.chapters[0].start.strftime('%Y-%m-%d %H:%M')
        )
        self.assertEqual(
            schedule[0]['children'][0]['start'],
            self.sequentials[0].start.strftime('%Y-%m-%d %H:%M')
        )

        if self.sequentials[0].due:
            expected_due = self.sequentials[0].due.strftime('%Y-%m-%d %H:%M')
        else:
            expected_due = None
        self.assertEqual(schedule[0]['children'][0]['due'], expected_due)

        url = reverse(
            'save_ccx',
            kwargs={'course_id': CCXLocator.from_course_locator(self.course.id, ccx.id)})

        def unhide(unit):
            """
            Recursively unhide a unit and all of its children in the CCX
            schedule.
            """
            unit['hidden'] = False
            for child in unit.get('children', ()):
                unhide(child)

        unhide(schedule[0])
        schedule[0]['start'] = u'2014-11-20 00:00'
        schedule[0]['children'][0]['due'] = u'2014-12-25 00:00'  # what a jerk!
        schedule[0]['children'][0]['children'][0]['start'] = u'2014-12-20 00:00'
        schedule[0]['children'][0]['children'][0]['due'] = u'2014-12-25 00:00'

        response = self.client.post(
            url, json.dumps(schedule), content_type='application/json'
        )

        schedule = json.loads(response.content)['schedule']
        self.assertEqual(schedule[0]['hidden'], False)
        self.assertEqual(schedule[0]['start'], u'2014-11-20 00:00')
        self.assertEqual(
            schedule[0]['children'][0]['due'], u'2014-12-25 00:00'
        )

        self.assertEqual(
            schedule[0]['children'][0]['children'][0]['due'], u'2014-12-25 00:00'
        )
        self.assertEqual(
            schedule[0]['children'][0]['children'][0]['start'], u'2014-12-20 00:00'
        )

        # Make sure start date set on course, follows start date of earliest
        # scheduled chapter
        ccx = CustomCourseForEdX.objects.get()
        course_start = get_override_for_ccx(ccx, self.course, 'start')
        self.assertEqual(str(course_start)[:-9], self.chapters[0].start.strftime('%Y-%m-%d %H:%M'))

        # Make sure grading policy adjusted
        policy = get_override_for_ccx(ccx, self.course, 'grading_policy',
                                      self.course.grading_policy)
        self.assertEqual(policy['GRADER'][0]['type'], 'Homework')
        self.assertEqual(policy['GRADER'][0]['min_count'], 8)
        self.assertEqual(policy['GRADER'][1]['type'], 'Lab')
        self.assertEqual(policy['GRADER'][1]['min_count'], 0)
        self.assertEqual(policy['GRADER'][2]['type'], 'Midterm Exam')
        self.assertEqual(policy['GRADER'][2]['min_count'], 0)
        self.assertEqual(policy['GRADER'][3]['type'], 'Final Exam')
        self.assertEqual(policy['GRADER'][3]['min_count'], 0)
Example #25
0
    def test_create_ccx(self, ccx_name='New CCX'):
        """
        Create CCX. Follow redirect to coach dashboard, confirm we see
        the coach dashboard for the new CCX.
        """

        self.make_coach()
        url = reverse(
            'create_ccx',
            kwargs={'course_id': str(self.course.id)})

        response = self.client.post(url, {'name': ccx_name})
        assert response.status_code == 302
        url = response.get('location')
        response = self.client.get(url)
        assert response.status_code == 200

        # Get the ccx_key
        path = six.moves.urllib.parse.urlparse(url).path
        resolver = resolve(path)
        ccx_key = resolver.kwargs['course_id']

        course_key = CourseKey.from_string(ccx_key)

        assert CourseEnrollment.is_enrolled(self.coach, course_key)
        assert re.search('id="ccx-schedule"', response.content.decode('utf-8'))

        # check if the max amount of student that can be enrolled has been overridden
        ccx = CustomCourseForEdX.objects.get()
        course_enrollments = get_override_for_ccx(ccx, self.course, 'max_student_enrollments_allowed')
        assert course_enrollments == settings.CCX_MAX_STUDENTS_ALLOWED
        # check if the course display name is properly set
        course_display_name = get_override_for_ccx(ccx, self.course, 'display_name')
        assert course_display_name == ccx_name

        # check if the course display name is properly set in modulestore
        course_display_name = self.mstore.get_course(ccx.locator).display_name
        assert course_display_name == ccx_name

        # assert ccx creator has role=staff
        role = CourseStaffRole(course_key)
        assert role.has_user(self.coach)

        # assert that staff and instructors of master course has staff and instructor roles on ccx
        list_staff_master_course = list_with_level(self.course, 'staff')
        list_instructor_master_course = list_with_level(self.course, 'instructor')

        # assert that forum roles are seeded
        assert are_permissions_roles_seeded(course_key)
        assert has_forum_access(self.coach.username, course_key, FORUM_ROLE_ADMINISTRATOR)

        with ccx_course(course_key) as course_ccx:
            list_staff_ccx_course = list_with_level(course_ccx, 'staff')
            # The "Coach" in the parent course becomes "Staff" on the CCX, so the CCX should have 1 "Staff"
            # user more than the parent course
            assert (len(list_staff_master_course) + 1) == len(list_staff_ccx_course)
            assert list_staff_master_course[0].email in [ccx_staff.email for ccx_staff in list_staff_ccx_course]
            # Make sure the "Coach" on the parent course is "Staff" on the CCX
            assert self.coach in list_staff_ccx_course

            list_instructor_ccx_course = list_with_level(course_ccx, 'instructor')
            assert len(list_instructor_ccx_course) == len(list_instructor_master_course)
            assert list_instructor_ccx_course[0].email == list_instructor_master_course[0].email
Example #26
0
    def test_edit_schedule(self, today):
        """
        Get CCX schedule, modify it, save it.
        """
        today.return_value = datetime.datetime(2014, 11, 25, tzinfo=pytz.UTC)
        self.make_coach()
        ccx = self.make_ccx()
        url = reverse(
            "ccx_coach_dashboard", kwargs={"course_id": CCXLocator.from_course_locator(self.course.id, ccx.id)}
        )
        response = self.client.get(url)
        schedule = json.loads(response.mako_context["schedule"])  # pylint: disable=no-member

        self.assertEqual(len(schedule), 2)
        self.assertEqual(schedule[0]["hidden"], False)
        # If a coach does not override dates, then dates will be imported from master course.
        self.assertEqual(schedule[0]["start"], self.chapters[0].start.strftime("%Y-%m-%d %H:%M"))
        self.assertEqual(schedule[0]["children"][0]["start"], self.sequentials[0].start.strftime("%Y-%m-%d %H:%M"))

        if self.sequentials[0].due:
            expected_due = self.sequentials[0].due.strftime("%Y-%m-%d %H:%M")
        else:
            expected_due = None
        self.assertEqual(schedule[0]["children"][0]["due"], expected_due)

        url = reverse("save_ccx", kwargs={"course_id": CCXLocator.from_course_locator(self.course.id, ccx.id)})

        def unhide(unit):
            """
            Recursively unhide a unit and all of its children in the CCX
            schedule.
            """
            unit["hidden"] = False
            for child in unit.get("children", ()):
                unhide(child)

        unhide(schedule[0])
        schedule[0]["start"] = u"2014-11-20 00:00"
        schedule[0]["children"][0]["due"] = u"2014-12-25 00:00"  # what a jerk!
        schedule[0]["children"][0]["children"][0]["start"] = u"2014-12-20 00:00"
        schedule[0]["children"][0]["children"][0]["due"] = u"2014-12-25 00:00"

        response = self.client.post(url, json.dumps(schedule), content_type="application/json")

        schedule = json.loads(response.content)["schedule"]
        self.assertEqual(schedule[0]["hidden"], False)
        self.assertEqual(schedule[0]["start"], u"2014-11-20 00:00")
        self.assertEqual(schedule[0]["children"][0]["due"], u"2014-12-25 00:00")

        self.assertEqual(schedule[0]["children"][0]["children"][0]["due"], u"2014-12-25 00:00")
        self.assertEqual(schedule[0]["children"][0]["children"][0]["start"], u"2014-12-20 00:00")

        # Make sure start date set on course, follows start date of earliest
        # scheduled chapter
        ccx = CustomCourseForEdX.objects.get()
        course_start = get_override_for_ccx(ccx, self.course, "start")
        self.assertEqual(str(course_start)[:-9], self.chapters[0].start.strftime("%Y-%m-%d %H:%M"))

        # Make sure grading policy adjusted
        policy = get_override_for_ccx(ccx, self.course, "grading_policy", self.course.grading_policy)
        self.assertEqual(policy["GRADER"][0]["type"], "Homework")
        self.assertEqual(policy["GRADER"][0]["min_count"], 8)
        self.assertEqual(policy["GRADER"][1]["type"], "Lab")
        self.assertEqual(policy["GRADER"][1]["min_count"], 0)
        self.assertEqual(policy["GRADER"][2]["type"], "Midterm Exam")
        self.assertEqual(policy["GRADER"][2]["min_count"], 0)
        self.assertEqual(policy["GRADER"][3]["type"], "Final Exam")
        self.assertEqual(policy["GRADER"][3]["min_count"], 0)
def 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',
    )
Example #28
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',
    )