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 #2
0
    def test_ccx_constructor_package_id_branch_and_version_guid(self):
        """Verify a locator constructed with branch and version is correct"""
        test_id_loc = '519665f6223ebd6980884f2b'
        org = 'mit.eecs'
        course = '~6002x'
        run = '2014_T2'
        branch = 'draft-1'
        ccx = '1'
        expected_urn = '{}+{}+{}+{}@{}+{}@{}+{}@{}'.format(
            org, course, run,
            CCXLocator.BRANCH_PREFIX, branch,
            CCXLocator.VERSION_PREFIX, test_id_loc,
            CCXLocator.CCX_PREFIX, ccx
        )
        testobj = CCXLocator(
            org=org,
            course=course,
            run=run,
            branch=branch,
            version_guid=test_id_loc,
            ccx=ccx
        )

        self.check_course_locn_fields(
            testobj,
            org=org,
            course=course,
            run=run,
            branch=branch,
            version_guid=ObjectId(test_id_loc)
        )
        self.assertEqual(testobj.ccx, ccx)
        # Allow access to _to_string
        # pylint: disable=protected-access
        self.assertEqual(testobj._to_string(), expected_urn)
Beispiel #3
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)
Beispiel #4
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 #5
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 #6
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 #7
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 #8
0
    def test_ccx_constructor_package_id_separate_branch(self):
        """Verify a locator constructed with branch is correct"""
        org = 'mit.eecs'
        course = '6002x'
        run = '2014_T2'
        test_branch = 'published'
        ccx = '1'
        expected_urn = '{}+{}+{}+{}@{}+{}@{}'.format(
            org, course, run,
            CCXLocator.BRANCH_PREFIX, test_branch,
            CCXLocator.CCX_PREFIX, ccx
        )
        testobj = CCXLocator(
            org=org, course=course, run=run, branch=test_branch, ccx=ccx
        )

        self.check_course_locn_fields(
            testobj,
            org=org,
            course=course,
            run=run,
            branch=test_branch,
        )
        self.assertEqual(testobj.ccx, ccx)
        # Allow access to _to_string
        # pylint: disable=protected-access
        self.assertEqual(testobj._to_string(), expected_urn)
Beispiel #9
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 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 #11
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 #12
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 #13
0
    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 #14
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 #15
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 #16
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_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 #18
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 #19
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 #20
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 #21
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 #22
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)
Beispiel #23
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_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)
    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):
     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)
Beispiel #28
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
Beispiel #29
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 #30
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 #31
0
    def setUp(self):
        """
        Set up tests
        """
        super().setUp()

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

        # Create CCX
        role = CourseCcxCoachRole(self._course.id)
        role.add_users(coach)
        ccx = CcxFactory(course_id=self._course.id, coach=self.coach)

        # override course grading policy and make last section invisible to students
        override_field_for_ccx(ccx, self._course, 'grading_policy', {
            'GRADER': [
                {'drop_count': 0,
                 'min_count': 2,
                 'short_label': 'HW',
                 'type': 'Homework',
                 'weight': 1}
            ],
            'GRADE_CUTOFFS': {'Pass': 0.75},
        })
        override_field_for_ccx(
            ccx, self.sections[-1], 'visible_to_staff_only', True
        )

        # create a ccx locator and retrieve the course structure using that key
        # which emulates how a student would get access.
        self.ccx_key = CCXLocator.from_course_locator(self._course.id, str(ccx.id))
        self.course = get_course_by_id(self.ccx_key, depth=None)
        CourseOverview.load_from_module_store(self.course.id)
        setup_students_and_grades(self)
        self.client.login(username=coach.username, password="******")
        self.addCleanup(RequestCache.clear_all_namespaces)
        from xmodule.modulestore.django import SignalHandler

        # using CCX object as sender here.
        SignalHandler.course_published.send(
            sender=ccx,
            course_key=self.ccx_key
        )
