Beispiel #1
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)
Beispiel #2
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)
    def test_manage_student_enrollment_limit(self):
        """
        Enroll students up to the enrollment limit.

        This test is specific to one of the enrollment views: the reason is because
        the view used in this test cannot perform bulk enrollments.
        """
        students_limit = 1
        self.make_coach()
        ccx = self.make_ccx(max_students_allowed=students_limit)
        ccx_course_key = CCXLocator.from_course_locator(self.course.id, ccx.id)
        students = [UserFactory.create(is_staff=False) for _ in range(2)]
        url = reverse(
            "ccx_manage_student", kwargs={"course_id": CCXLocator.from_course_locator(self.course.id, ccx.id)}
        )
        # enroll the first student
        data = {"student-action": "add", "student-id": u",".join([students[0].email])}
        response = self.client.post(url, data=data, follow=True)
        self.assertEqual(response.status_code, 200)
        # a CcxMembership exists for this student
        self.assertTrue(CourseEnrollment.objects.filter(course_id=ccx_course_key, user=students[0]).exists())
        # try to enroll the second student without success
        # enroll the first student
        data = {"student-action": "add", "student-id": u",".join([students[1].email])}
        response = self.client.post(url, data=data, follow=True)
        self.assertEqual(response.status_code, 200)
        # a CcxMembership does not exist for this student
        self.assertFalse(CourseEnrollment.objects.filter(course_id=ccx_course_key, user=students[1]).exists())
        error_message = "The course is full: the limit is {students_limit}".format(students_limit=students_limit)
        self.assertContains(response, error_message, status_code=200)
Beispiel #4
0
def get_email_params(ccx, auto_enroll, secure=True):
    """
    get parameters for enrollment emails
    """
    protocol = "https" if secure else "http"

    stripped_site_name = microsite.get_value("SITE_NAME", settings.SITE_NAME)
    registration_url = u"{proto}://{site}{path}".format(
        proto=protocol, site=stripped_site_name, path=reverse("register_user")
    )
    course_url = u"{proto}://{site}{path}".format(
        proto=protocol,
        site=stripped_site_name,
        path=reverse("course_root", kwargs={"course_id": CCXLocator.from_course_locator(ccx.course_id, ccx.id)}),
    )

    course_about_url = None
    if not settings.FEATURES.get("ENABLE_MKTG_SITE", False):
        course_about_url = u"{proto}://{site}{path}".format(
            proto=protocol,
            site=stripped_site_name,
            path=reverse("about_course", kwargs={"course_id": CCXLocator.from_course_locator(ccx.course_id, ccx.id)}),
        )

    email_params = {
        "site_name": stripped_site_name,
        "registration_url": registration_url,
        "course": ccx,
        "auto_enroll": auto_enroll,
        "course_url": course_url,
        "course_about_url": course_about_url,
    }
    return email_params
Beispiel #5
0
    def test_manage_remove_single_student(self):
        """unenroll a single student who is a member of the class already
        """
        self.make_coach()
        ccx = self.make_ccx()
        course_key = CCXLocator.from_course_locator(self.course.id, ccx.id)
        enrollment = CourseEnrollmentFactory(course_id=course_key)
        student = enrollment.user
        # no emails have been sent so far
        outbox = self.get_outbox()
        self.assertEqual(outbox, [])

        url = reverse(
            'ccx_manage_student',
            kwargs={'course_id': CCXLocator.from_course_locator(self.course.id, ccx.id)}
        )
        data = {
            'student-action': 'revoke',
            'student-id': u','.join([student.email, ]),  # pylint: disable=no-member
        }
        response = self.client.post(url, data=data, follow=True)
        self.assertEqual(response.status_code, 200)
        # we were redirected to our current location
        self.assertEqual(len(response.redirect_chain), 1)
        self.assertTrue(302 in response.redirect_chain[0])
        self.assertEqual(outbox, [])
Beispiel #6
0
def ccx_invite(request, course, ccx=None):
    """
    Invite users to new ccx
    """
    if not ccx:
        raise Http404

    action = request.POST.get("enrollment-button")
    identifiers_raw = request.POST.get("student-ids")
    identifiers = _split_input_list(identifiers_raw)
    auto_enroll = True if "auto-enroll" in request.POST else False
    email_students = True if "email-students" in request.POST else False
    for identifier in identifiers:
        user = None
        email = None
        try:
            user = get_student_from_identifier(identifier)
        except User.DoesNotExist:
            email = identifier
        else:
            email = user.email
        try:
            validate_email(email)
            course_key = CCXLocator.from_course_locator(course.id, ccx.id)
            email_params = get_email_params(course, auto_enroll, course_key=course_key, display_name=ccx.display_name)
            if action == "Enroll":
                enroll_email(
                    course_key, email, auto_enroll=auto_enroll, email_students=email_students, email_params=email_params
                )
            if action == "Unenroll":
                unenroll_email(course_key, email, email_students=email_students, email_params=email_params)
        except ValidationError:
            log.info("Invalid user name or email when trying to invite students: %s", email)
    url = reverse("ccx_coach_dashboard", kwargs={"course_id": CCXLocator.from_course_locator(course.id, ccx.id)})
    return redirect(url)
