def test_post_list_staff_master_course_in_ccx(self): """ Specific test to check that the staff and instructor of the master course are assigned to the 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 that only one email has been sent and it is to to the coach self.assertEqual(len(outbox), 1) self.assertIn(self.coach.email, outbox[0].recipients()) # pylint: disable=no-member list_staff_master_course = list_with_level(self.course, 'staff') list_instructor_master_course = list_with_level(self.course, 'instructor') course_key = CourseKey.from_string(resp.data.get('ccx_course_id')) # pylint: disable=no-member with ccx_course_cm(course_key) as course_ccx: list_staff_ccx_course = list_with_level(course_ccx, 'staff') list_instructor_ccx_course = list_with_level(course_ccx, 'instructor') self.assertEqual(len(list_staff_master_course), len(list_staff_ccx_course)) for course_user, ccx_user in izip(sorted(list_staff_master_course), sorted(list_staff_ccx_course)): self.assertEqual(course_user, ccx_user) self.assertEqual(len(list_instructor_master_course), len(list_instructor_ccx_course)) for course_user, ccx_user in izip(sorted(list_instructor_master_course), sorted(list_instructor_ccx_course)): self.assertEqual(course_user, ccx_user)
def test_create_ccx(self, ccx_name='New CCX'): """ Create CCX. Follow redirect to coach dashboard, confirm we see the coach dashboard for the new CCX. """ self.make_coach() url = reverse( 'create_ccx', kwargs={'course_id': unicode(self.course.id)}) response = self.client.post(url, {'name': ccx_name}) self.assertEqual(response.status_code, 302) url = response.get('location') # pylint: disable=no-member response = self.client.get(url) self.assertEqual(response.status_code, 200) # Get the ccx_key path = urlparse.urlparse(url).path resolver = resolve(path) ccx_key = resolver.kwargs['course_id'] course_key = CourseKey.from_string(ccx_key) self.assertTrue(CourseEnrollment.is_enrolled(self.coach, course_key)) self.assertTrue(re.search('id="ccx-schedule"', response.content)) # check if the max amount of student that can be enrolled has been overridden ccx = CustomCourseForEdX.objects.get() course_enrollments = get_override_for_ccx(ccx, self.course, 'max_student_enrollments_allowed') self.assertEqual(course_enrollments, settings.CCX_MAX_STUDENTS_ALLOWED) # assert ccx creator has role=ccx_coach role = CourseCcxCoachRole(course_key) self.assertTrue(role.has_user(self.coach)) # assert that staff and instructors of master course has staff and instructor roles on ccx list_staff_master_course = list_with_level(self.course, 'staff') list_instructor_master_course = list_with_level(self.course, 'instructor') with ccx_course(course_key) as course_ccx: list_staff_ccx_course = list_with_level(course_ccx, 'staff') self.assertEqual(len(list_staff_master_course), len(list_staff_ccx_course)) self.assertEqual(list_staff_master_course[0].email, list_staff_ccx_course[0].email) list_instructor_ccx_course = list_with_level(course_ccx, 'instructor') self.assertEqual(len(list_instructor_ccx_course), len(list_instructor_master_course)) self.assertEqual(list_instructor_ccx_course[0].email, list_instructor_master_course[0].email)
def remove_master_course_staff_from_ccx(master_course, ccx_key, display_name, send_email=True): """ Remove staff and instructor roles on ccx to all the staff and instructors members of master course. Arguments: master_course (CourseDescriptorWithMixins): Master course instance. ccx_key (CCXLocator): CCX course key. display_name (str): ccx display name for email. send_email (bool): flag to switch on or off email to the users on revoke access. """ list_staff = list_with_level(master_course, 'staff') list_instructor = list_with_level(master_course, 'instructor') with ccx_course(ccx_key) as course_ccx: list_staff_ccx = list_with_level(course_ccx, 'staff') list_instructor_ccx = list_with_level(course_ccx, 'instructor') email_params = get_email_params(course_ccx, auto_enroll=True, course_key=ccx_key, display_name=display_name) for staff in list_staff: if staff in list_staff_ccx: # revoke 'staff' access on ccx. revoke_access(course_ccx, staff, 'staff') # Unenroll the staff on ccx. unenroll_email( course_id=ccx_key, student_email=staff.email, email_students=send_email, email_params=email_params, ) for instructor in list_instructor: if instructor in list_instructor_ccx: # revoke 'instructor' access on ccx. revoke_access(course_ccx, instructor, 'instructor') # Unenroll the instructor on ccx. unenroll_email( course_id=ccx_key, student_email=instructor.email, email_students=send_email, email_params=email_params, )
def add_master_course_staff_to_ccx(master_course, ccx_key, display_name): """ Added staff role on ccx to all the staff members of master course. Arguments: master_course (CourseDescriptorWithMixins): Master course instance ccx_key (CCXLocator): CCX course key display_name (str): ccx display name for email """ list_staff = list_with_level(master_course, 'staff') list_instructor = list_with_level(master_course, 'instructor') with ccx_course(ccx_key) as course_ccx: email_params = get_email_params(course_ccx, auto_enroll=True, course_key=ccx_key, display_name=display_name) for staff in list_staff: # allow 'staff' access on ccx to staff of master course allow_access(course_ccx, staff, 'staff') # Enroll the staff in the ccx enroll_email( course_id=ccx_key, student_email=staff.email, auto_enroll=True, email_students=True, email_params=email_params, ) for instructor in list_instructor: # allow 'instructor' access on ccx to instructor of master course allow_access(course_ccx, instructor, 'instructor') # Enroll the instructor in the ccx enroll_email( course_id=ccx_key, student_email=instructor.email, auto_enroll=True, email_students=True, email_params=email_params, )
def list_course_role_members(request, course_id): """ List instructors and staff. Requires instructor access. rolename is one of ['instructor', 'staff', 'beta'] Returns JSON of the form { "course_id": "some/course/id", "staff": [ { "username": "******", "email": "*****@*****.**", "first_name": "Joe", "last_name": "Shmoe", } ] } """ course = get_course_with_access( request.user, course_id, 'instructor', depth=None ) rolename = request.GET.get('rolename') if not rolename in ['instructor', 'staff', 'beta']: return HttpResponseBadRequest() def extract_user_info(user): """ convert user into dicts for json view """ return { 'username': user.username, 'email': user.email, 'first_name': user.first_name, 'last_name': user.last_name, } response_payload = { 'course_id': course_id, rolename: map(extract_user_info, access.list_with_level( course, rolename )), } response = HttpResponse( json.dumps(response_payload), content_type="application/json" ) return response
def list_course_role_members(request, course_id): """ List instructors and staff. Requires instructor access. rolename is one of ['instructor', 'staff', 'beta'] Returns JSON of the form { "course_id": "some/course/id", "staff": [ { "username": "******", "email": "*****@*****.**", "first_name": "Joe", "last_name": "Shmoe", } ] } """ course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) course = get_course_with_access( request.user, 'instructor', course_id, depth=None ) rolename = request.GET.get('rolename') if not rolename in ['instructor', 'staff', 'beta']: return HttpResponseBadRequest() def extract_user_info(user): """ convert user into dicts for json view """ return { 'username': user.username, 'email': user.email, 'first_name': user.first_name, 'last_name': user.last_name, } response_payload = { 'course_id': course_id.to_deprecated_string(), rolename: map(extract_user_info, list_with_level( course, rolename )), } return JsonResponse(response_payload)
def list_course_role_members(request, course_id): """ List instructors and staff. Requires instructor access. rolename is one of ['instructor', 'staff', 'beta'] Returns JSON of the form { "course_id": "some/course/id", "staff": [ { "username": "******", "email": "*****@*****.**", "first_name": "Joe", "last_name": "Shmoe", } ] } """ course = get_course_with_access(request.user, course_id, "instructor", depth=None) rolename = request.GET.get("rolename") if not rolename in ["instructor", "staff", "beta"]: return HttpResponseBadRequest() def extract_user_info(user): """ convert user into dicts for json view """ return { "username": user.username, "email": user.email, "first_name": user.first_name, "last_name": user.last_name, } response_payload = {"course_id": course_id, rolename: map(extract_user_info, list_with_level(course, rolename))} return JsonResponse(response_payload)
def add_master_course_staff_to_ccx(master_course, ccx_key, display_name, send_email=True): """ Add staff and instructor roles on ccx to all the staff and instructors members of master course. Arguments: master_course (CourseDescriptorWithMixins): Master course instance. ccx_key (CCXLocator): CCX course key. display_name (str): ccx display name for email. send_email (bool): flag to switch on or off email to the users on access grant. """ list_staff = list_with_level(master_course, 'staff') list_instructor = list_with_level(master_course, 'instructor') with ccx_course(ccx_key) as course_ccx: email_params = get_email_params(course_ccx, auto_enroll=True, course_key=ccx_key, display_name=display_name) list_staff_ccx = list_with_level(course_ccx, 'staff') list_instructor_ccx = list_with_level(course_ccx, 'instructor') for staff in list_staff: # this call should be idempotent if staff not in list_staff_ccx: try: # Enroll the staff in the ccx enroll_email( course_id=ccx_key, student_email=staff.email, auto_enroll=True, email_students=send_email, email_params=email_params, ) # allow 'staff' access on ccx to staff of master course allow_access(course_ccx, staff, 'staff') except CourseEnrollmentException: log.warning( "Unable to enroll staff %s to course with id %s", staff.email, ccx_key ) continue except SMTPException: continue for instructor in list_instructor: # this call should be idempotent if instructor not in list_instructor_ccx: try: # Enroll the instructor in the ccx enroll_email( course_id=ccx_key, student_email=instructor.email, auto_enroll=True, email_students=send_email, email_params=email_params, ) # allow 'instructor' access on ccx to instructor of master course allow_access(course_ccx, instructor, 'instructor') except CourseEnrollmentException: log.warning( "Unable to enroll instructor %s to course with id %s", instructor.email, ccx_key ) continue except SMTPException: continue
def test_list_beta(self): beta_testers = list_with_level(self.course, 'beta') self.assertEqual(set(beta_testers), set(self.beta_testers))
def test_list_instructors(self): instructors = list_with_level(self.course, 'instructor') self.assertEqual(set(instructors), set(self.instructors))
def add_master_course_staff_to_ccx(master_course, ccx_key, display_name, send_email=True): """ Add staff and instructor roles on ccx to all the staff and instructors members of master course. Arguments: master_course (CourseDescriptorWithMixins): Master course instance. ccx_key (CCXLocator): CCX course key. display_name (str): ccx display name for email. send_email (bool): flag to switch on or off email to the users on access grant. """ list_staff = list_with_level(master_course, 'staff') list_instructor = list_with_level(master_course, 'instructor') with ccx_course(ccx_key) as course_ccx: email_params = get_email_params(course_ccx, auto_enroll=True, course_key=ccx_key, display_name=display_name) list_staff_ccx = list_with_level(course_ccx, 'staff') list_instructor_ccx = list_with_level(course_ccx, 'instructor') for staff in list_staff: # this call should be idempotent if staff not in list_staff_ccx: try: # Enroll the staff in the ccx enroll_email( course_id=ccx_key, student_email=staff.email, auto_enroll=True, email_students=send_email, email_params=email_params, ) # allow 'staff' access on ccx to staff of master course allow_access(course_ccx, staff, 'staff') except CourseEnrollmentException: log.warning( "Unable to enroll staff %s to course with id %s", staff.email, ccx_key) continue except SMTPException: continue for instructor in list_instructor: # this call should be idempotent if instructor not in list_instructor_ccx: try: # Enroll the instructor in the ccx enroll_email( course_id=ccx_key, student_email=instructor.email, auto_enroll=True, email_students=send_email, email_params=email_params, ) # allow 'instructor' access on ccx to instructor of master course allow_access(course_ccx, instructor, 'instructor') except CourseEnrollmentException: log.warning( "Unable to enroll instructor %s to course with id %s", instructor.email, ccx_key) continue except SMTPException: continue
def test_allow_beta(self): """ Test allow beta against list beta. """ user = UserFactory() allow_access(self.course, user, 'beta') self.assertIn(user, list_with_level(self.course, 'beta'))
def test_revoke_beta(self): user = self.beta_testers[0] revoke_access(self.course, user, 'beta') self.assertNotIn(user, list_with_level(self.course, 'beta'))
def populate_user(user, authentication_response): attr = authentication_response.find(CAS + 'authenticationSuccess/' + CAS + 'attributes', namespaces=NSMAP) if attr is not None: staff_flag = attr.find(CAS + 'is_staff', NSMAP) if staff_flag is not None: user.is_staff = (staff_flag.text or '').upper() == 'TRUE' superuser_flag = attr.find(CAS + 'is_superuser', NSMAP) if superuser_flag is not None: user.is_superuser = (superuser_flag.text or '').upper() == 'TRUE' active_flag = attr.find(CAS + 'is_active', NSMAP) if active_flag is not None: user.is_active = (active_flag.text or '').upper() == 'TRUE' # Limiting by maximum lengths. # Max length of firstname/lastname is 30. # Max length of a email is 75. first_name = attr.find(CAS + 'givenName', NSMAP) if first_name is not None: user.first_name = (first_name.text or '')[0:30] last_name = attr.find(CAS + 'sn', NSMAP) if last_name is not None: user.last_name = (last_name.text or '')[0:30] email = attr.find(CAS + 'email', NSMAP) if email is not None: user.email = (email.text or '')[0:75] # Here we handle things that go into UserProfile instead. # This is a dirty hack and you shouldn't do that. # However, I don't think it's going to work when imported outside of the function body. from student.models import UserProfile # Make the user's password unusable. But only if they don't have an unusable password already, # to prevent SessionAuthenticationMiddleware from logging them out because their password changed. if user.has_usable_password(): user.set_unusable_password() user.save() # If the user doesn't yet have a profile, it means it's a new one and we need to create it a profile. # but we need to save the user first. user_profile, created = UserProfile.objects.get_or_create( user=user, defaults={'name': user.username}) # There should be more variables, but let's settle on the actual model first. full_name = attr.find(CAS + 'fullName', NSMAP) if full_name is not None: user_profile.name = full_name.text or '' user_profile.save() # Now the really fun bit. Signing the user up for courses given. coursetag = attr.find(CAS + 'courses', NSMAP) from student.models import CourseEnrollment from opaque_keys.edx.locator import CourseLocator from opaque_keys import InvalidKeyError from xmodule.modulestore.django import modulestore from xmodule.modulestore.exceptions import ItemNotFoundError if coursetag is not None: try: courses = json.loads(coursetag.text) assert isinstance(courses, list) except (ValueError, AssertionError): # We failed to parse the tag and get a list, so we leave. log.error("Course list failed to parse.") return # We got a list. Compare it to existing enrollments. existing_enrollments = CourseEnrollment.objects.filter( user=user, is_active=True).values_list('course_id', flat=True) for course in courses: if course and not course in existing_enrollments: try: locator = CourseLocator.from_string(course) except (InvalidKeyError, AttributeError) as e: log.error( "Invalid course identifier {}".format(course)) continue try: course = modulestore().get_course(locator) except ItemNotFoundError: log.error("Course {} does not exist.".format(course)) continue CourseEnrollment.enroll(user, locator) # Now we need to unsub the user from courses for which they are not enrolled. for course in existing_enrollments: if not course in courses: try: locator = CourseLocator.from_string(course) except (InvalidKeyError, AttributeError) as e: log.error( "Invalid course identifier {} in existing enrollments." .format(course)) continue CourseEnrollment.unenroll(user, locator) # Now implement CourseEnrollmentAllowed objects, because otherwise they will only ever fire when # users click a link in the registration email -- which can never happen here. # Considering the new setup, I doubt this will ever be useful. if created: from student.models import CourseEnrollmentAllowed for cea in CourseEnrollmentAllowed.objects.filter( email=user.email, auto_enroll=True): CourseEnrollment.enroll(user, cea.course_id) # Now, deal with course administration packets. course_admin_tag = attr.find(CAS + 'course_administration_update', NSMAP) if course_admin_tag is not None: try: courses = json.loads(course_admin_tag.text) assert isinstance(courses, dict) except (ValueError, AssertionError): # We failed to parse the tag, so we leave. log.error( "Could not parse course administration block: <<{}>>". format(course_admin_tag.text)) return from instructor.access import list_with_level, allow_access, revoke_access from django_comment_common.models import Role, FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA from django.contrib.auth.models import User for course_id, admin_block in courses.iteritems(): try: locator = CourseLocator.from_string(course_id) except (InvalidKeyError, AttributeError) as e: log.error("Invalid course identifier {}".format(course_id)) continue try: course = modulestore().get_course(locator) except ItemNotFoundError: log.error("Course {} does not exist.".format(course_id)) continue if not course: continue # Course roles are relatively easy. for block_name, role in [('admin', 'instructor'), ('staff', 'staff'), ('beta', 'beta')]: role_list = admin_block.get(block_name, []) existing = list_with_level(course, role) for username in role_list: try: user = User.objects.get(username=username) except User.DoesNotExist: continue if not user in existing: allow_access(course, user, role) try: CourseEnrollment.enroll(user, locator) except: pass for user in existing: if not user.username in role_list: revoke_access(course, user, role) # Forum roles, considerably different. for block_name, rolename in [ ('forum_admin', FORUM_ROLE_ADMINISTRATOR), ('forum_moderator', FORUM_ROLE_MODERATOR), ('forum_assistant', FORUM_ROLE_COMMUNITY_TA) ]: role_list = admin_block.get(block_name, []) try: role = Role.objects.get(course_id=locator, name=rolename) except Role.DoesNotExist: continue existing = role.users.all() for user in existing: if not user.username in role_list: role.users.remove(user) for username in role_list: try: user = User.objects.get(username=username) except User.DoesNotExist: continue if not user in existing: role.users.add(user) try: CourseEnrollment.enroll(user, locator) except: pass pass
def populate_user(user, authentication_response): attr = authentication_response.find(CAS + 'authenticationSuccess/' + CAS + 'attributes' , namespaces=NSMAP) if attr is not None: staff_flag = attr.find(CAS + 'is_staff', NSMAP) if staff_flag is not None: user.is_staff = (staff_flag.text or '').upper() == 'TRUE' superuser_flag = attr.find(CAS + 'is_superuser', NSMAP) if superuser_flag is not None: user.is_superuser = (superuser_flag.text or '').upper() == 'TRUE' active_flag = attr.find(CAS + 'is_active', NSMAP) if active_flag is not None: user.is_active = (active_flag.text or '').upper() == 'TRUE' # Limiting by maximum lengths. # Max length of firstname/lastname is 30. # Max length of a email is 75. first_name = attr.find(CAS + 'givenName', NSMAP) if first_name is not None: user.first_name = (first_name.text or '')[0:30] last_name = attr.find(CAS + 'sn', NSMAP) if last_name is not None: user.last_name = (last_name.text or '')[0:30] email = attr.find(CAS + 'email', NSMAP) if email is not None: user.email = (email.text or '')[0:75] # Here we handle things that go into UserProfile instead. # This is a dirty hack and you shouldn't do that. # However, I don't think it's going to work when imported outside of the function body. from student.models import UserProfile # Make the user's password unusable. But only if they don't have an unusable password already, # to prevent SessionAuthenticationMiddleware from logging them out because their password changed. if user.has_usable_password(): user.set_unusable_password() user.save() # If the user doesn't yet have a profile, it means it's a new one and we need to create it a profile. # but we need to save the user first. user_profile, created = UserProfile.objects.get_or_create(user=user, defaults={'name':user.username}) # There should be more variables, but let's settle on the actual model first. full_name = attr.find(CAS + 'fullName', NSMAP) if full_name is not None: user_profile.name = full_name.text or '' user_profile.save() # Now the really fun bit. Signing the user up for courses given. coursetag = attr.find(CAS + 'courses', NSMAP) from student.models import CourseEnrollment from opaque_keys.edx.locator import CourseLocator from opaque_keys import InvalidKeyError from xmodule.modulestore.django import modulestore from xmodule.modulestore.exceptions import ItemNotFoundError if coursetag is not None: try: courses = json.loads(coursetag.text) assert isinstance(courses,list) except (ValueError, AssertionError): # We failed to parse the tag and get a list, so we leave. log.error("Course list failed to parse.") return # We got a list. Compare it to existing enrollments. existing_enrollments = CourseEnrollment.objects.filter(user=user, is_active=True).values_list('course_id',flat=True) for course in courses: if course and not course in existing_enrollments: try: locator = CourseLocator.from_string(course) except (InvalidKeyError, AttributeError) as e: log.error("Invalid course identifier {}".format(course)) continue try: course = modulestore().get_course(locator) except ItemNotFoundError: log.error("Course {} does not exist.".format(course)) continue CourseEnrollment.enroll(user,locator) # Now we need to unsub the user from courses for which they are not enrolled. for course in existing_enrollments: if not course in courses: try: locator = CourseLocator.from_string(course) except (InvalidKeyError, AttributeError) as e: log.error("Invalid course identifier {} in existing enrollments.".format(course)) continue CourseEnrollment.unenroll(user, locator) # Now implement CourseEnrollmentAllowed objects, because otherwise they will only ever fire when # users click a link in the registration email -- which can never happen here. # Considering the new setup, I doubt this will ever be useful. if created: from student.models import CourseEnrollmentAllowed for cea in CourseEnrollmentAllowed.objects.filter(email=user.email, auto_enroll=True): CourseEnrollment.enroll(user, cea.course_id) # Now, deal with course administration packets. course_admin_tag = attr.find(CAS + 'course_administration_update', NSMAP) if course_admin_tag is not None: try: courses = json.loads(course_admin_tag.text) assert isinstance(courses,dict) except (ValueError, AssertionError): # We failed to parse the tag, so we leave. log.error("Could not parse course administration block: <<{}>>".format(course_admin_tag.text)) return from instructor.access import list_with_level, allow_access, revoke_access from django_comment_common.models import Role, FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA from django.contrib.auth.models import User for course_id, admin_block in courses.iteritems(): try: locator = CourseLocator.from_string(course_id) except (InvalidKeyError, AttributeError) as e: log.error("Invalid course identifier {}".format(course_id)) continue try: course = modulestore().get_course(locator) except ItemNotFoundError: log.error("Course {} does not exist.".format(course_id)) continue if not course: continue # Course roles are relatively easy. for block_name, role in [('admin','instructor'), ('staff','staff'), ('beta','beta')]: role_list = admin_block.get(block_name,[]) existing = list_with_level(course,role) for username in role_list: try: user = User.objects.get(username=username) except User.DoesNotExist: continue if not user in existing: allow_access(course, user, role) try: CourseEnrollment.enroll(user, locator) except: pass for user in existing: if not user.username in role_list: revoke_access(course, user, role) # Forum roles, considerably different. for block_name, rolename in [('forum_admin',FORUM_ROLE_ADMINISTRATOR), ('forum_moderator',FORUM_ROLE_MODERATOR), ('forum_assistant',FORUM_ROLE_COMMUNITY_TA)]: role_list = admin_block.get(block_name,[]) try: role = Role.objects.get(course_id=locator, name=rolename) except Role.DoesNotExist: continue existing = role.users.all() for user in existing: if not user.username in role_list: role.users.remove(user) for username in role_list: try: user = User.objects.get(username=username) except User.DoesNotExist: continue if not user in existing: role.users.add(user) try: CourseEnrollment.enroll(user, locator) except: pass pass