Beispiel #32
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):
            self.assertContains(response, 'Could not find a user with name or email ', 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 #33
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 #34
0
    def test_from_course_locator_constructor(self, fields):
        available_fields = {
            'version_guid': '519665f6223ebd6980884f2b',
            'org': 'mit.eecs',
            'course': '6002x',
            'run': '2014_T2',
            'branch': 'draft-1',
        }
        ccx = '1'
        use_fields = dict(
            (k, v) for k, v in available_fields.items() if k in fields)
        course_id = CourseLocator(**use_fields)
        testobj = CCXLocator.from_course_locator(course_id, ccx)

        if 'version_guid' in use_fields:
            use_fields['version_guid'] = ObjectId(use_fields['version_guid'])
        self.check_course_locn_fields(testobj, **use_fields)
        self.assertEqual(testobj.ccx, ccx)
    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 #36
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)
    email_students = 'email-students' in request.POST
    course_key = CCXLocator.from_course_locator(course.id, ccx.id)
    email_params = get_email_params(course, auto_enroll=True, course_key=course_key, display_name=ccx.display_name)

    _ccx_students_enrrolling_center(action, identifiers, email_students, course_key, email_params)

    url = reverse('ccx_coach_dashboard', kwargs={'course_id': course_key})
    return redirect(url)
Beispiel #37
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 #38
0
 def test_valid_locations(self, org, course, run, ccx, category, name,
                          revision):  # pylint: disable=unused-argument
     course_key = CCXLocator(org=org,
                             course=course,
                             run=run,
                             branch=revision,
                             ccx=ccx)
     locator = CCXBlockUsageLocator(
         course_key,
         block_type=category,
         block_id=name,
     )
     self.assertEqual(org, locator.org)
     self.assertEqual(course, locator.course)
     self.assertEqual(run, locator.run)
     self.assertEqual(ccx, locator.ccx)
     self.assertEqual(category, locator.block_type)
     self.assertEqual(name, locator.block_id)
     self.assertEqual(revision, locator.branch)
Beispiel #39
0
def save_display_name(apps, schema_editor):
    '''
    Add override for `display_name` for CCX courses that don't have one yet.
    '''
    CcxFieldOverride = apps.get_model('ccx', 'CcxFieldOverride')
    CustomCourseForEdX = apps.get_model('ccx', 'CustomCourseForEdX')

    # Build list of CCX courses that don't have an override for `display_name` yet
    ccx_display_name_present_ids = list(
        CcxFieldOverride.objects.filter(field='display_name').values_list(
            'ccx__id', flat=True))
    ccx_list = CustomCourseForEdX.objects.exclude(
        id__in=ccx_display_name_present_ids)

    # Create `display_name` overrides for these CCX courses
    for ccx in ccx_list:
        try:
            course = get_course_by_id(ccx.course_id, depth=None)
        except Http404:
            log.error(
                "Root course %s not found. Can't create display_name override for %s.",
                ccx.course_id, ccx.display_name)
            continue
        display_name = course.fields['display_name']
        display_name_json = display_name.to_json(ccx.display_name)
        serialized_display_name = json.dumps(display_name_json)

        CcxFieldOverride.objects.get_or_create(
            ccx=ccx,
            location=course.location,
            field='display_name',
            defaults={'value': serialized_display_name},
        )

        # Publish change
        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. Course %s. Receiver: %s. Response: %s',
                ccx.course_id, rec, response)
Beispiel #40
0
    def setUp(self):
        super(TestGetEmailParamsCCX, self).setUp()

        self.course = CourseFactory.create()
        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,
        )
    def get_email_params_ccx(self):
        """
        Returns a dictionary of parameters used to render an email for CCX.
        """
        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)

        email_params = get_email_params(self.course,
                                        True,
                                        course_key=self.course_key,
                                        display_name=self.ccx.display_name)
        email_params["email_address"] = "*****@*****.**"
        email_params["full_name"] = "Jean Reno"

        return email_params
Beispiel #42
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, 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)
Beispiel #43
0
def ccx_students_management(request, course, ccx=None):
    """
    Manage the enrollment of the students in a CCX
    """
    if not ccx:
        raise Http404

    action, identifiers = get_enrollment_action_and_identifiers(request)
    email_students = 'email-students' in request.POST
    course_key = CCXLocator.from_course_locator(course.id, unicode(ccx.id))
    email_params = get_email_params(course, auto_enroll=True, course_key=course_key, display_name=ccx.display_name)

    errors = ccx_students_enrolling_center(action, identifiers, email_students, course_key, email_params, ccx.coach)

    for error_message in errors:
        messages.error(request, error_message)

    url = reverse('ccx_coach_dashboard', kwargs={'course_id': course_key})
    return redirect(url)
