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 make_ccx(self, max_students_allowed=200): """ Overridden method to replicate (part of) the actual creation of ccx courses """ ccx = super().make_ccx(max_students_allowed=max_students_allowed) ccx.structure_json = json.dumps(self.master_course_chapters) ccx.save() override_field_for_ccx(ccx, self.course, 'start', now()) 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 _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(u"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(u"unrecognized action '{}'".format(action))
def test_enroll_inactive_user(self, auto_enroll): before_ideal = SettableEnrollmentState( user=True, enrollment=False, allowed=False, auto_enroll=False, ) print("checking initialization...") eobjs = before_ideal.create_user(self.course_key, is_active=False) before = EmailEnrollmentState(self.course_key, eobjs.email) self.assertEqual(before, before_ideal) print('running action...') enroll_email(self.course_key, eobjs.email, auto_enroll=auto_enroll) print('checking effects...') after_ideal = SettableEnrollmentState( user=True, enrollment=False, allowed=True, auto_enroll=auto_enroll, ) after = EmailEnrollmentState(self.course_key, eobjs.email) self.assertEqual(after, after_ideal)
def test_enroll_inactive_user_again(self, auto_enroll): course_key = CourseLocator('Robot', 'fAKE', 'C--se--ID') before_ideal = SettableEnrollmentState( user=True, enrollment=False, allowed=True, auto_enroll=auto_enroll, ) print("checking initialization...") user = UserFactory() user.is_active = False user.save() eobjs = EnrollmentObjects( user.email, None, None, CourseEnrollmentAllowed.objects.create(email=user.email, course_id=course_key, auto_enroll=auto_enroll)) before = EmailEnrollmentState(course_key, eobjs.email) self.assertEqual(before, before_ideal) print('running action...') enroll_email(self.course_key, eobjs.email, auto_enroll=auto_enroll) print('checking effects...') after_ideal = SettableEnrollmentState( user=True, enrollment=False, allowed=True, auto_enroll=auto_enroll, ) after = EmailEnrollmentState(self.course_key, eobjs.email) self.assertEqual(after, after_ideal)
def auto_enroll_email(course_id, email, send_email=True): """ Auto-enroll email in course. Based on lms.djangoapps.instructor.views.api.students_update_enrollment() """ # Raises ValidationError if invalid validate_email(email) locator = CourseLocator.from_string(course_id) course = get_course_by_id(locator) # If we want to notify the newly enrolled student by email, fetch # the required parameters email_params = None language = None if send_email: email_params = get_email_params(course, True, secure=True) # Try to find out what language to send the email in. user = None try: user = User.objects.get(email=email) except User.DoesNotExist: pass else: language = get_user_email_language(user) # Enroll the email enroll_email(locator, email, auto_enroll=True, email_students=send_email, email_params=email_params, language=language)
def put(self, request, course_id): """ Enroll a user in a course; requires staff access **Example Request** PUT /api/enrollment/v1/roster/course-v1:foo+bar+foobar { 'email': '*****@*****.**', 'email_students': false, 'auto_enroll': true } """ try: course_key = CourseKey.from_string(course_id) except InvalidKeyError: return Response( status=status.HTTP_400_BAD_REQUEST, data={ 'message': u'Invalid or missing course_id', }, ) if not user_has_role(request.user, CourseStaffRole(course_key)): return Response( status=status.HTTP_403_FORBIDDEN, data={ 'message': u'User does not have permission to update enrollment for [{course_id}].' .format(course_id=course_id, ), }, ) email = request.data.get('email') try: validate_email(email) except ValidationError: return Response( status=status.HTTP_400_BAD_REQUEST, data={ 'message': u'Invalid email address', }, ) email_students = request.data.get('email_students', False) in ['true', 'True', True] auto_enroll = request.data.get('auto_enroll', False) in ['true', 'True', True] email_params = {} language = None if email_students: course = get_course_by_id(course_key) email_params = get_email_params(course, auto_enroll) if User.objects.filter(email=email).exists(): user = User.objects.get(email=email) language = get_user_email_language(user) enroll_email(course_key, email, auto_enroll, email_students, email_params, language=language) return Response(status=status.HTTP_204_NO_CONTENT)
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 ccx_students_enrolling_center(action, identifiers, email_students, course_key, email_params, coach): """ Function to enroll or unenroll/revoke students. Arguments: action (str): type of action to perform (Enroll, Unenroll/revoke) 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': 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 put(self, request, course_id): """ Enroll a user in a course; requires staff access **Example Request** PUT /api/enrollment/v1/roster/course-v1:foo+bar+foobar { 'email': '*****@*****.**', 'email_students': false, 'auto_enroll': true } """ try: course_key = CourseKey.from_string(course_id) except InvalidKeyError: return Response( status=status.HTTP_400_BAD_REQUEST, data={ 'message': u'Invalid or missing course_id', }, ) if not user_has_role(request.user, CourseStaffRole(course_key)): return Response( status=status.HTTP_403_FORBIDDEN, data={ 'message': u'User does not have permission to update enrollment for [{course_id}].'.format( course_id=course_id, ), }, ) email = request.data.get('email') try: validate_email(email) except ValidationError: return Response( status=status.HTTP_400_BAD_REQUEST, data={ 'message': u'Invalid email address', }, ) email_students, auto_enroll, email_params, language = self.api_params_helper(request, course_key, email) enroll_email( course_key, email, auto_enroll, email_students, email_params, language=language ) return Response(status=status.HTTP_204_NO_CONTENT)
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_affiliate_course_enrollments(sender, instance, **kwargs): # pylint: disable=unused-argument """ Allow staff or instructor level access to affiliate member into all affiliate courses if they are staff or instructor member. """ if not instance.role == AffiliateMembership.CCX_COACH: for ccx in instance.affiliate.courses: ccx_locator = CCXLocator.from_course_locator(ccx.course_id, ccx.id) course = get_course_by_id(ccx_locator) try: with transaction.atomic(): allow_access(course, instance.member, instance.role, False) except IntegrityError: LOG.error('IntegrityError: Allow access failed.') # FastTrac main course and Facilitator Guide course course_overviews = CourseOverview.objects.exclude(id__startswith='ccx-') # Program Director and Course Manager needs to be a CCX coach on FastTrac course if instance.role in AffiliateMembership.STAFF_ROLES: for course_overview in course_overviews: course_id = course_overview.id course = get_course_by_id(course_id) try: with transaction.atomic(): allow_access(course, instance.member, AffiliateMembership.CCX_COACH, False) except IntegrityError: LOG.error('IntegrityError: CCX coach failed.') elif instance.role == AffiliateMembership.CCX_COACH: for course_overview in course_overviews: course_id = course_overview.id enroll_email(course_id, instance.member.email, auto_enroll=True)
def test_enroll_nouser_change_autoenroll(self): before_ideal = SettableEnrollmentState( user=False, enrollment=False, allowed=True, auto_enroll=True, ) after_ideal = SettableEnrollmentState( user=False, enrollment=False, allowed=True, auto_enroll=False, ) action = lambda email: enroll_email(self.course_key, email, auto_enroll=False) return self._run_state_change_test(before_ideal, after_ideal, action)
def post(self, request, *args, **kwargs): course_id = request.POST.get('course_id', False) try: course_id = SlashSeparatedCourseKey.from_deprecated_string( course_id) except Exception: course_id = None if not course_id: self.msg = u"课程ID错误" context = {'msg': self.msg, 'datatable': self.make_datatable()} return render_to_response(self.template_name, context) elif not request.POST.get('identifiers'): self.msg = u"邮箱用户名错误" context = {'msg': self.msg, 'datatable': self.make_datatable()} return render_to_response(self.template_name, context) action = request.POST.get('action') identifiers_raw = request.POST.get('identifiers') identifiers = _split_input_list(identifiers_raw) auto_enroll = _get_boolean_param(request, 'auto_enroll') email_students = _get_boolean_param(request, 'email_students') is_white_label = CourseMode.is_white_label(course_id) reason = request.POST.get('reason') if is_white_label: if not reason: self.msg = "400" context = {'msg': self.msg, 'datatable': self.make_datatable()} return render_to_response(self.template_name, context) enrollment_obj = None state_transition = DEFAULT_TRANSITION_STATE email_params = {} if email_students: course = get_course_by_id(course_id) email_params = get_email_params(course, auto_enroll, secure=request.is_secure()) results = [] for identifier in identifiers: # First try to get a user object from the identifer user = None email = None language = None try: user = get_student_from_identifier(identifier) except User.DoesNotExist: email = identifier else: email = user.email language = get_user_email_language(user) 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, enrollment_obj = enroll_email( course_id, email, auto_enroll, email_students, email_params, language=language) before_enrollment = before.to_dict()['enrollment'] before_user_registered = before.to_dict()['user'] before_allowed = before.to_dict()['allowed'] after_enrollment = after.to_dict()['enrollment'] after_allowed = after.to_dict()['allowed'] if before_user_registered: if after_enrollment: if before_enrollment: state_transition = ENROLLED_TO_ENROLLED else: if before_allowed: state_transition = ALLOWEDTOENROLL_TO_ENROLLED else: state_transition = UNENROLLED_TO_ENROLLED else: if after_allowed: state_transition = UNENROLLED_TO_ALLOWEDTOENROLL elif action == 'unenroll': before, after = unenroll_email(course_id, email, email_students, email_params, language=language) before_enrollment = before.to_dict()['enrollment'] before_allowed = before.to_dict()['allowed'] enrollment_obj = CourseEnrollment.get_enrollment( user, course_id) if before_enrollment: state_transition = ENROLLED_TO_UNENROLLED else: if before_allowed: state_transition = ALLOWEDTOENROLL_TO_UNENROLLED else: state_transition = UNENROLLED_TO_UNENROLLED 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=broad-except # catch and log any exceptions # so that one error doesn't cause a 500. log.exception(u"Error while #{}ing student") log.exception(exc) results.append({ 'identifier': identifier, 'error': True, }) else: ManualEnrollmentAudit.create_manual_enrollment_audit( request.user, email, state_transition, reason, enrollment_obj) results.append({ 'identifier': identifier, 'before': before.to_dict(), 'after': after.to_dict(), }) invalid_id = [] valid_id = [] for result in results: if ('error' in result) or ('invalidIdentifier' in result): invalid_id.append(result['identifier']) else: valid_id.append(result['identifier']) invalid_message = [ "{} 无效 <br>".format(i.encode('utf-8')) for i in invalid_id ] valid_message = [] action = "选课" if action == "enroll" else "弃选" for i in valid_id: if action == "弃选": valid_message.append("{0} {1} 成功 <br>".format(i, action)) continue if email_students: valid_message.append("{0} {1} 成功,并向他发送电子邮件 <br>".format( i, action)) else: valid_message.append("{0} {1} 成功<br>".format(i, action)) invalid_message.extend(valid_message) self.msg = "".join(invalid_message) context = {'msg': self.msg, 'datatable': self.make_datatable()} return render_to_response(self.template_name, context)
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) # Save display name explicitly override_field_for_ccx(ccx, course, 'display_name', name) # 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, str(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_staff_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, str(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_staff_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 str( 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( str(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, ) # make the new coach staff on the CCX assign_staff_role_to_ccx(ccx_course_key, coach, master_course_object.id) # using CCX object as sender here. responses = SignalHandler.course_published.send( sender=ccx_course_object, course_key=ccx_course_key) for rec, response in responses: log.info( 'Signal fired when course is published. Receiver: %s. Response: %s', rec, response) 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: # Retired users should effectively appear to not exist when # attempts are made to modify them, so a direct User model email # lookup is sufficient here. This corner case relies on the fact # that we scramble emails immediately during user lock-out. Of # course, the normal cases are that the email just never existed, # or it is currently associated with an active account. 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, str(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 staff role for the coach to the newly created ccx assign_staff_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) # using CCX object as sender here. responses = SignalHandler.course_published.send( sender=ccx_course_object, course_key=ccx_course_key) for rec, response in responses: log.info( 'Signal fired when course is published. Receiver: %s. Response: %s', rec, response) 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, ) # make the new coach staff on the CCX assign_staff_role_to_ccx(ccx_course_key, coach, master_course_object.id) # using CCX object as sender here. responses = SignalHandler.course_published.send( sender=ccx_course_object, course_key=ccx_course_key ) for rec, response in responses: log.info('Signal fired when course is published. Receiver: %s. Response: %s', rec, response) 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: # Retired users should effectively appear to not exist when # attempts are made to modify them, so a direct User model email # lookup is sufficient here. This corner case relies on the fact # that we scramble emails immediately during user lock-out. Of # course, the normal cases are that the email just never existed, # or it is currently associated with an active account. 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, unicode(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 staff role for the coach to the newly created ccx assign_staff_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) # using CCX object as sender here. responses = SignalHandler.course_published.send( sender=ccx_course_object, course_key=ccx_course_key ) for rec, response in responses: log.info('Signal fired when course is published. Receiver: %s. Response: %s', rec, response) return Response( status=status.HTTP_201_CREATED, data=serializer.data )
def enroll_user_to_course(request_info, course_id, username_or_email): """ Look up the given user, and if successful, enroll them to the specified course. Arguments: request_info (dict): Dict containing task request information course_id (str): The ID string of the course username_or_email: user's username or email string Returns: User object (or None if user in not registered, and whether the user is already enrolled or not """ # First try to get a user object from the identifier (email) user = None user_already_enrolled = False language = None email_students = True auto_enroll = True thread_site = Site.objects.get(domain=request_info['host']) thread_author = User.objects.get(username=request_info['username']) try: user = get_student_from_identifier(username_or_email) except User.DoesNotExist: email = username_or_email else: email = user.email language = get_user_email_language(user) if user: course_enrollment = CourseEnrollment.get_enrollment( user=user, course_key=course_id) if course_enrollment: user_already_enrolled = True # Set the enrollment to active if its not already active if not course_enrollment.is_active: course_enrollment.update_enrollment(is_active=True) if not user or not user_already_enrolled: course = get_course_by_id(course_id, depth=0) try: with emulate_http_request(site=thread_site, user=thread_author): email_params = get_email_params(course, auto_enroll) __ = enroll_email(course_id, email, auto_enroll, email_students, email_params, language=language) if user: TASK_LOG.info( u'User %s enrolled successfully in course %s via CSV bulk enrollment', username_or_email, course_id) except: TASK_LOG.exception( u'There was an error enrolling user %s in course %s via CSV bulk enrollment', username_or_email, course_id) return None, None return user, user_already_enrolled
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 (CourseBlockWithMixins): 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.id, 'staff') list_instructor = list_with_level(master_course.id, '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.id, 'staff') list_instructor_ccx = list_with_level(course_ccx.id, '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 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 task_generate_user(self): task_input = self.request valid_rows = task_input.get("valid_rows") microsite = task_input.get("microsite") requester_id = task_input.get("requester_id") _requester_user = User.objects.get(pk=requester_id) self.site_name = task_input.get('site_name') + ' ' log.warning( u'tma_dashboard.task_generate_user inscription users pour le microsite : ' + microsite) log.warning( u'tma_dashboard.task_generate_user inscription users par le username ' + _requester_user.username + ' email : ' + _requester_user.email) generated_passwords = [] _generates = [] _failed = [] warnings = [] #Get all keys from register form register_keys = [] register_form = task_input.get("register_form") for _key in register_form: register_keys.append(_key.get('name')) # for white labels we use 'shopping cart' which uses CourseMode.DEFAULT_SHOPPINGCART_MODE_SLUG as # course mode for creating course enrollments. if CourseMode.is_white_label(self.course_key): course_mode = CourseMode.DEFAULT_SHOPPINGCART_MODE_SLUG else: course_mode = None #TREATING EACH USER for _user in valid_rows: #get current users values try: email = _user.get('email') username = email.split('@')[0].replace('-', '').replace( '.', '').replace('_', '')[0:10] + '_' + random_string(5) first_name = str(_user.get("first_name")) last_name = str(_user.get("last_name")) complete_name = first_name + ' ' + last_name #check valid email email_params = get_email_params(self.course, True, secure=True) new_course_url = 'https://' + self.site_name.replace( ' ', '') + '/dashboard/' + str(self.course.id) email_params.update({ 'site_name': self.site_name, 'course_url': new_course_url, }) except: _failed.append({ 'email': email, 'response': _('Invalid info {email_address}.').format( email_address=email) }) try: validate_email(email) except ValidationError: _failed.append({ 'email': email, 'response': _('Invalid email {email_address}.').format( email_address=email) }) if User.objects.filter(email=email).exists(): # ENROLL EXISTING USER TO COURSE user = User.objects.get(email=email) # see if it is an exact match with email and username if it's not an exact match then just display a warning message, but continue onwards if not User.objects.filter(email=email, username=username).exists(): warning_message = _( 'An account with email {email} exists but the provided username {username} ' 'is different. Enrolling anyway with {email}.').format( email=email, username=username) warnings.append({ 'username': username, 'email': email, 'response': warning_message }) log.warning(u'email %s already exist', email) else: log.info( u"user already exists with username '%s' and email '%s'", username, email) # enroll a user if it is not already enrolled. if not CourseEnrollment.is_enrolled(user, self.course_key): create_manual_course_enrollment( user=user, course_id=self.course_key, mode=course_mode, enrolled_by=_requester_user, reason='Enrolling via csv upload', state_transition=UNENROLLED_TO_ENROLLED, ) enroll_email(course_id=self.course_key, student_email=email, auto_enroll=True, email_students=True, email_params=email_params) else: # CREATE NEW ACCOUNT password = self.generate_unique_password(generated_passwords) #generate custom_field custom_field = {} for key, value in _user.items(): #assurer que la key est presente dans la liste des key et non presente dans les custom_fields actuels if (key in register_keys) and (not key in custom_field.keys()): custom_field[key] = value created_user = self.create_and_enroll_user( email, username, custom_field, password, complete_name, self.course_id, course_mode, _requester_user, email_params, first_name, last_name) #maj de l'info if created_user != '': _generates.append({ "id": created_user.id, "email": created_user.email }) else: _failed.append({ "email": email, "reponse": "creation failed" }) log.warning( u'tma_dashboard.task_generate_user fin inscription users pour le microsite : ' + microsite) log.warning( u'tma_dashboard.task_generate_user fin inscription users par le username ' + _requester_user.username + ' email : ' + _requester_user.email) #Send an email to requester with potential failures status_text = '' if not _failed: status_text = 'Tous les utilisateurs ont bien été créés et/ou inscrits au cours.' else: status_text = "Une erreur s'est produite lors de l'inscription des utilisateurs suivants :<ul>" for user in _failed: status_text += "<li>" + user['email'] + "</li>" status_text += "</ul><p>Merci de remonter le problème au service IT pour identifier l'erreur sur ces profils. Les autres profils utilisateur ont été correctement créés et/ou inscrits au cours.</p>" course = get_course_by_id(self.course_key) html = "<html><head></head><body><p>Bonjour,<br><br> L'inscription par CSV de vos utilisateurs au cours " + course.display_name_with_default + " sur le microsite " + microsite + " est maintenant terminée.<br> " + status_text + "<br><br>The MOOC Agency<br></p></body></html>" part2 = MIMEText(html.encode('utf-8'), 'html', 'utf-8') fromaddr = "*****@*****.**" toaddr = _requester_user.email msg = MIMEMultipart() msg['From'] = fromaddr msg['To'] = toaddr msg['Subject'] = "Import utilisateurs csv" part = MIMEBase('application', 'octet-stream') server = smtplib.SMTP('mail3.themoocagency.com', 25) server.starttls() server.login('contact', 'waSwv6Eqer89') msg.attach(part2) text = msg.as_string() server.sendmail(fromaddr, toaddr, text) server.quit() retour = { "requester": _requester_user.email, "_generates": _generates, "_failed": _failed, "warning": warnings } return retour
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