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)
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)
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 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)
def set_grading_policy(request, course, ccx=None): """ Set grading policy for the CCX. """ if not ccx: raise Http404 override_field_for_ccx(ccx, course, 'grading_policy', json.loads(request.POST['policy'])) # using CCX object as sender here. responses = SignalHandler.course_published.send( sender=ccx, course_key=CCXLocator.from_course_locator(course.id, unicode(ccx.id))) for rec, response in responses: log.info( 'Signal fired when course is published. Receiver: %s. Response: %s', rec, response) url = reverse( 'ccx_coach_dashboard', kwargs={ 'course_id': CCXLocator.from_course_locator( course.id, unicode(ccx.id)) }) return redirect(url)
def 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, [])
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
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)
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)
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)
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() )
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())
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() )
def setUp(self): """ Set up tests """ super(TestCCXGrades, self).setUp() # Create instructor account self.coach = coach = AdminFactory.create() self.client.login(username=coach.username, password="******") # Create CCX role = CourseCcxCoachRole(self._course.id) role.add_users(coach) ccx = CcxFactory(course_id=self._course.id, coach=self.coach) # override course grading policy and make last section invisible to students override_field_for_ccx( ccx, self._course, "grading_policy", { "GRADER": [{"drop_count": 0, "min_count": 2, "short_label": "HW", "type": "Homework", "weight": 1}], "GRADE_CUTOFFS": {"Pass": 0.75}, }, ) override_field_for_ccx(ccx, self.sections[-1], "visible_to_staff_only", True) # create a ccx locator and retrieve the course structure using that key # which emulates how a student would get access. self.ccx_key = CCXLocator.from_course_locator(self._course.id, ccx.id) self.course = get_course_by_id(self.ccx_key, depth=None) setup_students_and_grades(self) self.client.login(username=coach.username, password="******") self.addCleanup(RequestCache.clear_request_cache)
def 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)
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)
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)
def make_ccx(self, max_students_allowed=200): """ Overridden method to replicate (part of) the actual creation of ccx courses """ ccx = super(CcxDetailTest, self).make_ccx(max_students_allowed=max_students_allowed) today = datetime.datetime.today() start = today.replace(tzinfo=pytz.UTC) override_field_for_ccx(ccx, self.course, 'start', start) override_field_for_ccx(ccx, self.course, 'due', None) # Hide anything that can show up in the schedule hidden = 'visible_to_staff_only' for chapter in self.course.get_children(): override_field_for_ccx(ccx, chapter, hidden, True) for sequential in chapter.get_children(): override_field_for_ccx(ccx, sequential, hidden, True) for vertical in sequential.get_children(): override_field_for_ccx(ccx, vertical, hidden, True) # enroll the coach in the CCX ccx_course_key = CCXLocator.from_course_locator(self.course.id, ccx.id) email_params = get_email_params( self.course, auto_enroll=True, course_key=ccx_course_key, display_name=ccx.display_name ) enroll_email( course_id=ccx_course_key, student_email=self.coach.email, auto_enroll=True, email_students=False, email_params=email_params, ) return ccx
def 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), })
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_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() )
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_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 setUp(self): """ Set up courses and enrollments. """ super(TestStudentViewsWithCCX, self).setUp() # Create a Draft Mongo and a Split Mongo course and enroll a student user in them. self.student_password = "******" self.student = UserFactory.create(username="******", password=self.student_password, is_staff=False) self.draft_course = SampleCourseFactory.create(default_store=ModuleStoreEnum.Type.mongo) self.split_course = SampleCourseFactory.create(default_store=ModuleStoreEnum.Type.split) CourseEnrollment.enroll(self.student, self.draft_course.id) CourseEnrollment.enroll(self.student, self.split_course.id) # Create a CCX coach. self.coach = AdminFactory.create() role = CourseCcxCoachRole(self.split_course.id) role.add_users(self.coach) # Create a CCX course and enroll the user in it. self.ccx = CcxFactory(course_id=self.split_course.id, coach=self.coach) last_week = datetime.datetime.now(UTC()) - datetime.timedelta(days=7) override_field_for_ccx(self.ccx, self.split_course, 'start', last_week) # Required by self.ccx.has_started(). self.ccx_course_key = CCXLocator.from_course_locator(self.split_course.id, self.ccx.id) CourseEnrollment.enroll(self.student, self.ccx_course_key)
def 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)
def setUp(self): """ Set up tests """ super().setUp() # Create instructor account self.coach = coach = AdminFactory.create() self.client.login(username=coach.username, password="******") # Create CCX role = CourseCcxCoachRole(self._course.id) role.add_users(coach) ccx = CcxFactory(course_id=self._course.id, coach=self.coach) # override course grading policy and make last section invisible to students override_field_for_ccx(ccx, self._course, 'grading_policy', { 'GRADER': [ {'drop_count': 0, 'min_count': 2, 'short_label': 'HW', 'type': 'Homework', 'weight': 1} ], 'GRADE_CUTOFFS': {'Pass': 0.75}, }) override_field_for_ccx( ccx, self.sections[-1], 'visible_to_staff_only', True ) # create a ccx locator and retrieve the course structure using that key # which emulates how a student would get access. self.ccx_key = CCXLocator.from_course_locator(self._course.id, str(ccx.id)) self.course = get_course_by_id(self.ccx_key, depth=None) CourseOverview.load_from_module_store(self.course.id) setup_students_and_grades(self) self.client.login(username=coach.username, password="******") self.addCleanup(RequestCache.clear_all_namespaces) from xmodule.modulestore.django import SignalHandler # using CCX object as sender here. SignalHandler.course_published.send( sender=ccx, course_key=self.ccx_key )
def 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() )
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)
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)
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)
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), })
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)
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)
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
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)
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)
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())
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)
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)
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)
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 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), })
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)
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)
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)))
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)
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)
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()
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)
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)
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)
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)