Beispiel #7
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)
def revert_ccx_staff_to_coaches(apps, schema_editor):
    """
    Modify all staff on CCX courses so that they no longer have the staff role
    on the course that they coach.

    Arguments:
        apps (Applications): Apps in edX platform.
        schema_editor (SchemaEditor): For editing database schema (unused)

    """
    CustomCourseForEdX = apps.get_model('ccx', 'CustomCourseForEdX')
    db_alias = schema_editor.connection.alias
    if not db_alias == 'default':
        return
    list_ccx = CustomCourseForEdX.objects.using(db_alias).all()
    for ccx in list_ccx:
        ccx_locator = CCXLocator.from_course_locator(ccx.course_id, six.text_type(ccx.id))
        try:
            course = get_course_by_id(ccx_locator)
        except Http404:
            log.error('Could not migrate access for CCX course: %s', six.text_type(ccx_locator))
        else:
            coach = User.objects.get(id=ccx.coach.id)
            allow_access(course, coach, 'ccx_coach', send_email=False)
            revoke_access(course, coach, 'staff', send_email=False)
            log.info(
                'The CCX coach of CCX %s has been switched from "Staff" to "CCX Coach".',
                six.text_type(ccx_locator)
            )
    def test_unenroll_member_student(self, view_name, send_email, outbox_count, student_form_input_name, button_tuple):
        """
        Tests the unenrollment of a list of students who are members of the class.

        It tests 2 different views that use slightly different parameters,
        but that perform the same task.
        """
        self.make_coach()
        ccx = self.make_ccx()
        course_key = CCXLocator.from_course_locator(self.course.id, ccx.id)
        enrollment = CourseEnrollmentFactory(course_id=course_key)
        student = enrollment.user
        outbox = self.get_outbox()
        self.assertEqual(outbox, [])

        url = reverse(view_name, kwargs={"course_id": course_key})
        data = {
            button_tuple[0]: button_tuple[1],
            student_form_input_name: u",".join([student.email]),  # pylint: disable=no-member
        }
        if send_email:
            data["email-students"] = "Notify-students-by-email"
        response = self.client.post(url, data=data, follow=True)
        self.assertEqual(response.status_code, 200)
        # we were redirected to our current location
        self.assertEqual(len(response.redirect_chain), 1)
        self.assertIn(302, response.redirect_chain[0])
        self.assertEqual(len(outbox), outbox_count)
        if send_email:
            self.assertIn(student.email, outbox[0].recipients())  # pylint: disable=no-member
        # a CcxMembership does not exists for this student
        self.assertFalse(CourseEnrollment.objects.filter(course_id=self.course.id, user=student).exists())
Beispiel #10
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)
Beispiel #11
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)
 def test_ccx_locator(self):
     """verify that the ccx is retuned if using a ccx locator
     """
     ccx = CcxFactory(course_id=self.course.id, coach=self.coach)
     course_key = CCXLocator.from_course_locator(self.course.id, ccx.id)
     result = self.call_fut(course_key)
     self.assertEqual(result, ccx)
Beispiel #13
0
 def setUp(self):
     super(TestRenderMessageToString, self).setUp()
     coach = AdminFactory.create()
     role = CourseCcxCoachRole(self.course.id)
     role.add_users(coach)
     self.ccx = CcxFactory(course_id=self.course.id, coach=coach)
     self.course_key = CCXLocator.from_course_locator(self.course.id, self.ccx.id)
 def test_course_structure_generated(self):
     """Check that course structure is generated after course published signal is sent
     """
     ccx_structure = {
         u"blocks": {
             u"ccx-block-v1:edX+999+Run_666+ccx@1+type@course+block@course": {
                 u"block_type": u"course",
                 u"graded": False,
                 u"format": None,
                 u"usage_key": u"ccx-block-v1:edX+999+Run_666+ccx@1+type@course+block@course",
                 u"children": [],
                 u"display_name": u"Run 666",
             }
         },
         u"root": u"ccx-block-v1:edX+999+Run_666+ccx@1+type@course+block@course",
     }
     course_key = CCXLocator.from_course_locator(self.course.id, self.ccx.id)
     structure = CourseStructure.objects.filter(course_id=course_key)
     # no structure exists before signal is called
     self.assertEqual(len(structure), 0)
     with mock_signal_receiver(SignalHandler.course_published) as receiver:
         self.call_fut(self.course.id)
         self.assertEqual(receiver.call_count, 3)
         structure = CourseStructure.objects.get(course_id=course_key)
         self.assertEqual(structure.structure, ccx_structure)
Beispiel #15
0
    def test_unenroll_non_user_student(
            self, view_name, send_email, outbox_count, student_form_input_name, button_tuple, identifier):
        """
        Unenroll a list of students who are not users yet
        """
        self.make_coach()
        course = CourseFactory.create()
        ccx = self.make_ccx()
        course_key = CCXLocator.from_course_locator(course.id, ccx.id)
        outbox = self.get_outbox()
        CourseEnrollmentAllowed(course_id=course_key, email=identifier)
        self.assertEqual(outbox, [])

        url = reverse(
            view_name,
            kwargs={'course_id': course_key}
        )
        data = {
            button_tuple[0]: button_tuple[1],
            student_form_input_name: u','.join([identifier, ]),
        }
        if send_email:
            data['email-students'] = 'Notify-students-by-email'
        response = self.client.post(url, data=data, follow=True)
        self.assertEqual(response.status_code, 200)
        # we were redirected to our current location
        self.assertEqual(len(response.redirect_chain), 1)
        self.assertIn(302, response.redirect_chain[0])
        self.assertEqual(len(outbox), outbox_count)
        self.assertFalse(
            CourseEnrollmentAllowed.objects.filter(
                course_id=course_key, email=identifier
            ).exists()
        )
