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 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 _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 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 _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 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 ccx_students_enrolling_center(action, identifiers, email_students, course_key, email_params): """ Function to enroll/add or unenroll/revoke students. This function exists for backwards compatibility: in CCX there are two different views to manage students that used to implement a different logic. Now the logic has been reconciled at the point that this function can be used by both. The two different views can be merged after some UI refactoring. Arguments: action (str): type of action to perform (add, Enroll, revoke, Unenroll) identifiers (list): list of students username/email email_students (bool): Flag to send an email to students course_key (CCXLocator): a CCX course key email_params (dict): dictionary of settings for the email to be sent Returns: list: list of error """ errors = [] if action == 'Enroll' or action == 'add': ccx_course_overview = CourseOverview.get_from_id(course_key) for identifier in identifiers: if CourseEnrollment.objects.is_course_full(ccx_course_overview): error = _( 'The course is full: the limit is {max_student_enrollments_allowed}' ).format(max_student_enrollments_allowed=ccx_course_overview. max_student_enrollments_allowed) log.info("%s", error) errors.append(error) break try: email = get_valid_student_email(identifier) except CCXUserValidationException as exp: log.info("%s", exp) errors.append("{0}".format(exp)) continue enroll_email(course_key, email, auto_enroll=True, email_students=email_students, email_params=email_params) elif action == 'Unenroll' or action == 'revoke': for identifier in identifiers: try: email = get_valid_student_email(identifier) except CCXUserValidationException as exp: log.info("%s", exp) errors.append("{0}".format(exp)) continue unenroll_email(course_key, email, email_students=email_students, email_params=email_params) return errors
def ccx_students_enrolling_center(action, identifiers, email_students, course_key, email_params, coach): """ Function to enroll/add or unenroll/revoke students. This function exists for backwards compatibility: in CCX there are two different views to manage students that used to implement a different logic. Now the logic has been reconciled at the point that this function can be used by both. The two different views can be merged after some UI refactoring. Arguments: action (str): type of action to perform (add, Enroll, revoke, Unenroll) identifiers (list): list of students username/email email_students (bool): Flag to send an email to students course_key (CCXLocator): a CCX course key email_params (dict): dictionary of settings for the email to be sent coach (User): ccx coach Returns: list: list of error """ errors = [] if action == 'Enroll' or action == 'add': ccx_course_overview = CourseOverview.get_from_id(course_key) course_locator = course_key.to_course_locator() staff = CourseStaffRole(course_locator).users_with_role() admins = CourseInstructorRole(course_locator).users_with_role() for identifier in identifiers: must_enroll = False try: email, student = get_valid_student_with_email(identifier) if student: must_enroll = student in staff or student in admins or student == coach except CCXUserValidationException as exp: log.info("%s", exp) errors.append("{0}".format(exp)) continue if CourseEnrollment.objects.is_course_full(ccx_course_overview) and not must_enroll: error = _('The course is full: the limit is {max_student_enrollments_allowed}').format( max_student_enrollments_allowed=ccx_course_overview.max_student_enrollments_allowed) log.info("%s", error) errors.append(error) break enroll_email(course_key, email, auto_enroll=True, email_students=email_students, email_params=email_params) elif action == 'Unenroll' or action == 'revoke': for identifier in identifiers: try: email, __ = get_valid_student_with_email(identifier) except CCXUserValidationException as exp: log.info("%s", exp) errors.append("{0}".format(exp)) continue unenroll_email(course_key, email, email_students=email_students, email_params=email_params) return errors
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 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 _ccx_students_enrrolling_center(action, identifiers, email_students, course_key, email_params): """ Function to enroll/add or unenroll/revoke students. This function exists for backwards compatibility: in CCX there are two different views to manage students that used to implement a different logic. Now the logic has been reconciled at the point that this function can be used by both. The two different views can be merged after some UI refactoring. Arguments: action (str): type of action to perform (add, Enroll, revoke, Unenroll) identifiers (list): list of students username/email email_students (bool): Flag to send an email to students course_key (CCXLocator): a CCX course key email_params (dict): dictionary of settings for the email to be sent Returns: list: list of error """ errors = [] if action == "Enroll" or action == "add": ccx_course_overview = CourseOverview.get_from_id(course_key) for identifier in identifiers: if CourseEnrollment.objects.is_course_full(ccx_course_overview): error = "The course is full: the limit is {0}".format( ccx_course_overview.max_student_enrollments_allowed ) log.info("%s", error) errors.append(error) break try: email = get_valid_student_email(identifier) except CCXUserValidationException as exp: log.info("%s", exp) errors.append("{0}".format(exp)) continue enroll_email(course_key, email, auto_enroll=True, email_students=email_students, email_params=email_params) elif action == "Unenroll" or action == "revoke": for identifier in identifiers: try: email = get_valid_student_email(identifier) except CCXUserValidationException as exp: log.info("%s", exp) errors.append("{0}".format(exp)) continue unenroll_email(course_key, email, email_students=email_students, email_params=email_params) return errors
def test_enroll(self): before_ideal = SettableEnrollmentState(user=True, enrollment=False, allowed=False, auto_enroll=False) after_ideal = SettableEnrollmentState(user=True, enrollment=True, allowed=False, auto_enroll=False) action = lambda email: enroll_email(self.course_key, email) return self._run_state_change_test(before_ideal, after_ideal, action)
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 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 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 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 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 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 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 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 enroled as soon as they register. 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] results = [] for email in emails: try: if action == 'enroll': before, after = enroll_email(course_id, email, auto_enroll) elif action == 'unenroll': before, after = unenroll_email(course_id, email) 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 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 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 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 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 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 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 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 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 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 )