def _change_access(course, user, level, action, send_email=True): """ Change access of user. `level` is one of ['instructor', 'staff', 'beta'] action is one of ['allow', 'revoke'] NOTE: will create a group if it does not yet exist. """ try: role = ROLES[level](course.id) except KeyError: raise ValueError("unrecognized level '{}'".format(level)) if action == "allow": if level == "ccx_coach": email_params = get_email_params(course, True) enroll_email( course_id=course.id, student_email=user.email, auto_enroll=True, email_students=send_email, email_params=email_params, ) role.add_users(user) elif action == "revoke": role.remove_users(user) else: raise ValueError("unrecognized action '{}'".format(action))
def bulk_beta_modify_access(request, course_id): """ Enroll or unenroll users in beta testing program. Query parameters: - emails is string containing a list of emails separated by anything split_input_list can handle. - action is one of ['add', 'remove'] """ action = request.GET.get('action') emails_raw = request.GET.get('emails') emails = _split_input_list(emails_raw) email_students = request.GET.get('email_students') in ['true', 'True', True] results = [] rolename = 'beta' course = get_course_by_id(course_id) email_params = {} if email_students: email_params = get_email_params(course, auto_enroll=False) for email in emails: try: error = False user_does_not_exist = False user = User.objects.get(email=email) if action == 'add': allow_access(course, user, rolename) elif action == 'remove': revoke_access(course, user, rolename) else: return HttpResponseBadRequest(strip_tags( "Unrecognized action '{}'".format(action) )) except User.DoesNotExist: error = True user_does_not_exist = True # catch and log any unexpected exceptions # so that one error doesn't cause a 500. except Exception as exc: # pylint: disable=broad-except log.exception("Error while #{}ing student") log.exception(exc) error = True else: # If no exception thrown, see if we should send an email if email_students: send_beta_role_email(action, user, email_params) finally: # Tabulate the action result of this email address results.append({ 'email': email, 'error': error, 'userDoesNotExist': user_does_not_exist }) response_payload = { 'action': action, 'results': results, } return JsonResponse(response_payload)
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_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 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_enrolling_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 _change_access(course, user, level, action): """ Change access of user. `level` is one of ['instructor', 'staff', 'beta'] action is one of ['allow', 'revoke'] NOTE: will create a group if it does not yet exist. """ try: role = ROLES[level](course.id) except KeyError: raise ValueError("unrecognized level '{}'".format(level)) if action == 'allow': if level == 'ccx_coach': email_params = get_email_params(course, True) enroll_email( course_id=course.id, student_email=user.email, auto_enroll=True, email_students=True, email_params=email_params, ) role.add_users(user) elif action == 'revoke': role.remove_users(user) else: raise ValueError("unrecognized action '{}'".format(action))
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 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) # Enforce a static limit for the maximum amount of students that can be enrolled override_field_for_ccx(ccx, course, 'max_student_enrollments_allowed', settings.CCX_MAX_STUDENTS_ALLOWED) # 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) 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, ) assign_coach_role_to_ccx(ccx_id, request.user, course.id) add_master_course_staff_to_ccx(course, ccx_id, ccx.display_name) return redirect(url)
def bulk_beta_modify_access(request, course_id): """ Enroll or unenroll users in beta testing program. Query parameters: - identifiers is string containing a list of emails and/or usernames separated by anything split_input_list can handle. - action is one of ['add', 'remove'] """ action = request.GET.get("action") identifiers_raw = request.GET.get("identifiers") identifiers = _split_input_list(identifiers_raw) email_students = request.GET.get("email_students") in ["true", "True", True] auto_enroll = request.GET.get("auto_enroll") in ["true", "True", True] results = [] rolename = "beta" course = get_course_by_id(course_id) email_params = {} if email_students: email_params = get_email_params(course, auto_enroll=auto_enroll) for identifier in identifiers: try: error = False user_does_not_exist = False user = get_student_from_identifier(identifier) if action == "add": allow_access(course, user, rolename) elif action == "remove": revoke_access(course, user, rolename) else: return HttpResponseBadRequest(strip_tags("Unrecognized action '{}'".format(action))) except User.DoesNotExist: error = True user_does_not_exist = True # catch and log any unexpected exceptions # so that one error doesn't cause a 500. except Exception as exc: # pylint: disable=broad-except log.exception("Error while #{}ing student") log.exception(exc) error = True else: # If no exception thrown, see if we should send an email if email_students: send_beta_role_email(action, user, email_params) # See if we should autoenroll the student if auto_enroll: # Check if student is already enrolled if not CourseEnrollment.is_enrolled(user, course_id): CourseEnrollment.enroll(user, course_id) finally: # Tabulate the action result of this email address results.append({"identifier": identifier, "error": error, "userDoesNotExist": user_does_not_exist}) response_payload = {"action": action, "results": results} return JsonResponse(response_payload)
def get_email_params(self): """ Returns a dictionary of parameters used to render an email. """ email_params = get_email_params(self.course, True) email_params["email_address"] = "*****@*****.**" email_params["full_name"] = "Jean Reno" return email_params
def test_normal_params(self): # For a normal site, what do we expect to get for the URLs? # Also make sure `auto_enroll` is properly passed through. result = get_email_params(self.course, False) self.assertEqual(result['auto_enroll'], False) self.assertEqual(result['course_about_url'], self.course_about_url) self.assertEqual(result['registration_url'], self.registration_url) self.assertEqual(result['course_url'], self.course_url)
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) # Enforce a static limit for the maximum amount of students that can be enrolled override_field_for_ccx(ccx, course, "max_student_enrollments_allowed", settings.CCX_MAX_STUDENTS_ALLOWED) # 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) 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, ) assign_coach_role_to_ccx(ccx_id, request.user, course.id) add_master_course_staff_to_ccx(course, ccx_id, ccx.display_name) return redirect(url)
def test_ccx_enrollment_email_params(self): # For a CCX, what do we expect to get for the URLs? # Also make sure `auto_enroll` is properly passed through. result = get_email_params(self.course, True, course_key=self.course_key, display_name=self.ccx.display_name) self.assertEqual(result["display_name"], self.ccx.display_name) self.assertEqual(result["auto_enroll"], True) self.assertEqual(result["course_about_url"], self.course_about_url) self.assertEqual(result["registration_url"], self.registration_url) self.assertEqual(result["course_url"], self.course_url)
def get_email_params_ccx(self): """ Returns a dictionary of parameters used to render an email for CCX. """ 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 test_marketing_params(self): # For a site with a marketing front end, what do we expect to get for the URLs? # Also make sure `auto_enroll` is properly passed through. with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}): result = get_email_params(self.course, True) self.assertEqual(result['auto_enroll'], True) # We should *not* get a course about url (LMS doesn't know what the marketing site URLs are) self.assertEqual(result['course_about_url'], None) self.assertEqual(result['registration_url'], self.registration_url) self.assertEqual(result['course_url'], self.course_url)
def get_email_params_ccx(self): """ Returns a dictionary of parameters used to render an email for CCX. """ 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 test_ccx_enrollment_email_params(self): # For a CCX, what do we expect to get for the URLs? # Also make sure `auto_enroll` is properly passed through. result = get_email_params(self.course, True, course_key=self.course_key, display_name=self.ccx.display_name) self.assertEqual(result['display_name'], self.ccx.display_name) self.assertEqual(result['auto_enroll'], True) self.assertEqual(result['course_about_url'], self.course_about_url) self.assertEqual(result['registration_url'], self.registration_url) self.assertEqual(result['course_url'], self.course_url)
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 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 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 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 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 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 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 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 delete_redemption_entry(request, code_redemption, course_key): """ delete the redemption entry from the table and unenroll the user who used the registration code for the enrollment and send him/her the unenrollment email. """ user = code_redemption.redeemed_by email_address = code_redemption.redeemed_by.email full_name = code_redemption.redeemed_by.profile.name CourseEnrollment.unenroll(user, course_key, skip_refund=True) course = get_course_by_id(course_key, depth=0) email_params = get_email_params(course, True, secure=request.is_secure()) email_params['message'] = 'enrolled_unenroll' email_params['email_address'] = email_address email_params['full_name'] = full_name send_mail_to_student(email_address, email_params) # remove the redemption entry from the database. log.info('deleting redemption entry (%s) from the database.', code_redemption.id) code_redemption.delete()
def students_update_enrollment(request, course_id): """ Enroll or unenroll students by email. Requires staff access. Query Parameters: - action in ['enroll', 'unenroll'] - identifiers is string containing a list of emails and/or usernames separated by anything split_input_list can handle. - auto_enroll is a boolean (defaults to false) If auto_enroll is false, students will be allowed to enroll. If auto_enroll is true, students will be enrolled as soon as they register. - email_students is a boolean (defaults to false) If email_students is true, students will be sent email notification If email_students is false, students will not be sent email notification Returns an analog to this JSON structure: { "action": "enroll", "auto_enroll": false, "results": [ { "email": "*****@*****.**", "before": { "enrollment": false, "auto_enroll": false, "user": true, "allowed": false }, "after": { "enrollment": true, "auto_enroll": false, "user": true, "allowed": false } } ] } """ course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) action = request.GET.get('action') identifiers_raw = request.GET.get('identifiers') identifiers = _split_input_list(identifiers_raw) auto_enroll = request.GET.get('auto_enroll') in ['true', 'True', True] email_students = request.GET.get('email_students') in ['true', 'True', True] email_params = {} if email_students: course = get_course_by_id(course_id) email_params = get_email_params(course, auto_enroll) results = [] for identifier in identifiers: # First try to get a user object from the identifer user = None email = None try: user = get_student_from_identifier(identifier) except User.DoesNotExist: email = identifier else: email = user.email try: # Use django.core.validators.validate_email to check email address # validity (obviously, cannot check if email actually /exists/, # simply that it is plausibly valid) validate_email(email) # Raises ValidationError if invalid if action == 'enroll': before, after = enroll_email(course_id, email, auto_enroll, email_students, email_params) elif action == 'unenroll': before, after = unenroll_email(course_id, email, email_students, email_params) else: return HttpResponseBadRequest(strip_tags( "Unrecognized action '{}'".format(action) )) except ValidationError: # Flag this email as an error if invalid, but continue checking # the remaining in the list results.append({ 'identifier': identifier, 'invalidIdentifier': True, }) except Exception as exc: # pylint: disable=W0703 # catch and log any exceptions # so that one error doesn't cause a 500. log.exception("Error while #{}ing student") log.exception(exc) results.append({ 'identifier': identifier, 'error': True, }) else: results.append({ 'identifier': identifier, 'before': before.to_dict(), 'after': after.to_dict(), }) response_payload = { 'action': action, 'results': results, 'auto_enroll': auto_enroll, } return JsonResponse(response_payload)
def create_ccx(request, course, ccx=None, **kwargs): """ Create a new CCX """ if not is_ccx_coach_on_master_course( request.user, course) or not request.user.profile.affiliate: return HttpResponseForbidden() affiliate_slug = request.POST.get('affiliate') name = request.POST.get('name') delivery_mode = request.POST.get('delivery_mode') location_city = request.POST.get('city') location_state = request.POST.get('state') location_postal_code = request.POST.get('postal_code') time = '{} {}Z'.format(request.POST.get('date'), request.POST.get('time')) enrollment_end_date = '{} {}Z'.format( request.POST.get('enrollment_end_date'), request.POST.get('enrollment_end_time')) end_date = '{} {}Z'.format(request.POST.get('end_date'), request.POST.get('end_time')) fee = request.POST.get('fee') course_description = request.POST.get('course_description') enrollment_type = request.POST.get('enrollment_type') facilitators = dict(request.POST).get('facilitators') context = get_ccx_creation_dict(course) if not affiliate_slug: messages.error(request, 'Affiliate not selected.') return render_to_response('ccx/coach_dashboard.html', context) if not facilitators: messages.error(request, 'No facilitators added.') return render_to_response('ccx/coach_dashboard.html', context) if hasattr(course, 'ccx_connector') and course.ccx_connector: # if ccx connector url is set in course settings then inform user that he can # only create ccx by using ccx connector url. messages.error(request, context['use_ccx_con_error_message']) return render_to_response('ccx/coach_dashboard.html', context) # 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) affiliate = AffiliateEntity.objects.get(slug=affiliate_slug) if not request.user.is_staff and not AffiliateMembership.objects.filter( member=request.user, affiliate=affiliate, role__in=AffiliateMembership.STAFF_ROLES).exists(): return HttpResponseForbidden() ccx = CustomCourseForEdX(affiliate=affiliate, course_id=course.id, coach=request.user, display_name=name, delivery_mode=delivery_mode, location_city=location_city, location_state=location_state, location_postal_code=location_postal_code, time=time, enrollment_end_date=enrollment_end_date, end_date=end_date, fee=ast.literal_eval(fee), enrollment_type=enrollment_type, course_description=course_description) ccx.save() # we need this for authorization 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) # Enforce a static limit for the maximum amount of students that can be enrolled override_field_for_ccx(ccx, course, 'max_student_enrollments_allowed', settings.CCX_MAX_STUDENTS_ALLOWED) # 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.pk) 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, ) # Add facilitators if facilitators: course_obj = get_course_by_id(ccx.ccx_course_id, depth=None) facilitator_ids = [int(i) for i in facilitators] for user_id in facilitator_ids: user = User.objects.get(id=user_id) enroll_email(course_id=ccx_id, student_email=user.email, auto_enroll=True, email_students=True, email_params=email_params) allow_access(course_obj, user, AffiliateMembership.CCX_COACH, False) return redirect(url)
def edit_ccx(request, course, ccx=None, **kwargs): if not ccx: raise Http404 name = request.POST.get('name') delivery_mode = request.POST.get('delivery_mode') location_city = request.POST.get('city') location_state = request.POST.get('state') location_postal_code = request.POST.get('postal_code') time = '{} {}Z'.format(request.POST.get('date'), request.POST.get('time')) enrollment_end_date = '{} {}Z'.format( request.POST.get('enrollment_end_date'), request.POST.get('enrollment_end_time')) end_date = '{} {}Z'.format(request.POST.get('end_date'), request.POST.get('end_time')) fee = request.POST.get('fee') course_description = request.POST.get('course_description') enrollment_type = request.POST.get('enrollment_type') facilitators = dict(request.POST).get('facilitators') ccx.display_name = name ccx.delivery_mode = delivery_mode ccx.location_city = location_city ccx.location_state = location_state ccx.location_postal_code = location_postal_code ccx.enrollment_type = enrollment_type ccx.time = time ccx.enrollment_end_date = enrollment_end_date ccx.end_date = end_date ccx.fee = ast.literal_eval(fee) ccx.course_description = course_description ccx.save() current_facilitator_ids = CourseAccessRole.objects.filter( course_id=ccx.ccx_course_id, role=AffiliateMembership.CCX_COACH).values_list('user_id', flat=True) removed_facilitator_ids = set(current_facilitator_ids).difference( set(facilitators)) added_facilitator_ids = set(facilitators).difference( set(current_facilitator_ids)) ccx_id = CCXLocator.from_course_locator(course.id, ccx.pk) course_obj = get_course_by_id(ccx.ccx_course_id, depth=None) for facilitator_id in removed_facilitator_ids: user = User.objects.get(id=facilitator_id) revoke_access(course_obj, user, AffiliateMembership.CCX_COACH, False) email_params = get_email_params(course, auto_enroll=True, course_key=ccx_id, display_name=ccx.display_name) for facilitator_id in added_facilitator_ids: user = User.objects.get(id=facilitator_id) enroll_email(course_id=ccx_id, student_email=user.email, auto_enroll=True, email_students=True, email_params=email_params) allow_access(course_obj, user, AffiliateMembership.CCX_COACH, False) url = reverse('ccx_coach_dashboard', kwargs={'course_id': ccx_id}) return redirect(url)
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 students_update_enrollment(request, course_id): """ Enroll or unenroll students by email. Requires staff access. Query Parameters: - action in ['enroll', 'unenroll'] - emails is string containing a list of emails separated by anything split_input_list can handle. - auto_enroll is a boolean (defaults to false) If auto_enroll is false, students will be allowed to enroll. If auto_enroll is true, students will be enrolled as soon as they register. - email_students is a boolean (defaults to false) If email_students is true, students will be sent email notification If email_students is false, students will not be sent email notification Returns an analog to this JSON structure: { "action": "enroll", "auto_enroll": false, "results": [ { "email": "*****@*****.**", "before": { "enrollment": false, "auto_enroll": false, "user": true, "allowed": false }, "after": { "enrollment": true, "auto_enroll": false, "user": true, "allowed": false } } ] } """ action = request.GET.get("action") emails_raw = request.GET.get("emails") emails = _split_input_list(emails_raw) auto_enroll = request.GET.get("auto_enroll") in ["true", "True", True] email_students = request.GET.get("email_students") in ["true", "True", True] email_params = {} if email_students: course = get_course_by_id(course_id) email_params = get_email_params(course, auto_enroll) results = [] for email in emails: try: # Use django.core.validators.validate_email to check email address # validity (obviously, cannot check if email actually /exists/, # simply that it is plausibly valid) validate_email(email) except ValidationError: # Flag this email as an error if invalid, but continue checking # the remaining in the list results.append({"email": email, "error": True, "invalidEmail": True}) continue try: if action == "enroll": before, after = enroll_email(course_id, email, auto_enroll, email_students, email_params) elif action == "unenroll": before, after = unenroll_email(course_id, email, email_students, email_params) else: return HttpResponseBadRequest(strip_tags("Unrecognized action '{}'".format(action))) results.append({"email": email, "before": before.to_dict(), "after": after.to_dict()}) # catch and log any exceptions # so that one error doesn't cause a 500. except Exception as exc: # pylint: disable=W0703 log.exception("Error while #{}ing student") log.exception(exc) results.append({"email": email, "error": True, "invalidEmail": False}) response_payload = {"action": action, "results": results, "auto_enroll": auto_enroll} return JsonResponse(response_payload)
def post(self, request): """ Creates a new CCX course for a given Master Course. Args: request (Request): Django request object. Return: A JSON serialized representation a newly created CCX course. """ master_course_id = request.data.get('master_course_id') master_course_object, master_course_key, error_code, http_status = get_valid_course( master_course_id, advanced_course_check=True ) if master_course_object is None: return Response( status=http_status, data={ 'error_code': error_code } ) # validating the rest of the input valid_input, field_errors = get_valid_input(request.data) if field_errors: return Response( status=status.HTTP_400_BAD_REQUEST, data={ 'field_errors': field_errors } ) try: coach = User.objects.get(email=valid_input['coach_email']) except User.DoesNotExist: return Response( status=status.HTTP_404_NOT_FOUND, data={ 'error_code': 'coach_user_does_not_exist' } ) if valid_input.get('course_modules'): if not valid_course_modules(valid_input['course_modules'], master_course_key): return Response( status=status.HTTP_400_BAD_REQUEST, data={ 'error_code': 'course_module_list_not_belonging_to_master_course' } ) # prepare the course_modules to be stored in a json stringified field course_modules_json = json.dumps(valid_input.get('course_modules')) with transaction.atomic(): ccx_course_object = CustomCourseForEdX( course_id=master_course_object.id, coach=coach, display_name=valid_input['display_name'], structure_json=course_modules_json ) ccx_course_object.save() # Make sure start/due are overridden for entire course start = TODAY().replace(tzinfo=pytz.UTC) override_field_for_ccx(ccx_course_object, master_course_object, 'start', start) override_field_for_ccx(ccx_course_object, master_course_object, 'due', None) # Enforce a static limit for the maximum amount of students that can be enrolled override_field_for_ccx( ccx_course_object, master_course_object, 'max_student_enrollments_allowed', valid_input['max_students_allowed'] ) # Hide anything that can show up in the schedule hidden = 'visible_to_staff_only' for chapter in master_course_object.get_children(): override_field_for_ccx(ccx_course_object, chapter, hidden, True) for sequential in chapter.get_children(): override_field_for_ccx(ccx_course_object, sequential, hidden, True) for vertical in sequential.get_children(): override_field_for_ccx(ccx_course_object, vertical, hidden, True) # make the coach user a coach on the master course make_user_coach(coach, master_course_key) # pull the ccx course key ccx_course_key = CCXLocator.from_course_locator(master_course_object.id, ccx_course_object.id) # enroll the coach in the newly created ccx email_params = get_email_params( master_course_object, auto_enroll=True, course_key=ccx_course_key, display_name=ccx_course_object.display_name ) enroll_email( course_id=ccx_course_key, student_email=coach.email, auto_enroll=True, email_students=True, email_params=email_params, ) # assign coach role for the coach to the newly created ccx assign_coach_role_to_ccx(ccx_course_key, coach, master_course_object.id) serializer = self.get_serializer(ccx_course_object) return Response( status=status.HTTP_201_CREATED, data=serializer.data )
def create_ccx(request, course, ccx=None): """ Create a new CCX """ name = request.POST.get('name') if hasattr(course, 'ccx_connector') and course.ccx_connector: # if ccx connector url is set in course settings then inform user that he can # only create ccx by using ccx connector url. context = get_ccx_creation_dict(course) messages.error(request, context['use_ccx_con_error_message']) return render_to_response('ccx/coach_dashboard.html', context) # 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) # Enforce a static limit for the maximum amount of students that can be enrolled override_field_for_ccx(ccx, course, 'max_student_enrollments_allowed', settings.CCX_MAX_STUDENTS_ALLOWED) # 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, unicode(ccx.id)) # Create forum roles seed_permissions_roles(ccx_id) # Assign administrator forum role to CCX coach assign_role(ccx_id, request.user, FORUM_ROLE_ADMINISTRATOR) 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, ) assign_coach_role_to_ccx(ccx_id, request.user, course.id) add_master_course_staff_to_ccx(course, ccx_id, ccx.display_name) # 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) return redirect(url)
def patch(self, request, ccx_course_id=None): """ Modifies a CCX course. Args: request (Request): Django request object. ccx_course_id (string): URI element specifying the CCX course location. """ ccx_course_object, ccx_course_key, error_code, http_status = self.get_object(ccx_course_id, is_ccx=True) if ccx_course_object is None: return Response( status=http_status, data={ 'error_code': error_code } ) master_course_id = request.data.get('master_course_id') if master_course_id is not None and unicode(ccx_course_object.course_id) != master_course_id: return Response( status=status.HTTP_403_FORBIDDEN, data={ 'error_code': 'master_course_id_change_not_allowed' } ) valid_input, field_errors = get_valid_input(request.data, ignore_missing=True) if field_errors: return Response( status=status.HTTP_400_BAD_REQUEST, data={ 'field_errors': field_errors } ) # get the master course key and master course object master_course_object, master_course_key, _, _ = get_valid_course(unicode(ccx_course_object.course_id)) with transaction.atomic(): # update the display name if 'display_name' in valid_input: ccx_course_object.display_name = valid_input['display_name'] # check if the coach has changed and in case update it old_coach = None if 'coach_email' in valid_input: try: coach = User.objects.get(email=valid_input['coach_email']) except User.DoesNotExist: return Response( status=status.HTTP_404_NOT_FOUND, data={ 'error_code': 'coach_user_does_not_exist' } ) if ccx_course_object.coach.id != coach.id: old_coach = ccx_course_object.coach ccx_course_object.coach = coach if 'course_modules' in valid_input: if valid_input.get('course_modules'): if not valid_course_modules(valid_input['course_modules'], master_course_key): return Response( status=status.HTTP_400_BAD_REQUEST, data={ 'error_code': 'course_module_list_not_belonging_to_master_course' } ) # course_modules to be stored in a json stringified field ccx_course_object.structure_json = json.dumps(valid_input.get('course_modules')) ccx_course_object.save() # update the overridden field for the maximum amount of students if 'max_students_allowed' in valid_input: override_field_for_ccx( ccx_course_object, ccx_course_object.course, 'max_student_enrollments_allowed', valid_input['max_students_allowed'] ) # if the coach has changed, update the permissions if old_coach is not None: # make the new ccx coach a coach on the master course make_user_coach(coach, master_course_key) # enroll the coach in the ccx email_params = get_email_params( master_course_object, auto_enroll=True, course_key=ccx_course_key, display_name=ccx_course_object.display_name ) enroll_email( course_id=ccx_course_key, student_email=coach.email, auto_enroll=True, email_students=True, email_params=email_params, ) # enroll the coach to the newly created ccx assign_coach_role_to_ccx(ccx_course_key, coach, master_course_object.id) return Response( status=status.HTTP_204_NO_CONTENT, )
def students_update_enrollment(request, course_id): """ Enroll or unenroll students by email. Requires staff access. Query Parameters: - action in ['enroll', 'unenroll'] - emails is string containing a list of emails separated by anything split_input_list can handle. - auto_enroll is a boolean (defaults to false) If auto_enroll is false, students will be allowed to enroll. If auto_enroll is true, students will be enrolled as soon as they register. - email_students is a boolean (defaults to false) If email_students is true, students will be sent email notification If email_students is false, students will not be sent email notification Returns an analog to this JSON structure: { "action": "enroll", "auto_enroll": false, "results": [ { "email": "*****@*****.**", "before": { "enrollment": false, "auto_enroll": false, "user": true, "allowed": false }, "after": { "enrollment": true, "auto_enroll": false, "user": true, "allowed": false } } ] } """ action = request.GET.get('action') emails_raw = request.GET.get('emails') emails = _split_input_list(emails_raw) auto_enroll = request.GET.get('auto_enroll') in ['true', 'True', True] email_students = request.GET.get('email_students') in ['true', 'True', True] email_params = {} if email_students: course = get_course_by_id(course_id) email_params = get_email_params(course, auto_enroll) results = [] for email in emails: try: if action == 'enroll': before, after = enroll_email(course_id, email, auto_enroll, email_students, email_params) elif action == 'unenroll': before, after = unenroll_email(course_id, email, email_students, email_params) else: return HttpResponseBadRequest("Unrecognized action '{}'".format(action)) results.append({ 'email': email, 'before': before.to_dict(), 'after': after.to_dict(), }) # catch and log any exceptions # so that one error doesn't cause a 500. except Exception as exc: # pylint: disable=W0703 log.exception("Error while #{}ing student") log.exception(exc) results.append({ 'email': email, 'error': True, }) response_payload = { 'action': action, 'results': results, 'auto_enroll': auto_enroll, } return JsonResponse(response_payload)
def post(self, request): """ Creates a new CCX course for a given Master Course. Args: request (Request): Django request object. Return: A JSON serialized representation a newly created CCX course. """ master_course_id = request.data.get('master_course_id') master_course_object, master_course_key, error_code, http_status = get_valid_course( master_course_id, advanced_course_check=True) if master_course_object is None: return Response(status=http_status, data={'error_code': error_code}) # validating the rest of the input valid_input, field_errors = get_valid_input(request.data) if field_errors: return Response(status=status.HTTP_400_BAD_REQUEST, data={'field_errors': field_errors}) try: coach = User.objects.get(email=valid_input['coach_email']) except User.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND, data={'error_code': 'coach_user_does_not_exist'}) if valid_input.get('course_modules'): if not valid_course_modules(valid_input['course_modules'], master_course_key): return Response( status=status.HTTP_400_BAD_REQUEST, data={ 'error_code': 'course_module_list_not_belonging_to_master_course' }) # prepare the course_modules to be stored in a json stringified field course_modules_json = json.dumps(valid_input.get('course_modules')) with transaction.atomic(): ccx_course_object = CustomCourseForEdX( course_id=master_course_object.id, coach=coach, display_name=valid_input['display_name'], structure_json=course_modules_json) ccx_course_object.save() # Make sure start/due are overridden for entire course start = TODAY().replace(tzinfo=pytz.UTC) override_field_for_ccx(ccx_course_object, master_course_object, 'start', start) override_field_for_ccx(ccx_course_object, master_course_object, 'due', None) # Enforce a static limit for the maximum amount of students that can be enrolled override_field_for_ccx(ccx_course_object, master_course_object, 'max_student_enrollments_allowed', valid_input['max_students_allowed']) # Hide anything that can show up in the schedule hidden = 'visible_to_staff_only' for chapter in master_course_object.get_children(): override_field_for_ccx(ccx_course_object, chapter, hidden, True) for sequential in chapter.get_children(): override_field_for_ccx(ccx_course_object, sequential, hidden, True) for vertical in sequential.get_children(): override_field_for_ccx(ccx_course_object, vertical, hidden, True) # make the coach user a coach on the master course make_user_coach(coach, master_course_key) # pull the ccx course key ccx_course_key = CCXLocator.from_course_locator( master_course_object.id, ccx_course_object.id) # enroll the coach in the newly created ccx email_params = get_email_params( master_course_object, auto_enroll=True, course_key=ccx_course_key, display_name=ccx_course_object.display_name) enroll_email( course_id=ccx_course_key, student_email=coach.email, auto_enroll=True, email_students=True, email_params=email_params, ) # assign coach role for the coach to the newly created ccx assign_coach_role_to_ccx(ccx_course_key, coach, master_course_object.id) # assign staff role for all the staff and instructor of the master course to the newly created ccx add_master_course_staff_to_ccx(master_course_object, ccx_course_key, ccx_course_object.display_name, send_email=False) serializer = self.get_serializer(ccx_course_object) return Response(status=status.HTTP_201_CREATED, data=serializer.data)
def patch(self, request, ccx_course_id=None): """ Modifies a CCX course. Args: request (Request): Django request object. ccx_course_id (string): URI element specifying the CCX course location. """ ccx_course_object, ccx_course_key, error_code, http_status = self.get_object( ccx_course_id, is_ccx=True) if ccx_course_object is None: return Response(status=http_status, data={'error_code': error_code}) master_course_id = request.data.get('master_course_id') if master_course_id is not None and unicode( ccx_course_object.course_id) != master_course_id: return Response( status=status.HTTP_403_FORBIDDEN, data={'error_code': 'master_course_id_change_not_allowed'}) valid_input, field_errors = get_valid_input(request.data, ignore_missing=True) if field_errors: return Response(status=status.HTTP_400_BAD_REQUEST, data={'field_errors': field_errors}) # get the master course key and master course object master_course_object, master_course_key, _, _ = get_valid_course( unicode(ccx_course_object.course_id)) with transaction.atomic(): # update the display name if 'display_name' in valid_input: ccx_course_object.display_name = valid_input['display_name'] # check if the coach has changed and in case update it old_coach = None if 'coach_email' in valid_input: try: coach = User.objects.get(email=valid_input['coach_email']) except User.DoesNotExist: return Response( status=status.HTTP_404_NOT_FOUND, data={'error_code': 'coach_user_does_not_exist'}) if ccx_course_object.coach.id != coach.id: old_coach = ccx_course_object.coach ccx_course_object.coach = coach if 'course_modules' in valid_input: if valid_input.get('course_modules'): if not valid_course_modules(valid_input['course_modules'], master_course_key): return Response( status=status.HTTP_400_BAD_REQUEST, data={ 'error_code': 'course_module_list_not_belonging_to_master_course' }) # course_modules to be stored in a json stringified field ccx_course_object.structure_json = json.dumps( valid_input.get('course_modules')) ccx_course_object.save() # update the overridden field for the maximum amount of students if 'max_students_allowed' in valid_input: override_field_for_ccx(ccx_course_object, ccx_course_object.course, 'max_student_enrollments_allowed', valid_input['max_students_allowed']) # if the coach has changed, update the permissions if old_coach is not None: # make the new ccx coach a coach on the master course make_user_coach(coach, master_course_key) # enroll the coach in the ccx email_params = get_email_params( master_course_object, auto_enroll=True, course_key=ccx_course_key, display_name=ccx_course_object.display_name) enroll_email( course_id=ccx_course_key, student_email=coach.email, auto_enroll=True, email_students=True, email_params=email_params, ) # enroll the coach to the newly created ccx assign_coach_role_to_ccx(ccx_course_key, coach, master_course_object.id) return Response(status=status.HTTP_204_NO_CONTENT, )
def students_update_enrollment(request, course_id): """ Enroll or unenroll students by email. Requires staff access. Query Parameters: - action in ['enroll', 'unenroll'] - identifiers is string containing a list of emails and/or usernames separated by anything split_input_list can handle. - auto_enroll is a boolean (defaults to false) If auto_enroll is false, students will be allowed to enroll. If auto_enroll is true, students will be enrolled as soon as they register. - email_students is a boolean (defaults to false) If email_students is true, students will be sent email notification If email_students is false, students will not be sent email notification Returns an analog to this JSON structure: { "action": "enroll", "auto_enroll": false, "results": [ { "email": "*****@*****.**", "before": { "enrollment": false, "auto_enroll": false, "user": true, "allowed": false }, "after": { "enrollment": true, "auto_enroll": false, "user": true, "allowed": false } } ] } """ action = request.GET.get('action') identifiers_raw = request.GET.get('identifiers') identifiers = _split_input_list(identifiers_raw) auto_enroll = request.GET.get('auto_enroll') in ['true', 'True', True] email_students = request.GET.get('email_students') in [ 'true', 'True', True ] email_params = {} if email_students: course = get_course_by_id(course_id) email_params = get_email_params(course, auto_enroll) results = [] for identifier in identifiers: # First try to get a user object from the identifer user = None email = None try: user = get_student_from_identifier(identifier) except User.DoesNotExist: email = identifier else: email = user.email try: # Use django.core.validators.validate_email to check email address # validity (obviously, cannot check if email actually /exists/, # simply that it is plausibly valid) validate_email(email) # Raises ValidationError if invalid if action == 'enroll': before, after = enroll_email(course_id, email, auto_enroll, email_students, email_params) elif action == 'unenroll': before, after = unenroll_email(course_id, email, email_students, email_params) else: return HttpResponseBadRequest( strip_tags("Unrecognized action '{}'".format(action))) except ValidationError: # Flag this email as an error if invalid, but continue checking # the remaining in the list results.append({ 'identifier': identifier, 'invalidIdentifier': True, }) except Exception as exc: # pylint: disable=W0703 # catch and log any exceptions # so that one error doesn't cause a 500. log.exception("Error while #{}ing student") log.exception(exc) results.append({ 'identifier': identifier, 'error': True, }) else: results.append({ 'identifier': identifier, 'before': before.to_dict(), 'after': after.to_dict(), }) response_payload = { 'action': action, 'results': results, 'auto_enroll': auto_enroll, } return JsonResponse(response_payload)
def bulk_beta_modify_access(request, course_id): """ Enroll or unenroll users in beta testing program. Query parameters: - identifiers is string containing a list of emails and/or usernames separated by anything split_input_list can handle. - action is one of ['add', 'remove'] """ action = request.GET.get('action') identifiers_raw = request.GET.get('identifiers') identifiers = _split_input_list(identifiers_raw) email_students = request.GET.get('email_students') in [ 'true', 'True', True ] auto_enroll = request.GET.get('auto_enroll') in ['true', 'True', True] results = [] rolename = 'beta' course = get_course_by_id(course_id) email_params = {} if email_students: email_params = get_email_params(course, auto_enroll=auto_enroll) for identifier in identifiers: try: error = False user_does_not_exist = False user = get_student_from_identifier(identifier) if action == 'add': allow_access(course, user, rolename) elif action == 'remove': revoke_access(course, user, rolename) else: return HttpResponseBadRequest( strip_tags("Unrecognized action '{}'".format(action))) except User.DoesNotExist: error = True user_does_not_exist = True # catch and log any unexpected exceptions # so that one error doesn't cause a 500. except Exception as exc: # pylint: disable=broad-except log.exception("Error while #{}ing student") log.exception(exc) error = True else: # If no exception thrown, see if we should send an email if email_students: send_beta_role_email(action, user, email_params) # See if we should autoenroll the student if auto_enroll: # Check if student is already enrolled if not CourseEnrollment.is_enrolled(user, course_id): CourseEnrollment.enroll(user, course_id) finally: # Tabulate the action result of this email address results.append({ 'identifier': identifier, 'error': error, 'userDoesNotExist': user_does_not_exist }) response_payload = { 'action': action, 'results': results, } return JsonResponse(response_payload)
def create_ccx(request, course, ccx=None): """ Create a new CCX """ name = request.POST.get('name') if hasattr(course, 'ccx_connector') and course.ccx_connector: # if ccx connector url is set in course settings then inform user that he can # only create ccx by using ccx connector url. context = get_ccx_creation_dict(course) messages.error(request, context['use_ccx_con_error_message']) return render_to_response('ccx/coach_dashboard.html', context) # 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) # Enforce a static limit for the maximum amount of students that can be enrolled override_field_for_ccx(ccx, course, 'max_student_enrollments_allowed', settings.CCX_MAX_STUDENTS_ALLOWED) # 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, unicode(ccx.id)) # Create forum roles seed_permissions_roles(ccx_id) # Assign administrator forum role to CCX coach assign_role(ccx_id, request.user, FORUM_ROLE_ADMINISTRATOR) 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, ) assign_coach_role_to_ccx(ccx_id, request.user, course.id) add_master_course_staff_to_ccx(course, ccx_id, ccx.display_name) # 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) return redirect(url)
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