Beispiel #16
0
 def test_get_ccx_schedule(self, today):
     """
     Gets CCX schedule and checks number of blocks in it.
     Hides nodes at a different depth and checks that these nodes
     are not in the schedule.
     """
     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)
         }
     )
     # all the elements are visible
     self.assert_elements_in_schedule(url)
     # hide a vertical
     vertical = self.verticals[0]
     self.hide_node(vertical)
     locations = self.assert_elements_in_schedule(url, n_verticals=7)
     self.assertNotIn(unicode(vertical.location), locations)
     # hide a sequential
     sequential = self.sequentials[0]
     self.hide_node(sequential)
     locations = self.assert_elements_in_schedule(url, n_sequentials=3, n_verticals=6)
     self.assertNotIn(unicode(sequential.location), locations)
     # hide a chapter
     chapter = self.chapters[0]
     self.hide_node(chapter)
     locations = self.assert_elements_in_schedule(url, n_chapters=1, n_sequentials=2, n_verticals=4)
     self.assertNotIn(unicode(chapter.location), locations)
Beispiel #17
0
    def test_unenroll_member_student(self):
        """unenroll a list of students who are members of the class
        """
        self.make_coach()
        ccx = self.make_ccx()
        course_key = CCXLocator.from_course_locator(self.course.id, ccx.id)
        enrollment = CourseEnrollmentFactory(course_id=course_key)
        student = enrollment.user
        outbox = self.get_outbox()
        self.assertEqual(outbox, [])

        url = reverse(
            'ccx_invite',
            kwargs={'course_id': course_key}
        )
        data = {
            'enrollment-button': 'Unenroll',
            'student-ids': u','.join([student.email, ]),  # pylint: disable=no-member
            'email-students': 'Notify-students-by-email',
        }
        response = self.client.post(url, data=data, follow=True)
        self.assertEqual(response.status_code, 200)
        # we were redirected to our current location
        self.assertEqual(len(response.redirect_chain), 1)
        self.assertTrue(302 in response.redirect_chain[0])
        self.assertEqual(len(outbox), 1)
        self.assertTrue(student.email in outbox[0].recipients())  # pylint: disable=no-member
def change_existing_ccx_coaches_to_staff(apps, schema_editor):
    """
    Modify all coaches of CCX courses so that they have the staff role on the
    CCX course they coach, but retain the CCX Coach role on the parent course.

    Arguments:
        apps (Applications): Apps in edX platform.
        schema_editor (SchemaEditor): For editing database schema (unused)

    """
    CustomCourseForEdX = apps.get_model('ccx', 'CustomCourseForEdX')
    db_alias = schema_editor.connection.alias
    if not db_alias == 'default':
        # This migration is not intended to run against the student_module_history database and
        # will fail if it does. Ensure that it'll only run against the default database.
        return
    list_ccx = CustomCourseForEdX.objects.using(db_alias).all()
    for ccx in list_ccx:
        ccx_locator = CCXLocator.from_course_locator(ccx.course_id, six.text_type(ccx.id))
        try:
            course = get_course_by_id(ccx_locator)
        except Http404:
            log.error('Could not migrate access for CCX course: %s', six.text_type(ccx_locator))
        else:
            coach = User.objects.get(id=ccx.coach.id)
            allow_access(course, coach, 'staff', send_email=False)
            revoke_access(course, coach, 'ccx_coach', send_email=False)
            log.info(
                'The CCX coach of CCX %s has been switched from "CCX Coach" to "Staff".',
                six.text_type(ccx_locator)
            )
Beispiel #19
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)
Beispiel #20
0
    def test_unenroll_non_user_student(self):
        """unenroll a list of students who are not users yet
        """
        test_email = "*****@*****.**"
        self.make_coach()
        course = CourseFactory.create()
        ccx = self.make_ccx()
        course_key = CCXLocator.from_course_locator(course.id, ccx.id)
        outbox = self.get_outbox()
        CourseEnrollmentAllowed(course_id=course_key, email=test_email)
        self.assertEqual(outbox, [])

        url = reverse(
            'ccx_invite',
            kwargs={'course_id': course_key}
        )
        data = {
            'enrollment-button': 'Unenroll',
            'student-ids': u','.join([test_email, ]),
            'email-students': 'Notify-students-by-email',
        }
        response = self.client.post(url, data=data, follow=True)
        self.assertEqual(response.status_code, 200)
        # we were redirected to our current location
        self.assertEqual(len(response.redirect_chain), 1)
        self.assertTrue(302 in response.redirect_chain[0])
        self.assertFalse(
            CourseEnrollmentAllowed.objects.filter(
                course_id=course_key, email=test_email
            ).exists()
        )
Beispiel #21
0
def ccx_student_management(request, course, ccx=None):
    """Manage the enrollment of individual students in a CCX
    """
    if not ccx:
        raise Http404

    action = request.POST.get('student-action', None)
    student_id = request.POST.get('student-id', '')
    user = email = None
    try:
        user = get_student_from_identifier(student_id)
    except User.DoesNotExist:
        email = student_id
    else:
        email = user.email

    course_key = CCXLocator.from_course_locator(course.id, ccx.id)
    try:
        validate_email(email)
        if action == 'add':
            # by decree, no emails sent to students added this way
            # by decree, any students added this way are auto_enrolled
            enroll_email(course_key, email, auto_enroll=True, email_students=False)
        elif action == 'revoke':
            unenroll_email(course_key, email, email_students=False)
    except ValidationError:
        log.info('Invalid user name or email when trying to enroll student: %s', email)

    url = reverse(
        'ccx_coach_dashboard',
        kwargs={'course_id': course_key}
    )
    return redirect(url)
Beispiel #22
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
Beispiel #23
0
    def test_manage_add_single_invalid_student(self, student_id):
        """enroll a single non valid student
        """
        self.make_coach()
        ccx = self.make_ccx()
        course_key = CCXLocator.from_course_locator(self.course.id, ccx.id)
        url = reverse(
            'ccx_manage_student',
            kwargs={'course_id': course_key}
        )
        redirect_url = reverse(
            'ccx_coach_dashboard',
            kwargs={'course_id': course_key}
        )
        data = {
            'student-action': 'add',
            'student-id': u','.join([student_id, ]),  # pylint: disable=no-member
        }
        response = self.client.post(url, data=data, follow=True)

        error_message = 'Could not find a user with name or email "{student_id}" '.format(
            student_id=student_id
        )
        self.assertContains(response, error_message, status_code=200)

        # we were redirected to our current location
        self.assertRedirects(response, redirect_url, status_code=302)