Beispiel #44
0
    def test_enroll_member_student(self, view_name, send_email, outbox_count,
                                   student_form_input_name, button_tuple):
        """
        Tests the enrollment 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()
        enrollment = CourseEnrollmentFactory(course_id=self.course.id)
        student = enrollment.user
        outbox = self.get_outbox()
        self.assertEqual(outbox, [])

        url = reverse(view_name,
                      kwargs={
                          'course_id':
                          CCXLocator.from_course_locator(
                              self.course.id, ccx.id)
                      })
        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 exists for this student
        self.assertTrue(
            CourseEnrollment.objects.filter(course_id=self.course.id,
                                            user=student).exists())
Beispiel #45
0
    def test_edit_schedule(self):
        """
        Get CCX schedule, modify it, save it.
        """
        self.make_coach()
        ccx = self.make_ccx()
        ccx_course_key = CCXLocator.from_course_locator(self.course.id, str(ccx.id))
        self.client.login(username=self.coach.username, password="******")

        url = reverse('ccx_coach_dashboard', kwargs={'course_id': ccx_course_key})
        response = self.client.get(url)

        schedule = json.loads(response.mako_context['schedule'])
        assert len(schedule) == 1

        unhide(schedule[0])

        # edit schedule
        date = datetime.datetime.now() - datetime.timedelta(days=5)
        start = date.strftime("%Y-%m-%d %H:%M")
        due = (date + datetime.timedelta(days=3)).strftime("%Y-%m-%d %H:%M")

        schedule[0]['start'] = start
        schedule[0]['children'][0]['start'] = start
        schedule[0]['children'][0]['due'] = due
        schedule[0]['children'][0]['children'][0]['start'] = start
        schedule[0]['children'][0]['children'][0]['due'] = due

        url = reverse('save_ccx', kwargs={'course_id': ccx_course_key})
        response = self.client.post(url, json.dumps(schedule), content_type='application/json')

        assert response.status_code == 200

        schedule = json.loads(response.content.decode('utf-8'))['schedule']
        assert schedule[0]['hidden'] is False
        assert schedule[0]['start'] == start
        assert schedule[0]['children'][0]['start'] == start
        assert schedule[0]['children'][0]['due'] == due
        assert schedule[0]['children'][0]['children'][0]['due'] == due
        assert schedule[0]['children'][0]['children'][0]['start'] == start

        self.assert_progress_summary(ccx_course_key, due)
Beispiel #46
0
def ccx_messages(request, course, ccx=None, **kwargs):
    if not ccx:
        raise Http404

    msgs = CourseUpdates.objects.filter(ccx=ccx)
    ccx_id = unicode(CCXLocator.from_course_locator(course.id,
                                                    unicode(ccx.id)))

    context = {
        'create_message_url':
        reverse('ccx_messages_create', kwargs={'course_id': ccx_id}),
        'delete_message_url':
        'ccx_messages/delete/',
        'messages':
        msgs,
        'course':
        get_course_by_id(ccx.ccx_course_id, depth=2)
    }

    return render_to_response('ccx/ccx_messages_dashboard.html', context)
def remove_affiliate_course_enrollments(sender, instance, **kwargs):  # pylint: disable=unused-argument
    'Remove all privileges over all affiliate courses.'
    for ccx in instance.affiliate.courses:
        ccx_locator = CCXLocator.from_course_locator(ccx.course_id, ccx.id)
        course = get_course_by_id(ccx_locator)

        revoke_access(course, instance.member, instance.role, False)

    # Remove CCX coach on FastTrac course if the user is a staff member in ONLY the affiliate
    # for which the membership has been deleted.
    is_staff_in_other_affiliate = AffiliateMembership.objects.filter(
        member=instance.member, role__in=AffiliateMembership.STAFF_ROLES
    ).exists()
    if instance.role in AffiliateMembership.STAFF_ROLES and not is_staff_in_other_affiliate:
        course_overviews = CourseOverview.objects.exclude(id__startswith='ccx-')
        for course_overview in course_overviews:
            course_id = course_overview.id
            course = get_course_by_id(course_id)

            revoke_access(course, instance.member, AffiliateMembership.CCX_COACH, False)
Beispiel #48
0
    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,
                                                     six.text_type(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_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
Beispiel #50
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, unicode(ccx.id))
    with ccx_course(ccx_key) as course:
        student_info, page = get_grade_book_page(request, course, course_key=ccx_key)

        return render_to_response('courseware/gradebook.html', {
            'page': page,
            'page_url': reverse('ccx_gradebook', kwargs={'course_id': ccx_key}),
            '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 #51
0
def remove_master_course_staff_from_ccx_for_existing_ccx(apps, schema_editor):
    """
    Remove all staff and instructors of master course from respective CCX(s).

    Arguments:
        apps (Applications): Apps in edX platform.
        schema_editor (SchemaEditor): For editing database schema i.e create, delete field (column)

    """
    CustomCourseForEdX = apps.get_model("ccx", "CustomCourseForEdX")
    list_ccx = CustomCourseForEdX.objects.all()
    for ccx in list_ccx:
        if ccx.course_id.deprecated:
            # prevent migration for deprecated course ids.
            continue
        ccx_locator = CCXLocator.from_course_locator(ccx.course_id,
                                                     unicode(ccx.id))
        remove_master_course_staff_from_ccx(get_course_by_id(ccx.course_id),
                                            ccx_locator,
                                            ccx.display_name,
                                            send_email=False)
Beispiel #52
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', '')
    email_students = 'email-students' in request.POST
    identifiers = [student_id]
    course_key = CCXLocator.from_course_locator(course.id, ccx.id)
    email_params = get_email_params(course, auto_enroll=True, course_key=course_key, display_name=ccx.display_name)

    errors = _ccx_students_enrrolling_center(action, identifiers, email_students, course_key, email_params)

    for error_message in errors:
        messages.error(request, error_message)

    url = reverse('ccx_coach_dashboard', kwargs={'course_id': course_key})
    return redirect(url)
Beispiel #53
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 #54
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)
            if action == 'Enroll':
                enroll_email(ccx,
                             email,
                             auto_enroll=auto_enroll,
                             email_students=email_students)
            if action == "Unenroll":
                unenroll_email(ccx, email, email_students=email_students)
        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 #55
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 #56
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-manage-students',
            kwargs={'course_id': ccx_course_key}
        )
        data = {
            'enrollment-button': 'Enroll',
            'student-ids': ','.join([student.email for student in students]),
        }
        response = self.client.post(url, data=data, follow=True)
        assert response.status_code == 200
        # even if course is coach can enroll staff and admins of master course into ccx
        assert CourseEnrollment.objects.filter(course_id=ccx_course_key, user=instructor).exists()
        assert CourseEnrollment.objects.filter(course_id=ccx_course_key, user=staff).exists()
        assert CourseEnrollment.objects.filter(course_id=ccx_course_key, user=self.coach).exists()

        # a CcxMembership exists for the first five students but not the sixth
        assert CourseEnrollment.objects.filter(course_id=ccx_course_key, user=students[3]).exists()
        assert CourseEnrollment.objects.filter(course_id=ccx_course_key, user=students[4]).exists()
        assert not CourseEnrollment.objects.filter(course_id=ccx_course_key, user=students[5]).exists()
Beispiel #57
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})
    return redirect(url)
Beispiel #58
0
    def test_courses_list_with_ccx_courses(self):
        """
        Tests that CCX courses are filtered in course listing.
        """
        # Create a course and assign access roles to user.
        course_location = CourseLocator('Org1', 'Course1', 'Course1')
        course = self._create_course_with_access_groups(
            course_location, self.user)

        # Create a ccx course key and add assign access roles to user.
        ccx_course_key = CCXLocator.from_course_locator(course.id, '1')
        self._add_role_access_to_user(self.user, ccx_course_key)

        # Test that CCX courses are filtered out.
        courses_list, __ = _accessible_courses_list_from_groups(self.request)
        self.assertEqual(len(courses_list), 1)
        self.assertNotIn(ccx_course_key,
                         [course.id for course in courses_list])

        # Get all courses which user has access.
        instructor_courses = UserBasedRole(
            self.user, CourseInstructorRole.ROLE).courses_with_role()
        staff_courses = UserBasedRole(
            self.user, CourseStaffRole.ROLE).courses_with_role()
        all_courses = (instructor_courses | staff_courses)

        # Verify that CCX course exists in access but filtered by `_accessible_courses_list_from_groups`.
        self.assertIn(ccx_course_key,
                      [access.course_id for access in all_courses])

        # Verify that CCX courses are filtered out while iterating over all courses
        mocked_ccx_course = Mock(id=ccx_course_key)
        with patch(
                'openedx.core.djangoapps.content.course_overviews.models.CourseOverview.get_all_courses',
                return_value=[mocked_ccx_course],
        ):
            courses_iter, __ = _accessible_courses_iter_for_tests(self.request)
            self.assertEqual(len(list(courses_iter)), 0)
Beispiel #59
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 #60
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)