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 dashboard(request, course, ccx=None): """ Display the CCX Coach Dashboard. """ # right now, we can only have one ccx per user and course # so, if no ccx is passed in, we can sefely redirect to that if ccx is None: ccx = get_ccx_for_coach(course, request.user) if ccx: url = reverse( "ccx_coach_dashboard", kwargs={"course_id": CCXLocator.from_course_locator(course.id, ccx.id)} ) return redirect(url) context = {"course": course, "ccx": ccx} if ccx: ccx_locator = CCXLocator.from_course_locator(course.id, ccx.id) schedule = get_ccx_schedule(course, ccx) grading_policy = get_override_for_ccx(ccx, course, "grading_policy", course.grading_policy) context["schedule"] = json.dumps(schedule, indent=4) context["save_url"] = reverse("save_ccx", kwargs={"course_id": ccx_locator}) context["ccx_members"] = CourseEnrollment.objects.filter(course_id=ccx_locator, is_active=True) context["gradebook_url"] = reverse("ccx_gradebook", kwargs={"course_id": ccx_locator}) context["grades_csv_url"] = reverse("ccx_grades_csv", kwargs={"course_id": ccx_locator}) context["grading_policy"] = json.dumps(grading_policy, indent=4) context["grading_policy_url"] = reverse("ccx_set_grading_policy", kwargs={"course_id": ccx_locator}) else: context["create_ccx_url"] = reverse("create_ccx", kwargs={"course_id": course.id}) return render_to_response("ccx/coach_dashboard.html", context)
def test_manage_student_enrollment_limit(self): """ Enroll students up to the enrollment limit. This test is specific to one of the enrollment views: the reason is because the view used in this test cannot perform bulk enrollments. """ students_limit = 1 self.make_coach() ccx = self.make_ccx(max_students_allowed=students_limit) ccx_course_key = CCXLocator.from_course_locator(self.course.id, ccx.id) students = [UserFactory.create(is_staff=False) for _ in range(2)] url = reverse( "ccx_manage_student", kwargs={"course_id": CCXLocator.from_course_locator(self.course.id, ccx.id)} ) # enroll the first student data = {"student-action": "add", "student-id": u",".join([students[0].email])} response = self.client.post(url, data=data, follow=True) self.assertEqual(response.status_code, 200) # a CcxMembership exists for this student self.assertTrue(CourseEnrollment.objects.filter(course_id=ccx_course_key, user=students[0]).exists()) # try to enroll the second student without success # enroll the first student data = {"student-action": "add", "student-id": u",".join([students[1].email])} response = self.client.post(url, data=data, follow=True) self.assertEqual(response.status_code, 200) # a CcxMembership does not exist for this student self.assertFalse(CourseEnrollment.objects.filter(course_id=ccx_course_key, user=students[1]).exists()) error_message = "The course is full: the limit is {students_limit}".format(students_limit=students_limit) self.assertContains(response, error_message, status_code=200)
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_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 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 dashboard(request, course, ccx=None): """ Display the CCX Coach Dashboard. """ # right now, we can only have one ccx per user and course # so, if no ccx is passed in, we can sefely redirect to that if ccx is None: ccx = get_ccx_for_coach(course, request.user) if ccx: url = reverse( 'ccx_coach_dashboard', kwargs={ 'course_id': CCXLocator.from_course_locator(course.id, unicode(ccx.id)) }) return redirect(url) context = { 'course': course, 'ccx': ccx, } context.update(get_ccx_creation_dict(course)) if ccx: ccx_locator = CCXLocator.from_course_locator(course.id, unicode(ccx.id)) # At this point we are done with verification that current user is ccx coach. assign_staff_role_to_ccx(ccx_locator, request.user, course.id) schedule = get_ccx_schedule(course, ccx) grading_policy = get_override_for_ccx(ccx, course, 'grading_policy', course.grading_policy) context['schedule'] = json.dumps(schedule, indent=4) context['save_url'] = reverse( 'save_ccx', kwargs={'course_id': ccx_locator}) context['ccx_members'] = CourseEnrollment.objects.filter( course_id=ccx_locator, is_active=True) context['gradebook_url'] = reverse( 'ccx_gradebook', kwargs={'course_id': ccx_locator}) context['grades_csv_url'] = reverse( 'ccx_grades_csv', kwargs={'course_id': ccx_locator}) context['grading_policy'] = json.dumps(grading_policy, indent=4) context['grading_policy_url'] = reverse( 'ccx_set_grading_policy', kwargs={'course_id': ccx_locator}) with ccx_course(ccx_locator) as course: context['course'] = course else: context['create_ccx_url'] = reverse( 'create_ccx', kwargs={'course_id': course.id}) return render_to_response('ccx/coach_dashboard.html', context)
def revert_ccx_staff_to_coaches(apps, schema_editor): """ Modify all staff on CCX courses so that they no longer have the staff role on the course that they coach. Arguments: apps (Applications): Apps in edX platform. schema_editor (SchemaEditor): For editing database schema (unused) """ CustomCourseForEdX = apps.get_model('ccx', 'CustomCourseForEdX') db_alias = schema_editor.connection.alias if not db_alias == 'default': return list_ccx = CustomCourseForEdX.objects.using(db_alias).all() for ccx in list_ccx: ccx_locator = CCXLocator.from_course_locator(ccx.course_id, six.text_type(ccx.id)) try: course = get_course_by_id(ccx_locator) except Http404: log.error('Could not migrate access for CCX course: %s', six.text_type(ccx_locator)) else: coach = User.objects.get(id=ccx.coach.id) allow_access(course, coach, 'ccx_coach', send_email=False) revoke_access(course, coach, 'staff', send_email=False) log.info( 'The CCX coach of CCX %s has been switched from "Staff" to "CCX Coach".', six.text_type(ccx_locator) )
def test_unenroll_member_student(self, view_name, send_email, outbox_count, student_form_input_name, button_tuple): """ Tests the unenrollment of a list of students who are members of the class. It tests 2 different views that use slightly different parameters, but that perform the same task. """ self.make_coach() ccx = self.make_ccx() course_key = CCXLocator.from_course_locator(self.course.id, ccx.id) enrollment = CourseEnrollmentFactory(course_id=course_key) student = enrollment.user outbox = self.get_outbox() self.assertEqual(outbox, []) url = reverse(view_name, kwargs={"course_id": course_key}) data = { button_tuple[0]: button_tuple[1], student_form_input_name: u",".join([student.email]), # pylint: disable=no-member } if send_email: data["email-students"] = "Notify-students-by-email" response = self.client.post(url, data=data, follow=True) self.assertEqual(response.status_code, 200) # we were redirected to our current location self.assertEqual(len(response.redirect_chain), 1) self.assertIn(302, response.redirect_chain[0]) self.assertEqual(len(outbox), outbox_count) if send_email: self.assertIn(student.email, outbox[0].recipients()) # pylint: disable=no-member # a CcxMembership does not exists for this student self.assertFalse(CourseEnrollment.objects.filter(course_id=self.course.id, user=student).exists())
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_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 setUp(self): super(TestRenderMessageToString, self).setUp() coach = AdminFactory.create() role = CourseCcxCoachRole(self.course.id) role.add_users(coach) self.ccx = CcxFactory(course_id=self.course.id, coach=coach) self.course_key = CCXLocator.from_course_locator(self.course.id, self.ccx.id)
def test_course_structure_generated(self): """Check that course structure is generated after course published signal is sent """ ccx_structure = { u"blocks": { u"ccx-block-v1:edX+999+Run_666+ccx@1+type@course+block@course": { u"block_type": u"course", u"graded": False, u"format": None, u"usage_key": u"ccx-block-v1:edX+999+Run_666+ccx@1+type@course+block@course", u"children": [], u"display_name": u"Run 666", } }, u"root": u"ccx-block-v1:edX+999+Run_666+ccx@1+type@course+block@course", } course_key = CCXLocator.from_course_locator(self.course.id, self.ccx.id) structure = CourseStructure.objects.filter(course_id=course_key) # no structure exists before signal is called self.assertEqual(len(structure), 0) with mock_signal_receiver(SignalHandler.course_published) as receiver: self.call_fut(self.course.id) self.assertEqual(receiver.call_count, 3) structure = CourseStructure.objects.get(course_id=course_key) self.assertEqual(structure.structure, ccx_structure)
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 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 test_unenroll_member_student(self): """unenroll a list of students who are members of the class """ self.make_coach() ccx = self.make_ccx() course_key = CCXLocator.from_course_locator(self.course.id, ccx.id) enrollment = CourseEnrollmentFactory(course_id=course_key) student = enrollment.user outbox = self.get_outbox() self.assertEqual(outbox, []) url = reverse( 'ccx_invite', kwargs={'course_id': course_key} ) data = { 'enrollment-button': 'Unenroll', 'student-ids': u','.join([student.email, ]), # pylint: disable=no-member 'email-students': 'Notify-students-by-email', } response = self.client.post(url, data=data, follow=True) self.assertEqual(response.status_code, 200) # we were redirected to our current location self.assertEqual(len(response.redirect_chain), 1) self.assertTrue(302 in response.redirect_chain[0]) self.assertEqual(len(outbox), 1) self.assertTrue(student.email in outbox[0].recipients()) # pylint: disable=no-member
def change_existing_ccx_coaches_to_staff(apps, schema_editor): """ Modify all coaches of CCX courses so that they have the staff role on the CCX course they coach, but retain the CCX Coach role on the parent course. Arguments: apps (Applications): Apps in edX platform. schema_editor (SchemaEditor): For editing database schema (unused) """ CustomCourseForEdX = apps.get_model('ccx', 'CustomCourseForEdX') db_alias = schema_editor.connection.alias if not db_alias == 'default': # This migration is not intended to run against the student_module_history database and # will fail if it does. Ensure that it'll only run against the default database. return list_ccx = CustomCourseForEdX.objects.using(db_alias).all() for ccx in list_ccx: ccx_locator = CCXLocator.from_course_locator(ccx.course_id, six.text_type(ccx.id)) try: course = get_course_by_id(ccx_locator) except Http404: log.error('Could not migrate access for CCX course: %s', six.text_type(ccx_locator)) else: coach = User.objects.get(id=ccx.coach.id) allow_access(course, coach, 'staff', send_email=False) revoke_access(course, coach, 'ccx_coach', send_email=False) log.info( 'The CCX coach of CCX %s has been switched from "CCX Coach" to "Staff".', six.text_type(ccx_locator) )
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_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 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 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_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_post_list(self): """ Test the creation of a CCX """ outbox = self.get_outbox() data = { 'master_course_id': self.master_course_key_str, 'max_students_allowed': 111, 'display_name': 'CCX Test Title', 'coach_email': self.coach.email } resp = self.client.post(self.list_url, data, format='json', HTTP_AUTHORIZATION=self.auth) self.assertEqual(resp.status_code, status.HTTP_201_CREATED) # check if the response has at least the same data of the request for key, val in data.iteritems(): self.assertEqual(resp.data.get(key), val) # pylint: disable=no-member self.assertIn('ccx_course_id', resp.data) # pylint: disable=no-member # check that the new CCX actually exists course_key = CourseKey.from_string(resp.data.get('ccx_course_id')) # pylint: disable=no-member ccx_course = CustomCourseForEdX.objects.get(pk=course_key.ccx) self.assertEqual( unicode(CCXLocator.from_course_locator(ccx_course.course.id, ccx_course.id)), resp.data.get('ccx_course_id') # pylint: disable=no-member ) # check that the coach user has coach role on the master course coach_role_on_master_course = CourseCcxCoachRole(self.master_course_key) self.assertTrue(coach_role_on_master_course.has_user(self.coach)) # check that the coach has been enrolled in the ccx ccx_course_object = courses.get_course_by_id(course_key) self.assertTrue( CourseEnrollment.objects.filter(course_id=ccx_course_object.id, user=self.coach).exists() ) # check that an email has been sent to the coach self.assertEqual(len(outbox), 1) self.assertIn(self.coach.email, outbox[0].recipients()) # pylint: disable=no-member
def test_enroll_non_user_student(self): """enroll a list of students who are not users yet """ test_email = "*****@*****.**" self.make_coach() ccx = self.make_ccx() outbox = self.get_outbox() self.assertEqual(outbox, []) url = reverse( 'ccx_invite', kwargs={'course_id': CCXLocator.from_course_locator(self.course.id, ccx.id)} ) data = { 'enrollment-button': 'Enroll', 'student-ids': u','.join([test_email, ]), 'email-students': 'Notify-students-by-email', } response = self.client.post(url, data=data, follow=True) self.assertEqual(response.status_code, 200) # we were redirected to our current location self.assertEqual(len(response.redirect_chain), 1) self.assertTrue(302 in response.redirect_chain[0]) self.assertEqual(len(outbox), 1) self.assertTrue(test_email in outbox[0].recipients()) self.assertTrue( CcxFutureMembership.objects.filter( ccx=ccx, email=test_email ).exists() )
def setUp(self): """ Set up tests """ super(TestFieldOverrides, self).setUp() self.ccx = ccx = CustomCourseForEdX( course_id=self.course.id, display_name='Test CCX', coach=AdminFactory.create()) ccx.save() patch = mock.patch('ccx.overrides.get_current_ccx') self.get_ccx = get_ccx = patch.start() get_ccx.return_value = ccx self.addCleanup(patch.stop) self.addCleanup(RequestCache.clear_request_cache) inject_field_overrides(iter_blocks(ccx.course), self.course, AdminFactory.create()) self.ccx_key = CCXLocator.from_course_locator(self.course.id, ccx.id) self.ccx_course = get_course_by_id(self.ccx_key, depth=None) def cleanup_provider_classes(): """ After everything is done, clean up by un-doing the change to the OverrideFieldData object that is done during the wrap method. """ OverrideFieldData.provider_classes = None self.addCleanup(cleanup_provider_classes)
def test_ccx_invite_enroll_up_to_limit(self): """ Enrolls a list of students up to the enrollment limit. This test is specific to one of the enrollment views: the reason is because the view used in this test can perform bulk enrollments. """ self.make_coach() # create ccx and limit the maximum amount of students that can be enrolled to 2 ccx = self.make_ccx(max_students_allowed=2) ccx_course_key = CCXLocator.from_course_locator(self.course.id, ccx.id) # create some users students = [ UserFactory.create(is_staff=False) for _ in range(3) ] url = reverse( 'ccx_invite', kwargs={'course_id': ccx_course_key} ) data = { 'enrollment-button': 'Enroll', 'student-ids': u','.join([student.email for student in students]), } response = self.client.post(url, data=data, follow=True) self.assertEqual(response.status_code, 200) # a CcxMembership exists for the first two students but not the third self.assertTrue( CourseEnrollment.objects.filter(course_id=ccx_course_key, user=students[0]).exists() ) self.assertTrue( CourseEnrollment.objects.filter(course_id=ccx_course_key, user=students[1]).exists() ) self.assertFalse( CourseEnrollment.objects.filter(course_id=ccx_course_key, user=students[2]).exists() )
def test_signal_not_sent_for_ccx(self): """Check that course published signal is not sent when course key is for a ccx """ course_key = CCXLocator.from_course_locator(self.course.id, self.ccx.id) with mock_signal_receiver(SignalHandler.course_published) as receiver: self.call_fut(course_key) self.assertEqual(receiver.call_count, 0)
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 grade_course(self, course, view_as_ccx): """ Renders the progress page for the given course. """ course_key = course.id if view_as_ccx: course_key = CCXLocator.from_course_locator(course_key, self.ccx.id) return progress( self.request, course_id=unicode(course_key), student_id=self.student.id )
def test_course_overview_cached(self): """ Check that course overview is cached after course published signal is sent """ course_key = CCXLocator.from_course_locator(self.course.id, self.ccx.id) overview = CourseOverview.objects.filter(id=course_key) self.assertEqual(len(overview), 0) with mock_signal_receiver(SignalHandler.course_published) as receiver: self.call_fut(self.course.id) self.assertEqual(receiver.call_count, 3) overview = CourseOverview.objects.filter(id=course_key) self.assertEqual(len(overview), 1)
def test_not_a_coach(self): """ User is not a coach, should get Forbidden response. """ ccx = self.make_ccx() url = reverse('ccx_coach_dashboard', kwargs={ 'course_id': CCXLocator.from_course_locator( self.course.id, ccx.id) }) response = self.client.get(url) self.assertEqual(response.status_code, 403)
def setUp(self): """ Set up tests """ super(TestCCXModulestoreWrapper, self).setUp() self.ccx = ccx = CustomCourseForEdX( course_id=self.course.id, display_name='Test CCX', coach=self.coach ) ccx.save() self.ccx_locator = CCXLocator.from_course_locator(self.course.id, ccx.id)
def setUp(self): super(TestStaffOnCCX, self).setUp() # Create instructor account self.client.login(username=self.coach.username, password="******") # create an instance of modulestore self.mstore = modulestore() self.make_coach() self.ccx = self.make_ccx() self.ccx_locator = CCXLocator.from_course_locator( self.course.id, self.ccx.id)
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_instructor_access_coach_dashboard(self): """ User is instructor, should access coach dashboard. """ instructor = self.make_instructor() self.client.login(username=instructor.username, password="******") self.make_coach() ccx = self.make_ccx() url = reverse( 'ccx_coach_dashboard', kwargs={'course_id': CCXLocator.from_course_locator(self.course.id, ccx.id)}) response = self.client.get(url) self.assertEqual(response.status_code, 200)
def setUp(self): """ Set up tests """ super(CcxDetailTest, self).setUp() self.make_coach() # create a ccx self.ccx = self.make_ccx(max_students_allowed=123) self.ccx_key = CCXLocator.from_course_locator(self.ccx.course.id, self.ccx.id) self.ccx_key_str = unicode(self.ccx_key) self.detail_url = reverse('ccx_api:v0:ccx:detail', kwargs={'ccx_course_id': self.ccx_key_str})
def test_forbidden_user_access_coach_dashboard(self): """ Assert user with no access must not see dashboard. """ user = UserFactory.create(password="******") self.client.login(username=user.username, password="******") self.make_coach() ccx = self.make_ccx() url = reverse( 'ccx_coach_dashboard', kwargs={'course_id': CCXLocator.from_course_locator(self.course.id, ccx.id)}) response = self.client.get(url) self.assertEqual(response.status_code, 403)
def setup_course(self, size, enable_ccx, view_as_ccx): """ Build a gradable course where each node has `size` children. """ grading_policy = { "GRADER": [{ "drop_count": 2, "min_count": 12, "short_label": "HW", "type": "Homework", "weight": 0.15 }, { "drop_count": 2, "min_count": 12, "type": "Lab", "weight": 0.15 }, { "drop_count": 0, "min_count": 1, "short_label": "Midterm", "type": "Midterm Exam", "weight": 0.3 }, { "drop_count": 0, "min_count": 1, "short_label": "Final", "type": "Final Exam", "weight": 0.4 }], "GRADE_CUTOFFS": { "Pass": 0.5 } } self.course = CourseFactory.create( graded=True, start=datetime.now(UTC), grading_policy=grading_policy, enable_ccx=enable_ccx, ) self.populate_course(size) course_key = self.course.id if enable_ccx: self.ccx = CcxFactory.create(course_id=self.course.id) if view_as_ccx: course_key = CCXLocator.from_course_locator( self.course.id, self.ccx.id) CourseEnrollment.enroll(self.student, course_key) return CourseKey.from_string(unicode(course_key))
def create_ccx(request, course, ccx=None): """ Create a new CCX """ name = request.POST.get('name') # prevent CCX objects from being created for deprecated course ids. if course.id.deprecated: messages.error(request, _( "You cannot create a CCX from a course using a deprecated id. " "Please create a rerun of this course in the studio to allow " "this action.")) url = reverse('ccx_coach_dashboard', kwargs={'course_id': course.id}) return redirect(url) ccx = CustomCourseForEdX( course_id=course.id, coach=request.user, display_name=name) ccx.save() # Make sure start/due are overridden for entire course start = TODAY().replace(tzinfo=pytz.UTC) override_field_for_ccx(ccx, course, 'start', start) override_field_for_ccx(ccx, course, 'due', None) # Hide anything that can show up in the schedule hidden = 'visible_to_staff_only' for chapter in course.get_children(): override_field_for_ccx(ccx, chapter, hidden, True) for sequential in chapter.get_children(): override_field_for_ccx(ccx, sequential, hidden, True) for vertical in sequential.get_children(): override_field_for_ccx(ccx, vertical, hidden, True) ccx_id = CCXLocator.from_course_locator(course.id, ccx.id) # pylint: disable=no-member url = reverse('ccx_coach_dashboard', kwargs={'course_id': ccx_id}) # Enroll the coach in the course email_params = get_email_params(course, auto_enroll=True, course_key=ccx_id, display_name=ccx.display_name) enroll_email( course_id=ccx_id, student_email=request.user.email, auto_enroll=True, email_students=True, email_params=email_params, ) return redirect(url)
def test_ccx_invite_enroll_up_to_limit(self): """ Enrolls a list of students up to the enrollment limit. This test is specific to one of the enrollment views: the reason is because the view used in this test can perform bulk enrollments. """ self.make_coach() # create ccx and limit the maximum amount of students that can be enrolled to 2 ccx = self.make_ccx(max_students_allowed=2) ccx_course_key = CCXLocator.from_course_locator(self.course.id, ccx.id) staff = self.make_staff() instructor = self.make_instructor() # create some users students = [instructor, staff, self.coach] + [ UserFactory.create(is_staff=False) for _ in range(3) ] url = reverse( 'ccx_invite', kwargs={'course_id': ccx_course_key} ) data = { 'enrollment-button': 'Enroll', 'student-ids': u','.join([student.email for student in students]), } response = self.client.post(url, data=data, follow=True) self.assertEqual(response.status_code, 200) # even if course is coach can enroll staff and admins of master course into ccx self.assertTrue( CourseEnrollment.objects.filter(course_id=ccx_course_key, user=instructor).exists() ) self.assertTrue( CourseEnrollment.objects.filter(course_id=ccx_course_key, user=staff).exists() ) self.assertTrue( CourseEnrollment.objects.filter(course_id=ccx_course_key, user=self.coach).exists() ) # a CcxMembership exists for the first five students but not the sixth self.assertTrue( CourseEnrollment.objects.filter(course_id=ccx_course_key, user=students[3]).exists() ) self.assertTrue( CourseEnrollment.objects.filter(course_id=ccx_course_key, user=students[4]).exists() ) self.assertFalse( CourseEnrollment.objects.filter(course_id=ccx_course_key, user=students[5]).exists() )
def ccx_grades_csv(request, course, ccx=None): """ Download grades as CSV. """ if not ccx: raise Http404 ccx_key = CCXLocator.from_course_locator(course.id, ccx.id) with ccx_course(ccx_key) as course: prep_course_for_grading(course, request) enrolled_students = User.objects.filter( courseenrollment__course_id=ccx_key, courseenrollment__is_active=1).order_by('username').select_related( "profile") grades = iterate_grades_for(course, enrolled_students) header = None rows = [] for student, gradeset, __ in grades: if gradeset: # We were able to successfully grade this student for this # course. if not header: # Encode the header row in utf-8 encoding in case there are # unicode characters header = [ section['label'].encode('utf-8') for section in gradeset[u'section_breakdown'] ] rows.append(["id", "email", "username", "grade"] + header) percents = { section['label']: section.get('percent', 0.0) for section in gradeset[u'section_breakdown'] if 'label' in section } row_percents = [percents.get(label, 0.0) for label in header] rows.append([ student.id, student.email, student.username, gradeset['percent'] ] + row_percents) buf = StringIO() writer = csv.writer(buf) for row in rows: writer.writerow(row) return HttpResponse(buf.getvalue(), content_type='text/plain')
def test_redirect_to_dashboard_unenrolled_ccx(self): """ Assert that when unenroll student tries to access ccx do not allow him self-register. Redirect him to his student dashboard """ # create ccx ccx = CcxFactory(course_id=self.course.id, coach=self.coach) ccx_locator = CCXLocator.from_course_locator(self.course.id, unicode(ccx.id)) self.setup_user() url = reverse('info', args=[ccx_locator]) response = self.client.get(url) expected = reverse('dashboard') self.assertRedirects(response, expected, status_code=302, target_status_code=200)
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 ccx_grades_csv(request, course, ccx=None): """ Download grades as CSV. """ if not ccx: raise Http404 ccx_key = CCXLocator.from_course_locator(course.id, six.text_type(ccx.id)) with ccx_course(ccx_key) as course: enrolled_students = User.objects.filter( courseenrollment__course_id=ccx_key, courseenrollment__is_active=1 ).order_by('username').select_related("profile") grades = CourseGradeFactory().iter(enrolled_students, course) header = None rows = [] for student, course_grade, __ in grades: if course_grade: # We were able to successfully grade this student for this # course. if not header: # Encode the header row in utf-8 encoding in case there are # unicode characters header = [section['label'].encode('utf-8') if six.PY2 else section['label'] for section in course_grade.summary[u'section_breakdown']] rows.append(["id", "email", "username", "grade"] + header) percents = { section['label']: section.get('percent', 0.0) for section in course_grade.summary[u'section_breakdown'] if 'label' in section } row_percents = [percents.get(label, 0.0) for label in header] rows.append([student.id, student.email.encode('utf-8'), student.username.encode('utf-8'), course_grade.percent] + row_percents) buf = StringIO() writer = csv.writer(buf) for row in rows: writer.writerow(row) response = HttpResponse(buf.getvalue(), content_type='text/csv') response['Content-Disposition'] = 'attachment' return response
def edit_ccx_context(course, ccx, user, **kwargs): ccx_locator = CCXLocator.from_course_locator(course.id, unicode(ccx.pk)) schedule = get_ccx_schedule(course, ccx) grading_policy = get_override_for_ccx(ccx, course, 'grading_policy', course.grading_policy) context = {} context['ccx_locator'] = ccx_locator context['modify_access_url'] = reverse('modify_access', kwargs={'course_id': ccx_locator}) context['schedule'] = json.dumps(schedule, indent=4) context['save_url'] = reverse('save_ccx', kwargs={'course_id': ccx_locator}) non_student_user_ids = CourseAccessRole.objects.filter( course_id=ccx_locator).values_list('user_id', flat=True) ccx_student_enrollments = CourseEnrollment.objects.filter( course_id=ccx_locator, is_active=True).exclude(user_id__in=non_student_user_ids) context['ccx_student_enrollments'] = ccx_student_enrollments context['gradebook_url'] = reverse('ccx_gradebook', kwargs={'course_id': ccx_locator}) context['grades_csv_url'] = reverse('ccx_grades_csv', kwargs={'course_id': ccx_locator}) context['grading_policy'] = json.dumps(grading_policy, indent=4) context['grading_policy_url'] = reverse('ccx_set_grading_policy', kwargs={'course_id': ccx_locator}) context['STATE_CHOICES'] = STATE_CHOICES all_facilitators = get_facilitators(ccx.affiliate) added_facilitator_user_ids = CourseAccessRole.objects.filter( course_id=ccx_locator, role=AffiliateMembership.CCX_COACH).values_list('user_id', flat=True) context['added_facilitators'] = all_facilitators.filter( id__in=added_facilitator_user_ids) context['not_added_facilitators'] = all_facilitators.exclude( id__in=added_facilitator_user_ids) with ccx_course(ccx_locator) as course: context['course'] = course context['edit_ccx_url'] = reverse('edit_ccx', kwargs={'course_id': ccx_locator}) context['edit_ccx_dasboard_url'] = reverse( 'ccx_edit_course_view', kwargs={'course_id': ccx_locator}) return context
def test_redirect_to_dashboard_unenrolled_ccx(self): """ Assert that when unenrolled user tries to access CCX do not allow the user to self-register. Redirect them to their student dashboard """ # create ccx ccx = CcxFactory(course_id=self.course.id, coach=self.coach) ccx_locator = CCXLocator.from_course_locator(self.course.id, str(ccx.id)) self.setup_user() url = reverse('openedx.course_experience.course_home', args=[ccx_locator]) response = self.client.get(url) expected = reverse('dashboard') self.assertRedirects(response, expected, status_code=302, target_status_code=200)
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 set_grading_policy(request, course, ccx=None): """ Set grading policy for the CCX. """ if not ccx: raise Http404 override_field_for_ccx( ccx, course, 'grading_policy', json.loads(request.POST['policy'])) url = reverse( 'ccx_coach_dashboard', kwargs={'course_id': CCXLocator.from_course_locator(course.id, ccx.id)} ) return redirect(url)
def setUp(self): super(TestGetEmailParamsCCX, self).setUp() self.coach = AdminFactory.create() role = CourseCcxCoachRole(self.course.id) role.add_users(self.coach) self.ccx = CcxFactory(course_id=self.course.id, coach=self.coach) self.course_key = CCXLocator.from_course_locator( self.course.id, self.ccx.id) # Explicitly construct what we expect the course URLs to be site = settings.SITE_NAME self.course_url = u'https://{}/courses/{}/'.format( site, self.course_key) self.course_about_url = self.course_url + 'about' self.registration_url = u'https://{}/register'.format(site)
def restore_ccx(val, ccx_id): """restore references to a CCX to the incoming value returns the value converted to a CCX-aware state, using the provided ccx_id """ if isinstance(val, CourseLocator): return CCXLocator.from_course_locator(val, ccx_id) elif isinstance(val, BlockUsageLocator): ccx_key = restore_ccx(val.course_key, ccx_id) val = CCXBlockUsageLocator(ccx_key, val.block_type, val.block_id) if hasattr(val, 'location'): val.location = restore_ccx(val.location, ccx_id) if hasattr(val, 'children'): val.children = restore_ccx_collection(val.children, ccx_id) return val
def make_ccx(self): """ create ccx """ ccx = CustomCourseForEdX(course_id=self.course.id, coach=self.coach, display_name="Test CCX") ccx.save() ccx_locator = CCXLocator.from_course_locator(self.course.id, unicode(ccx.id)) role = CourseCcxCoachRole(ccx_locator) role.add_users(self.coach) CourseEnrollment.enroll(self.coach, ccx_locator) return ccx_locator
def test_enroll_non_user_student( self, view_name, send_email, outbox_count, student_form_input_name, button_tuple, identifier): """ Tests the enrollment of a list of students who are not users yet. It tests 2 different views that use slightly different parameters, but that perform the same task. """ self.make_coach() ccx = self.make_ccx() course_key = CCXLocator.from_course_locator(self.course.id, ccx.id) outbox = self.get_outbox() self.assertEqual(outbox, []) url = reverse( view_name, kwargs={'course_id': course_key} ) data = { button_tuple[0]: button_tuple[1], student_form_input_name: u','.join([identifier, ]), } if send_email: data['email-students'] = 'Notify-students-by-email' response = self.client.post(url, data=data, follow=True) self.assertEqual(response.status_code, 200) # we were redirected to our current location self.assertEqual(len(response.redirect_chain), 1) self.assertIn(302, response.redirect_chain[0]) self.assertEqual(len(outbox), outbox_count) # some error messages are returned for one of the views only if view_name == 'ccx_manage_student' and not is_email(identifier): error_message = 'Could not find a user with name or email "{identifier}" '.format( identifier=identifier ) self.assertContains(response, error_message, status_code=200) if is_email(identifier): if send_email: self.assertIn(identifier, outbox[0].recipients()) self.assertTrue( CourseEnrollmentAllowed.objects.filter(course_id=course_key, email=identifier).exists() ) else: self.assertFalse( CourseEnrollmentAllowed.objects.filter(course_id=course_key, email=identifier).exists() )
def restore_ccx(val, ccx_id): """restore references to a CCX to the incoming value returns the value converted to a CCX-aware state, using the provided ccx_id """ if isinstance(val, CourseLocator): return CCXLocator.from_course_locator(val, ccx_id) elif isinstance(val, BlockUsageLocator): ccx_key = restore_ccx(val.course_key, ccx_id) val = CCXBlockUsageLocator(ccx_key, val.block_type, val.block_id) for field_name in XMODULE_FIELDS_WITH_USAGE_KEYS: if hasattr(val, field_name): setattr(val, field_name, restore_ccx(getattr(val, field_name), ccx_id)) if hasattr(val, 'children'): val.children = restore_ccx_collection(val.children, ccx_id) return val
def ccx_messages_create(request, course, ccx=None, **kwargs): if not ccx: raise Http404 post_data = request.POST.copy().dict() ccx_message = CourseUpdates(date=post_data['date'], content=post_data['content'], author=request.user, ccx=ccx) ccx_message.save() ccx_id = unicode(CCXLocator.from_course_locator(course.id, unicode(ccx.id))) return redirect(reverse('ccx_messages', kwargs={'course_id': ccx_id}))
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 send_ccx_course_published(course_key): """ Find all CCX derived from this course, and send course published event for them. """ course_key = CourseLocator.from_string(course_key) for ccx in CustomCourseForEdX.objects.filter(course_id=course_key): try: ccx_key = CCXLocator.from_course_locator(course_key, str(ccx.id)) except InvalidKeyError: log.info('Attempt to publish course with deprecated id. Course: %s. CCX: %s', course_key, ccx.id) continue responses = SignalHandler.course_published.send( sender=ccx, course_key=ccx_key ) for rec, response in responses: log.info('Signal fired when course is published. Receiver: %s. Response: %s', rec, response)
def forwards(self, orm): "Convert CCX Memberships to Course Enrollments." from ccx_keys.locator import CCXLocator memberships = orm['ccx.CcxMembership'].objects.select_related( 'ccx', 'student').all() for membership in memberships: ccx = membership.ccx try: course_key = CCXLocator.from_course_locator( ccx.course_id, ccx.id) enrollment, created = orm[ 'student.CourseEnrollment'].objects.get_or_create( user=membership.student, course_id=course_key, ) except InvalidKeyError: membership.delete()
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)