Beispiel #24
0
def ccx_gradebook(request, course, ccx=None):
    """
    Show the gradebook for this CCX.
    """
    if not ccx:
        raise Http404

    ccx_key = CCXLocator.from_course_locator(course.id, ccx.id)
    with ccx_course(ccx_key) as course:
        prep_course_for_grading(course, request)

        enrolled_students = User.objects.filter(
            courseenrollment__course_id=ccx_key,
            courseenrollment__is_active=1
        ).order_by('username').select_related("profile")

        student_info = [
            {
                'username': student.username,
                'id': student.id,
                'email': student.email,
                'grade_summary': student_grades(student, request, course),
                'realname': student.profile.name,
            }
            for student in enrolled_students
        ]

        return render_to_response('courseware/gradebook.html', {
            'students': student_info,
            'course': course,
            'course_id': course.id,
            'staff_access': request.user.is_staff,
            'ordered_grades': sorted(
                course.grade_cutoffs.items(), key=lambda i: i[1], reverse=True),
        })
Beispiel #25
0
 def test_post_list(self):
     """
     Test the creation of a CCX
     """
     outbox = self.get_outbox()
     data = {
         'master_course_id': self.master_course_key_str,
         'max_students_allowed': 111,
         'display_name': 'CCX Test Title',
         'coach_email': self.coach.email
     }
     resp = self.client.post(self.list_url, data, format='json', HTTP_AUTHORIZATION=self.auth)
     self.assertEqual(resp.status_code, status.HTTP_201_CREATED)
     # check if the response has at least the same data of the request
     for key, val in data.iteritems():
         self.assertEqual(resp.data.get(key), val)  # pylint: disable=no-member
     self.assertIn('ccx_course_id', resp.data)  # pylint: disable=no-member
     # check that the new CCX actually exists
     course_key = CourseKey.from_string(resp.data.get('ccx_course_id'))  # pylint: disable=no-member
     ccx_course = CustomCourseForEdX.objects.get(pk=course_key.ccx)
     self.assertEqual(
         unicode(CCXLocator.from_course_locator(ccx_course.course.id, ccx_course.id)),
         resp.data.get('ccx_course_id')  # pylint: disable=no-member
     )
     # check that the coach user has coach role on the master course
     coach_role_on_master_course = CourseCcxCoachRole(self.master_course_key)
     self.assertTrue(coach_role_on_master_course.has_user(self.coach))
     # check that the coach has been enrolled in the ccx
     ccx_course_object = courses.get_course_by_id(course_key)
     self.assertTrue(
         CourseEnrollment.objects.filter(course_id=ccx_course_object.id, user=self.coach).exists()
     )
     # check that an email has been sent to the coach
     self.assertEqual(len(outbox), 1)
     self.assertIn(self.coach.email, outbox[0].recipients())  # pylint: disable=no-member
    def test_enroll_non_user_student(self):
        """enroll a list of students who are not users yet
        """
        test_email = "*****@*****.**"
        self.make_coach()
        ccx = self.make_ccx()
        outbox = self.get_outbox()
        self.assertEqual(outbox, [])

        url = reverse(
            'ccx_invite',
            kwargs={'course_id': CCXLocator.from_course_locator(self.course.id, ccx.id)}
        )
        data = {
            'enrollment-button': 'Enroll',
            'student-ids': u','.join([test_email, ]),
            'email-students': 'Notify-students-by-email',
        }
        response = self.client.post(url, data=data, follow=True)
        self.assertEqual(response.status_code, 200)
        # we were redirected to our current location
        self.assertEqual(len(response.redirect_chain), 1)
        self.assertTrue(302 in response.redirect_chain[0])
        self.assertEqual(len(outbox), 1)
        self.assertTrue(test_email in outbox[0].recipients())
        self.assertTrue(
            CcxFutureMembership.objects.filter(
                ccx=ccx, email=test_email
            ).exists()
        )
Beispiel #27
0
    def setUp(self):
        """
        Set up tests
        """
        super(TestFieldOverrides, self).setUp()

        self.ccx = ccx = CustomCourseForEdX(
            course_id=self.course.id,
            display_name='Test CCX',
            coach=AdminFactory.create())
        ccx.save()

        patch = mock.patch('ccx.overrides.get_current_ccx')
        self.get_ccx = get_ccx = patch.start()
        get_ccx.return_value = ccx
        self.addCleanup(patch.stop)

        self.addCleanup(RequestCache.clear_request_cache)

        inject_field_overrides(iter_blocks(ccx.course), self.course, AdminFactory.create())

        self.ccx_key = CCXLocator.from_course_locator(self.course.id, ccx.id)
        self.ccx_course = get_course_by_id(self.ccx_key, depth=None)

        def cleanup_provider_classes():
            """
            After everything is done, clean up by un-doing the change to the
            OverrideFieldData object that is done during the wrap method.
            """
            OverrideFieldData.provider_classes = None
        self.addCleanup(cleanup_provider_classes)
    def test_ccx_invite_enroll_up_to_limit(self):
        """
        Enrolls a list of students up to the enrollment limit.

        This test is specific to one of the enrollment views: the reason is because
        the view used in this test can perform bulk enrollments.
        """
        self.make_coach()
        # create ccx and limit the maximum amount of students that can be enrolled to 2
        ccx = self.make_ccx(max_students_allowed=2)
        ccx_course_key = CCXLocator.from_course_locator(self.course.id, ccx.id)
        # create some users
        students = [
            UserFactory.create(is_staff=False) for _ in range(3)
        ]
        url = reverse(
            'ccx_invite',
            kwargs={'course_id': ccx_course_key}
        )
        data = {
            'enrollment-button': 'Enroll',
            'student-ids': u','.join([student.email for student in students]),
        }
        response = self.client.post(url, data=data, follow=True)
        self.assertEqual(response.status_code, 200)
        # a CcxMembership exists for the first two students but not the third
        self.assertTrue(
            CourseEnrollment.objects.filter(course_id=ccx_course_key, user=students[0]).exists()
        )
        self.assertTrue(
            CourseEnrollment.objects.filter(course_id=ccx_course_key, user=students[1]).exists()
        )
        self.assertFalse(
            CourseEnrollment.objects.filter(course_id=ccx_course_key, user=students[2]).exists()
        )
 def test_signal_not_sent_for_ccx(self):
     """Check that course published signal is not sent when course key is for a ccx
     """
     course_key = CCXLocator.from_course_locator(self.course.id, self.ccx.id)
     with mock_signal_receiver(SignalHandler.course_published) as receiver:
         self.call_fut(course_key)
         self.assertEqual(receiver.call_count, 0)
Beispiel #30
0
def ccx_student_management(request, course, ccx=None):
    """Manage the enrollment of individual students in a CCX
    """
    if not ccx:
        raise Http404

    action = request.POST.get("student-action", None)
    student_id = request.POST.get("student-id", "")
    user = email = None
    error_message = ""
    course_key = CCXLocator.from_course_locator(course.id, ccx.id)
    try:
        user = get_student_from_identifier(student_id)
    except User.DoesNotExist:
        email = student_id
        error_message = validate_student_email(email)
        if email and not error_message:
            error_message = _('Could not find a user with name or email "{email}" ').format(email=email)
    else:
        email = user.email
        error_message = validate_student_email(email)

    if error_message is None:
        if action == "add":
            # by decree, no emails sent to students added this way
            # by decree, any students added this way are auto_enrolled
            enroll_email(course_key, email, auto_enroll=True, email_students=False)
        elif action == "revoke":
            unenroll_email(course_key, email, email_students=False)
    else:
        messages.error(request, error_message)

    url = reverse("ccx_coach_dashboard", kwargs={"course_id": course_key})
    return redirect(url)
Beispiel #31
0
    def grade_course(self, course, view_as_ccx):
        """
        Renders the progress page for the given course.
        """
        course_key = course.id
        if view_as_ccx:
            course_key = CCXLocator.from_course_locator(course_key, self.ccx.id)

        return progress(
            self.request,
            course_id=unicode(course_key),
            student_id=self.student.id
        )
 def test_course_overview_cached(self):
     """
     Check that course overview is cached after course published signal is sent
     """
     course_key = CCXLocator.from_course_locator(self.course.id,
                                                 self.ccx.id)
     overview = CourseOverview.objects.filter(id=course_key)
     self.assertEqual(len(overview), 0)
     with mock_signal_receiver(SignalHandler.course_published) as receiver:
         self.call_fut(self.course.id)
         self.assertEqual(receiver.call_count, 3)
         overview = CourseOverview.objects.filter(id=course_key)
         self.assertEqual(len(overview), 1)
Beispiel #33
0
 def test_not_a_coach(self):
     """
     User is not a coach, should get Forbidden response.
     """
     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)
     self.assertEqual(response.status_code, 403)
    def setUp(self):
        """
        Set up tests
        """
        super(TestCCXModulestoreWrapper, self).setUp()
        self.ccx = ccx = CustomCourseForEdX(
            course_id=self.course.id,
            display_name='Test CCX',
            coach=self.coach
        )
        ccx.save()

        self.ccx_locator = CCXLocator.from_course_locator(self.course.id, ccx.id)
Beispiel #35
0
    def setUp(self):
        super(TestStaffOnCCX, self).setUp()

        # Create instructor account
        self.client.login(username=self.coach.username, password="******")

        # create an instance of modulestore
        self.mstore = modulestore()

        self.make_coach()
        self.ccx = self.make_ccx()
        self.ccx_locator = CCXLocator.from_course_locator(
            self.course.id, self.ccx.id)
Beispiel #36
0
def get_email_params(ccx, auto_enroll, secure=True):
    """
    get parameters for enrollment emails
    """
    protocol = 'https' if secure else 'http'

    stripped_site_name = microsite.get_value('SITE_NAME', settings.SITE_NAME)
    registration_url = u'{proto}://{site}{path}'.format(
        proto=protocol, site=stripped_site_name, path=reverse('register_user'))
    course_url = u'{proto}://{site}{path}'.format(
        proto=protocol,
        site=stripped_site_name,
        path=reverse('course_root',
                     kwargs={
                         'course_id':
                         CCXLocator.from_course_locator(ccx.course_id, ccx.id)
                     }))

    course_about_url = None
    if not settings.FEATURES.get('ENABLE_MKTG_SITE', False):
        course_about_url = u'{proto}://{site}{path}'.format(
            proto=protocol,
            site=stripped_site_name,
            path=reverse('about_course',
                         kwargs={
                             'course_id':
                             CCXLocator.from_course_locator(
                                 ccx.course_id, ccx.id)
                         }))

    email_params = {
        'site_name': stripped_site_name,
        'registration_url': registration_url,
        'course': ccx,
        'auto_enroll': auto_enroll,
        'course_url': course_url,
        'course_about_url': course_about_url,
    }
    return email_params
Beispiel #37
0
 def test_instructor_access_coach_dashboard(self):
     """
     User is instructor, should access coach dashboard.
     """
     instructor = self.make_instructor()
     self.client.login(username=instructor.username, password="******")
     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)
     self.assertEqual(response.status_code, 200)
Beispiel #38
0
 def setUp(self):
     """
     Set up tests
     """
     super(CcxDetailTest, self).setUp()
     self.make_coach()
     # create a ccx
     self.ccx = self.make_ccx(max_students_allowed=123)
     self.ccx_key = CCXLocator.from_course_locator(self.ccx.course.id,
                                                   self.ccx.id)
     self.ccx_key_str = unicode(self.ccx_key)
     self.detail_url = reverse('ccx_api:v0:ccx:detail',
                               kwargs={'ccx_course_id': self.ccx_key_str})
Beispiel #39
0
 def test_forbidden_user_access_coach_dashboard(self):
     """
     Assert user with no access must not see dashboard.
     """
     user = UserFactory.create(password="******")
     self.client.login(username=user.username, password="******")
     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)
     self.assertEqual(response.status_code, 403)
    def setup_course(self, size, enable_ccx, view_as_ccx):
        """
        Build a gradable course where each node has `size` children.
        """
        grading_policy = {
            "GRADER": [{
                "drop_count": 2,
                "min_count": 12,
                "short_label": "HW",
                "type": "Homework",
                "weight": 0.15
            }, {
                "drop_count": 2,
                "min_count": 12,
                "type": "Lab",
                "weight": 0.15
            }, {
                "drop_count": 0,
                "min_count": 1,
                "short_label": "Midterm",
                "type": "Midterm Exam",
                "weight": 0.3
            }, {
                "drop_count": 0,
                "min_count": 1,
                "short_label": "Final",
                "type": "Final Exam",
                "weight": 0.4
            }],
            "GRADE_CUTOFFS": {
                "Pass": 0.5
            }
        }

        self.course = CourseFactory.create(
            graded=True,
            start=datetime.now(UTC),
            grading_policy=grading_policy,
            enable_ccx=enable_ccx,
        )
        self.populate_course(size)

        course_key = self.course.id
        if enable_ccx:
            self.ccx = CcxFactory.create(course_id=self.course.id)
            if view_as_ccx:
                course_key = CCXLocator.from_course_locator(
                    self.course.id, self.ccx.id)

        CourseEnrollment.enroll(self.student, course_key)
        return CourseKey.from_string(unicode(course_key))
Beispiel #41
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)
Beispiel #42
0
    def test_ccx_invite_enroll_up_to_limit(self):
        """
        Enrolls a list of students up to the enrollment limit.

        This test is specific to one of the enrollment views: the reason is because
        the view used in this test can perform bulk enrollments.
        """
        self.make_coach()
        # create ccx and limit the maximum amount of students that can be enrolled to 2
        ccx = self.make_ccx(max_students_allowed=2)
        ccx_course_key = CCXLocator.from_course_locator(self.course.id, ccx.id)
        staff = self.make_staff()
        instructor = self.make_instructor()

        # create some users
        students = [instructor, staff, self.coach] + [
            UserFactory.create(is_staff=False) for _ in range(3)
        ]

        url = reverse(
            'ccx_invite',
            kwargs={'course_id': ccx_course_key}
        )
        data = {
            'enrollment-button': 'Enroll',
            'student-ids': u','.join([student.email for student in students]),
        }
        response = self.client.post(url, data=data, follow=True)
        self.assertEqual(response.status_code, 200)
        # even if course is coach can enroll staff and admins of master course into ccx
        self.assertTrue(
            CourseEnrollment.objects.filter(course_id=ccx_course_key, user=instructor).exists()
        )
        self.assertTrue(
            CourseEnrollment.objects.filter(course_id=ccx_course_key, user=staff).exists()
        )
        self.assertTrue(
            CourseEnrollment.objects.filter(course_id=ccx_course_key, user=self.coach).exists()
        )

        # a CcxMembership exists for the first five students but not the sixth
        self.assertTrue(
            CourseEnrollment.objects.filter(course_id=ccx_course_key, user=students[3]).exists()
        )
        self.assertTrue(
            CourseEnrollment.objects.filter(course_id=ccx_course_key, user=students[4]).exists()
        )
        self.assertFalse(
            CourseEnrollment.objects.filter(course_id=ccx_course_key, user=students[5]).exists()
        )
Beispiel #43
0
def ccx_grades_csv(request, course, ccx=None):
    """
    Download grades as CSV.
    """
    if not ccx:
        raise Http404

    ccx_key = CCXLocator.from_course_locator(course.id, ccx.id)
    with ccx_course(ccx_key) as course:
        prep_course_for_grading(course, request)

        enrolled_students = User.objects.filter(
            courseenrollment__course_id=ccx_key,
            courseenrollment__is_active=1).order_by('username').select_related(
                "profile")
        grades = iterate_grades_for(course, enrolled_students)

        header = None
        rows = []
        for student, gradeset, __ in grades:
            if gradeset:
                # We were able to successfully grade this student for this
                # course.
                if not header:
                    # Encode the header row in utf-8 encoding in case there are
                    # unicode characters
                    header = [
                        section['label'].encode('utf-8')
                        for section in gradeset[u'section_breakdown']
                    ]
                    rows.append(["id", "email", "username", "grade"] + header)

                percents = {
                    section['label']: section.get('percent', 0.0)
                    for section in gradeset[u'section_breakdown']
                    if 'label' in section
                }

                row_percents = [percents.get(label, 0.0) for label in header]
                rows.append([
                    student.id, student.email, student.username,
                    gradeset['percent']
                ] + row_percents)

        buf = StringIO()
        writer = csv.writer(buf)
        for row in rows:
            writer.writerow(row)

        return HttpResponse(buf.getvalue(), content_type='text/plain')
    def test_redirect_to_dashboard_unenrolled_ccx(self):
        """
        Assert that when unenroll student tries to access ccx do not allow him self-register.
        Redirect him to his student dashboard
        """
        # create ccx
        ccx = CcxFactory(course_id=self.course.id, coach=self.coach)
        ccx_locator = CCXLocator.from_course_locator(self.course.id, unicode(ccx.id))

        self.setup_user()
        url = reverse('info', args=[ccx_locator])
        response = self.client.get(url)
        expected = reverse('dashboard')
        self.assertRedirects(response, expected, status_code=302, target_status_code=200)
Beispiel #45
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)
Beispiel #46
0
def ccx_grades_csv(request, course, ccx=None):
    """
    Download grades as CSV.
    """
    if not ccx:
        raise Http404

    ccx_key = CCXLocator.from_course_locator(course.id, six.text_type(ccx.id))
    with ccx_course(ccx_key) as course:

        enrolled_students = User.objects.filter(
            courseenrollment__course_id=ccx_key,
            courseenrollment__is_active=1
        ).order_by('username').select_related("profile")
        grades = CourseGradeFactory().iter(enrolled_students, course)

        header = None
        rows = []
        for student, course_grade, __ in grades:
            if course_grade:
                # We were able to successfully grade this student for this
                # course.
                if not header:
                    # Encode the header row in utf-8 encoding in case there are
                    # unicode characters
                    header = [section['label'].encode('utf-8') if six.PY2 else section['label']
                              for section in course_grade.summary[u'section_breakdown']]
                    rows.append(["id", "email", "username", "grade"] + header)

                percents = {
                    section['label']: section.get('percent', 0.0)
                    for section in course_grade.summary[u'section_breakdown']
                    if 'label' in section
                }

                row_percents = [percents.get(label, 0.0) for label in header]
                rows.append([student.id, student.email.encode('utf-8'),
                             student.username.encode('utf-8'),
                             course_grade.percent] + row_percents)

        buf = StringIO()
        writer = csv.writer(buf)
        for row in rows:
            writer.writerow(row)

        response = HttpResponse(buf.getvalue(), content_type='text/csv')
        response['Content-Disposition'] = 'attachment'

        return response
Beispiel #47
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
    def test_redirect_to_dashboard_unenrolled_ccx(self):
        """
        Assert that when unenrolled user tries to access CCX do not allow the user to self-register.
        Redirect them to their student dashboard
        """

        # create ccx
        ccx = CcxFactory(course_id=self.course.id, coach=self.coach)
        ccx_locator = CCXLocator.from_course_locator(self.course.id, str(ccx.id))

        self.setup_user()
        url = reverse('openedx.course_experience.course_home', args=[ccx_locator])
        response = self.client.get(url)
        expected = reverse('dashboard')
        self.assertRedirects(response, expected, status_code=302, target_status_code=200)
Beispiel #49
0
    def test_not_a_coach(self):
        """
        User is not a coach, should get Forbidden response.
        """
        self.make_coach()
        ccx = self.make_ccx()

        # create session of non-coach user
        user = UserFactory.create(password="******")
        self.client.login(username=user.username, password="******")
        url = reverse(
            'ccx_coach_dashboard',
            kwargs={'course_id': CCXLocator.from_course_locator(self.course.id, ccx.id)})
        response = self.client.get(url)
        self.assertEqual(response.status_code, 403)
Beispiel #50
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)
Beispiel #51
0
    def setUp(self):
        super(TestGetEmailParamsCCX, self).setUp()
        self.coach = AdminFactory.create()
        role = CourseCcxCoachRole(self.course.id)
        role.add_users(self.coach)
        self.ccx = CcxFactory(course_id=self.course.id, coach=self.coach)
        self.course_key = CCXLocator.from_course_locator(
            self.course.id, self.ccx.id)

        # Explicitly construct what we expect the course URLs to be
        site = settings.SITE_NAME
        self.course_url = u'https://{}/courses/{}/'.format(
            site, self.course_key)
        self.course_about_url = self.course_url + 'about'
        self.registration_url = u'https://{}/register'.format(site)
Beispiel #52
0
def restore_ccx(val, ccx_id):
    """restore references to a CCX to the incoming value

    returns the value converted to a CCX-aware state, using the provided ccx_id
    """
    if isinstance(val, CourseLocator):
        return CCXLocator.from_course_locator(val, ccx_id)
    elif isinstance(val, BlockUsageLocator):
        ccx_key = restore_ccx(val.course_key, ccx_id)
        val = CCXBlockUsageLocator(ccx_key, val.block_type, val.block_id)
    if hasattr(val, 'location'):
        val.location = restore_ccx(val.location, ccx_id)
    if hasattr(val, 'children'):
        val.children = restore_ccx_collection(val.children, ccx_id)
    return val
Beispiel #53
0
    def make_ccx(self):
        """
        create ccx
        """
        ccx = CustomCourseForEdX(course_id=self.course.id,
                                 coach=self.coach,
                                 display_name="Test CCX")
        ccx.save()

        ccx_locator = CCXLocator.from_course_locator(self.course.id,
                                                     unicode(ccx.id))
        role = CourseCcxCoachRole(ccx_locator)
        role.add_users(self.coach)
        CourseEnrollment.enroll(self.coach, ccx_locator)
        return ccx_locator
Beispiel #54
0
    def test_enroll_non_user_student(
            self, view_name, send_email, outbox_count, student_form_input_name, button_tuple, identifier):
        """
        Tests the enrollment of a list of students who are not users yet.

        It tests 2 different views that use slightly different parameters,
        but that perform the same task.
        """
        self.make_coach()
        ccx = self.make_ccx()
        course_key = CCXLocator.from_course_locator(self.course.id, ccx.id)
        outbox = self.get_outbox()
        self.assertEqual(outbox, [])

        url = reverse(
            view_name,
            kwargs={'course_id': course_key}
        )
        data = {
            button_tuple[0]: button_tuple[1],
            student_form_input_name: u','.join([identifier, ]),
        }
        if send_email:
            data['email-students'] = 'Notify-students-by-email'
        response = self.client.post(url, data=data, follow=True)
        self.assertEqual(response.status_code, 200)
        # we were redirected to our current location
        self.assertEqual(len(response.redirect_chain), 1)
        self.assertIn(302, response.redirect_chain[0])
        self.assertEqual(len(outbox), outbox_count)

        # some error messages are returned for one of the views only
        if view_name == 'ccx_manage_student' and not is_email(identifier):
            error_message = 'Could not find a user with name or email "{identifier}" '.format(
                identifier=identifier
            )
            self.assertContains(response, error_message, status_code=200)

        if is_email(identifier):
            if send_email:
                self.assertIn(identifier, outbox[0].recipients())
            self.assertTrue(
                CourseEnrollmentAllowed.objects.filter(course_id=course_key, email=identifier).exists()
            )
        else:
            self.assertFalse(
                CourseEnrollmentAllowed.objects.filter(course_id=course_key, email=identifier).exists()
            )
Beispiel #55
0
def restore_ccx(val, ccx_id):
    """restore references to a CCX to the incoming value

    returns the value converted to a CCX-aware state, using the provided ccx_id
    """
    if isinstance(val, CourseLocator):
        return CCXLocator.from_course_locator(val, ccx_id)
    elif isinstance(val, BlockUsageLocator):
        ccx_key = restore_ccx(val.course_key, ccx_id)
        val = CCXBlockUsageLocator(ccx_key, val.block_type, val.block_id)
    for field_name in XMODULE_FIELDS_WITH_USAGE_KEYS:
        if hasattr(val, field_name):
            setattr(val, field_name, restore_ccx(getattr(val, field_name), ccx_id))
    if hasattr(val, 'children'):
        val.children = restore_ccx_collection(val.children, ccx_id)
    return val
Beispiel #56
0
def ccx_messages_create(request, course, ccx=None, **kwargs):
    if not ccx:
        raise Http404

    post_data = request.POST.copy().dict()

    ccx_message = CourseUpdates(date=post_data['date'],
                                content=post_data['content'],
                                author=request.user,
                                ccx=ccx)
    ccx_message.save()

    ccx_id = unicode(CCXLocator.from_course_locator(course.id,
                                                    unicode(ccx.id)))

    return redirect(reverse('ccx_messages', kwargs={'course_id': ccx_id}))
Beispiel #57
0
    def test_has_staff_access_to_preview_mode(self):
        """
        Tests users have right access to content in preview mode.
        """
        course_key = self.course.id
        usage_key = self.course.scope_ids.usage_id
        chapter = ItemFactory.create(category="chapter",
                                     parent_location=self.course.location)
        overview = CourseOverview.get_from_id(course_key)
        test_system = get_test_system()

        ccx = CcxFactory(course_id=course_key)
        ccx_locator = CCXLocator.from_course_locator(course_key, ccx.id)

        error_descriptor = ErrorDescriptor.from_xml(
            u"<problem>ABC \N{SNOWMAN}</problem>", test_system,
            CourseLocationManager(course_key), "error msg")
        # Enroll student to the course
        CourseEnrollmentFactory(user=self.student, course_id=self.course.id)

        modules = [
            self.course,
            overview,
            chapter,
            ccx_locator,
            error_descriptor,
            course_key,
            usage_key,
        ]
        # Course key is not None
        self.assertTrue(
            bool(
                access.has_staff_access_to_preview_mode(
                    self.global_staff, obj=self.course,
                    course_key=course_key)))

        for user in [
                self.global_staff, self.course_staff, self.course_instructor
        ]:
            for obj in modules:
                self.assertTrue(
                    bool(access.has_staff_access_to_preview_mode(user,
                                                                 obj=obj)))
                self.assertFalse(
                    bool(
                        access.has_staff_access_to_preview_mode(self.student,
                                                                obj=obj)))
Beispiel #58
0
def send_ccx_course_published(course_key):
    """
    Find all CCX derived from this course, and send course published event for them.
    """
    course_key = CourseLocator.from_string(course_key)
    for ccx in CustomCourseForEdX.objects.filter(course_id=course_key):
        try:
            ccx_key = CCXLocator.from_course_locator(course_key, str(ccx.id))
        except InvalidKeyError:
            log.info('Attempt to publish course with deprecated id. Course: %s. CCX: %s', course_key, ccx.id)
            continue
        responses = SignalHandler.course_published.send(
            sender=ccx,
            course_key=ccx_key
        )
        for rec, response in responses:
            log.info('Signal fired when course is published. Receiver: %s. Response: %s', rec, response)
 def forwards(self, orm):
     "Convert CCX Memberships to Course Enrollments."
     from ccx_keys.locator import CCXLocator
     memberships = orm['ccx.CcxMembership'].objects.select_related(
         'ccx', 'student').all()
     for membership in memberships:
         ccx = membership.ccx
         try:
             course_key = CCXLocator.from_course_locator(
                 ccx.course_id, ccx.id)
             enrollment, created = orm[
                 'student.CourseEnrollment'].objects.get_or_create(
                     user=membership.student,
                     course_id=course_key,
                 )
         except InvalidKeyError:
             membership.delete()
Beispiel #60
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)