def test_unenrollment_email_on(self): """ Do email on unenroll test """ course = self.course #Create invited, but not registered, user cea = CourseEnrollmentAllowed(email='*****@*****.**', course_id=course.id) cea.save() url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) response = self.client.post(url, {'action': 'Unenroll multiple students', 'multiple_students': '[email protected], [email protected], [email protected]', 'email_students': 'on'}) #Check the page output self.assertContains(response, '<td>[email protected]</td>') self.assertContains(response, '<td>[email protected]</td>') self.assertContains(response, '<td>un-enrolled, email sent</td>') #Check the outbox self.assertEqual(len(mail.outbox), 3) self.assertEqual(mail.outbox[0].subject, 'You have been un-enrolled from MITx/999/Robot_Super_Course') self.assertEqual(mail.outbox[0].body, "Dear Student,\n\nYou have been un-enrolled from course MITx/999/Robot_Super_Course by a member of the course staff. " + "Please disregard the invitation previously sent.\n\n" + "----\nThis email was automatically sent from edx.org to [email protected]") self.assertEqual(mail.outbox[1].subject, 'You have been un-enrolled from MITx/999/Robot_Super_Course')
def post(self, request): """ POST /api/user/v1/accounts/retire/ { 'username': '******' } Retires the user with the given username. This includes retiring this username, the associates email address, and any other PII associated with this user. """ username = request.data['username'] if is_username_retired(username): return Response(status=status.HTTP_404_NOT_FOUND) try: retirement_status = UserRetirementStatus.get_retirement_for_retirement_action(username) user = retirement_status.user retired_username = retirement_status.retired_username or get_retired_username_by_username(username) retired_email = retirement_status.retired_email or get_retired_email_by_email(user.email) original_email = retirement_status.original_email # Retire core user/profile information self.clear_pii_from_userprofile(user) self.delete_users_profile_images(user) self.delete_users_country_cache(user) # Retire data from Enterprise models self.retire_users_data_sharing_consent(username, retired_username) self.retire_sapsf_data_transmission(user) self.retire_user_from_pending_enterprise_customer_user(user, retired_email) self.retire_entitlement_support_detail(user) # Retire misc. models that may contain PII of this user SoftwareSecurePhotoVerification.retire_user(user.id) PendingEmailChange.delete_by_user_value(user, field='user') UserOrgTag.delete_by_user_value(user, field='user') # Retire any objects linked to the user via their original email CourseEnrollmentAllowed.delete_by_user_value(original_email, field='email') UnregisteredLearnerCohortAssignments.delete_by_user_value(original_email, field='email') # TODO: Password Reset links - https://openedx.atlassian.net/browse/PLAT-2104 # TODO: Delete OAuth2 records - https://openedx.atlassian.net/browse/EDUCATOR-2703 user.first_name = '' user.last_name = '' user.is_active = False user.username = retired_username user.save() except UserRetirementStatus.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) except RetirementStateError as exc: return Response(text_type(exc), status=status.HTTP_400_BAD_REQUEST) except Exception as exc: # pylint: disable=broad-except return Response(text_type(exc), status=status.HTTP_500_INTERNAL_SERVER_ERROR) return Response(status=status.HTTP_204_NO_CONTENT)
def post(self, request): """ POST /api/user/v1/accounts/retire/ { 'username': '******' } Retires the user with the given username. This includes retiring this username, the associated email address, and any other PII associated with this user. """ username = request.data['username'] try: retirement_status = UserRetirementStatus.get_retirement_for_retirement_action(username) user = retirement_status.user retired_username = retirement_status.retired_username or get_retired_username_by_username(username) retired_email = retirement_status.retired_email or get_retired_email_by_email(user.email) original_email = retirement_status.original_email # Retire core user/profile information self.clear_pii_from_userprofile(user) self.delete_users_profile_images(user) self.delete_users_country_cache(user) # Retire data from Enterprise models self.retire_users_data_sharing_consent(username, retired_username) self.retire_sapsf_data_transmission(user) self.retire_degreed_data_transmission(user) self.retire_user_from_pending_enterprise_customer_user(user, retired_email) self.retire_entitlement_support_detail(user) # Retire misc. models that may contain PII of this user PendingEmailChange.delete_by_user_value(user, field='user') UserOrgTag.delete_by_user_value(user, field='user') # Retire any objects linked to the user via their original email CourseEnrollmentAllowed.delete_by_user_value(original_email, field='email') UnregisteredLearnerCohortAssignments.delete_by_user_value(original_email, field='email') # This signal allows code in higher points of LMS to retire the user as necessary USER_RETIRE_LMS_CRITICAL.send(sender=self.__class__, user=user) user.first_name = '' user.last_name = '' user.is_active = False user.username = retired_username user.save() except UserRetirementStatus.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) except RetirementStateError as exc: return Response(text_type(exc), status=status.HTTP_400_BAD_REQUEST) except Exception as exc: # pylint: disable=broad-except return Response(text_type(exc), status=status.HTTP_500_INTERNAL_SERVER_ERROR) return Response(status=status.HTTP_204_NO_CONTENT)
def __init__(self, course_id, email): # N.B. retired users are not a concern here because they should be # handled at a higher level (i.e. in enroll_email). Besides, this # class creates readonly objects. exists_user = User.objects.filter(email=email).exists() if exists_user: user = User.objects.get(email=email) mode, is_active = CourseEnrollment.enrollment_mode_for_user(user, course_id) # is_active is `None` if the user is not enrolled in the course exists_ce = is_active is not None and is_active full_name = user.profile.name ceas = CourseEnrollmentAllowed.for_user(user).filter(course_id=course_id).all() else: mode = None exists_ce = False full_name = None ceas = CourseEnrollmentAllowed.objects.filter(email=email, course_id=course_id).all() exists_allowed = ceas.exists() state_auto_enroll = exists_allowed and ceas[0].auto_enroll self.user = exists_user self.enrollment = exists_ce self.allowed = exists_allowed self.auto_enroll = bool(state_auto_enroll) self.full_name = full_name self.mode = mode
def test_retiring_nonexistent_user_doesnt_modify_records(self): is_successful = CourseEnrollmentAllowed.delete_by_user_value( value='*****@*****.**', field='email' ) self.assertFalse(is_successful) user_search_results = CourseEnrollmentAllowed.objects.filter( email=self.email ) self.assertTrue(user_search_results.exists())
def test_retiring_user_deletes_record(self): is_successful = CourseEnrollmentAllowed.delete_by_user_value( value=self.email, field='email' ) self.assertTrue(is_successful) user_search_results = CourseEnrollmentAllowed.objects.filter( email=self.email ) self.assertFalse(user_search_results)
def test_unenrollment_email_on(self): """ Do email on unenroll test """ course = self.course # Create invited, but not registered, user cea = CourseEnrollmentAllowed(email="*****@*****.**", course_id=course.id) cea.save() url = reverse("instructor_dashboard_legacy", kwargs={"course_id": course.id.to_deprecated_string()}) response = self.client.post( url, { "action": "Unenroll multiple students", "multiple_students": "[email protected], [email protected], [email protected]", "email_students": "on", }, ) # Check the page output self.assertContains(response, "<td>[email protected]</td>") self.assertContains(response, "<td>[email protected]</td>") self.assertContains(response, "<td>un-enrolled, email sent</td>") # Check the outbox self.assertEqual(len(mail.outbox), 3) self.assertEqual(mail.outbox[0].subject, "You have been un-enrolled from {}".format(course.display_name)) self.assertEqual( mail.outbox[0].body, "Dear Student,\n\nYou have been un-enrolled from course " "{} by a member of the course staff. " "Please disregard the invitation previously sent.\n\n" "----\nThis email was automatically sent from edx.org " "to [email protected]".format(course.display_name), ) self.assertEqual(mail.outbox[1].subject, "You have been un-enrolled from {}".format(course.display_name))
def list_may_enroll(course_key, features): """ Return info about students who may enroll in a course as a dict. list_may_enroll(course_key, ['email']) would return [ {'email': 'email1'} {'email': 'email2'} {'email': 'email3'} ] Note that result does not include students who may enroll and have already done so. """ may_enroll_and_unenrolled = CourseEnrollmentAllowed.may_enroll_and_unenrolled(course_key) def extract_student(student, features): """ Build dict containing information about a single student. """ return dict((feature, getattr(student, feature)) for feature in features) return [extract_student(student, features) for student in may_enroll_and_unenrolled]
def __init__(self, course_id, email): exists_user = User.objects.filter(email=email).exists() if exists_user: user = User.objects.get(email=email) mode, is_active = CourseEnrollment.enrollment_mode_for_user(user, course_id) # is_active is `None` if the user is not enrolled in the course exists_ce = is_active is not None and is_active full_name = user.profile.name ceas = CourseEnrollmentAllowed.for_user(user).filter(course_id=course_id).all() else: mode = None exists_ce = False full_name = None ceas = CourseEnrollmentAllowed.objects.filter(email=email, course_id=course_id).all() exists_allowed = ceas.exists() state_auto_enroll = exists_allowed and ceas[0].auto_enroll self.user = exists_user self.enrollment = exists_ce self.allowed = exists_allowed self.auto_enroll = bool(state_auto_enroll) self.full_name = full_name self.mode = mode
def _do_enroll_students(course, course_key, students, secure=False, overload=False, auto_enroll=False, email_students=False, is_shib_course=False): """ Do the actual work of enrolling multiple students, presented as a string of emails separated by commas or returns `course` is course object `course_key` id of course (a CourseKey) `students` string of student emails separated by commas or returns (a `str`) `overload` un-enrolls all existing students (a `boolean`) `auto_enroll` is user input preference (a `boolean`) `email_students` is user input preference (a `boolean`) """ new_students, new_students_lc = get_and_clean_student_list(students) status = dict([x, 'unprocessed'] for x in new_students) if overload: # delete all but staff todelete = CourseEnrollment.objects.filter(course_id=course_key) for enrollee in todelete: if not has_access(enrollee.user, 'staff', course) and enrollee.user.email.lower() not in new_students_lc: status[enrollee.user.email] = 'deleted' enrollee.deactivate() else: status[enrollee.user.email] = 'is staff' ceaset = CourseEnrollmentAllowed.objects.filter(course_id=course_key) for cea in ceaset: status[cea.email] = 'removed from pending enrollment list' ceaset.delete() if email_students: protocol = 'https' if secure else 'http' stripped_site_name = microsite.get_value( 'SITE_NAME', settings.SITE_NAME ) # TODO: Use request.build_absolute_uri rather than '{proto}://{site}{path}'.format # and check with the Services team that this works well with microsites registration_url = '{proto}://{site}{path}'.format( proto=protocol, site=stripped_site_name, path=reverse('student.views.register_user') ) course_url = '{proto}://{site}{path}'.format( proto=protocol, site=stripped_site_name, path=reverse('course_root', kwargs={'course_id': course_key.to_deprecated_string()}) ) # We can't get the url to the course's About page if the marketing site is enabled. course_about_url = None if not settings.FEATURES.get('ENABLE_MKTG_SITE', False): course_about_url = u'{proto}://{site}{path}'.format( proto=protocol, site=stripped_site_name, path=reverse('about_course', kwargs={'course_id': course_key.to_deprecated_string()}) ) # Composition of email email_data = { 'site_name': stripped_site_name, 'registration_url': registration_url, 'course': course, 'auto_enroll': auto_enroll, 'course_url': course_url, 'course_about_url': course_about_url, 'is_shib_course': is_shib_course } for student in new_students: try: user = User.objects.get(email=student) except User.DoesNotExist: # Student not signed up yet, put in pending enrollment allowed table cea = CourseEnrollmentAllowed.objects.filter(email=student, course_id=course_key) # If enrollmentallowed already exists, update auto_enroll flag to however it was set in UI # Will be 0 or 1 records as there is a unique key on email + course_id if cea: cea[0].auto_enroll = auto_enroll cea[0].save() status[student] = 'user does not exist, enrollment already allowed, pending with auto enrollment ' \ + ('on' if auto_enroll else 'off') continue # EnrollmentAllowed doesn't exist so create it cea = CourseEnrollmentAllowed(email=student, course_id=course_key, auto_enroll=auto_enroll) cea.save() status[student] = 'user does not exist, enrollment allowed, pending with auto enrollment ' \ + ('on' if auto_enroll else 'off') if email_students: # User is allowed to enroll but has not signed up yet email_data['email_address'] = student email_data['message'] = 'allowed_enroll' send_mail_ret = send_mail_to_student(student, email_data) status[student] += (', email sent' if send_mail_ret else '') continue # Student has already registered if CourseEnrollment.is_enrolled(user, course_key): status[student] = 'already enrolled' continue try: # Not enrolled yet CourseEnrollment.enroll(user, course_key) status[student] = 'added' if email_students: # User enrolled for first time, populate dict with user specific info email_data['email_address'] = student email_data['full_name'] = user.profile.name email_data['message'] = 'enrolled_enroll' send_mail_ret = send_mail_to_student(student, email_data) status[student] += (', email sent' if send_mail_ret else '') except Exception: # pylint: disable=broad-except status[student] = 'rejected' datatable = {'header': ['StudentEmail', 'action']} datatable['data'] = [[x, status[x]] for x in sorted(status)] datatable['title'] = _('Enrollment of students') def sf(stat): return [x for x in status if status[x] == stat] data = dict(added=sf('added'), rejected=sf('rejected') + sf('exists'), deleted=sf('deleted'), datatable=datatable) return data
def _do_enroll_students( course, course_key, students, secure=False, overload=False, auto_enroll=False, email_students=False, is_shib_course=False, ): """ Do the actual work of enrolling multiple students, presented as a string of emails separated by commas or returns `course` is course object `course_key` id of course (a CourseKey) `students` string of student emails separated by commas or returns (a `str`) `overload` un-enrolls all existing students (a `boolean`) `auto_enroll` is user input preference (a `boolean`) `email_students` is user input preference (a `boolean`) """ new_students, new_students_lc = get_and_clean_student_list(students) status = dict([x, "unprocessed"] for x in new_students) if overload: # delete all but staff todelete = CourseEnrollment.objects.filter(course_id=course_key) for enrollee in todelete: if not has_access(enrollee.user, "staff", course) and enrollee.user.email.lower() not in new_students_lc: status[enrollee.user.email] = "deleted" enrollee.deactivate() else: status[enrollee.user.email] = "is staff" ceaset = CourseEnrollmentAllowed.objects.filter(course_id=course_key) for cea in ceaset: status[cea.email] = "removed from pending enrollment list" ceaset.delete() if email_students: protocol = "https" if secure else "http" stripped_site_name = microsite.get_value("SITE_NAME", settings.SITE_NAME) # TODO: Use request.build_absolute_uri rather than '{proto}://{site}{path}'.format # and check with the Services team that this works well with microsites registration_url = "{proto}://{site}{path}".format( proto=protocol, site=stripped_site_name, path=reverse("register_user") ) course_url = "{proto}://{site}{path}".format( proto=protocol, site=stripped_site_name, path=reverse("course_root", kwargs={"course_id": course_key.to_deprecated_string()}), ) # We can't get the url to the course's About page if the marketing site is enabled. course_about_url = None if not settings.FEATURES.get("ENABLE_MKTG_SITE", False): course_about_url = u"{proto}://{site}{path}".format( proto=protocol, site=stripped_site_name, path=reverse("about_course", kwargs={"course_id": course_key.to_deprecated_string()}), ) # Composition of email email_data = { "site_name": stripped_site_name, "registration_url": registration_url, "course": course, "auto_enroll": auto_enroll, "course_url": course_url, "course_about_url": course_about_url, "is_shib_course": is_shib_course, } for student in new_students: try: user = User.objects.get(email=student) except User.DoesNotExist: # Student not signed up yet, put in pending enrollment allowed table cea = CourseEnrollmentAllowed.objects.filter(email=student, course_id=course_key) # If enrollmentallowed already exists, update auto_enroll flag to however it was set in UI # Will be 0 or 1 records as there is a unique key on email + course_id if cea: cea[0].auto_enroll = auto_enroll cea[0].save() status[student] = "user does not exist, enrollment already allowed, pending with auto enrollment " + ( "on" if auto_enroll else "off" ) continue # EnrollmentAllowed doesn't exist so create it cea = CourseEnrollmentAllowed(email=student, course_id=course_key, auto_enroll=auto_enroll) cea.save() status[student] = "user does not exist, enrollment allowed, pending with auto enrollment " + ( "on" if auto_enroll else "off" ) if email_students: # User is allowed to enroll but has not signed up yet email_data["email_address"] = student email_data["message"] = "allowed_enroll" send_mail_ret = send_mail_to_student(student, email_data) status[student] += ", email sent" if send_mail_ret else "" continue # Student has already registered if CourseEnrollment.is_enrolled(user, course_key): status[student] = "already enrolled" continue try: # Not enrolled yet CourseEnrollment.enroll(user, course_key) status[student] = "added" if email_students: # User enrolled for first time, populate dict with user specific info email_data["email_address"] = student email_data["full_name"] = user.profile.name email_data["message"] = "enrolled_enroll" send_mail_ret = send_mail_to_student(student, email_data) status[student] += ", email sent" if send_mail_ret else "" except Exception: # pylint: disable=broad-except status[student] = "rejected" datatable = {"header": ["StudentEmail", "action"]} datatable["data"] = [[x, status[x]] for x in sorted(status)] datatable["title"] = _("Enrollment of students") def sf(stat): return [x for x in status if status[x] == stat] data = dict(added=sf("added"), rejected=sf("rejected") + sf("exists"), deleted=sf("deleted"), datatable=datatable) return data
def _do_enroll_students(course, course_key, students, secure=False, overload=False, auto_enroll=False, email_students=False, is_shib_course=False): """ Do the actual work of enrolling multiple students, presented as a string of emails separated by commas or returns `course` is course object `course_key` id of course (a CourseKey) `students` string of student emails separated by commas or returns (a `str`) `overload` un-enrolls all existing students (a `boolean`) `auto_enroll` is user input preference (a `boolean`) `email_students` is user input preference (a `boolean`) """ new_students, new_students_lc = get_and_clean_student_list(students) status = dict([x, 'unprocessed'] for x in new_students) if overload: # delete all but staff todelete = CourseEnrollment.objects.filter(course_id=course_key) for enrollee in todelete: if not has_access(enrollee.user, 'staff', course) and enrollee.user.email.lower() not in new_students_lc: status[enrollee.user.email] = 'deleted' enrollee.deactivate() else: status[enrollee.user.email] = 'is staff' ceaset = CourseEnrollmentAllowed.objects.filter(course_id=course_key) for cea in ceaset: status[cea.email] = 'removed from pending enrollment list' ceaset.delete() if email_students: protocol = 'https' if secure else 'http' stripped_site_name = microsite.get_value( 'SITE_NAME', settings.SITE_NAME ) # TODO: Use request.build_absolute_uri rather than '{proto}://{site}{path}'.format # and check with the Services team that this works well with microsites registration_url = '{proto}://{site}{path}'.format( proto=protocol, site=stripped_site_name, path=reverse('register_user') ) course_url = '{proto}://{site}{path}'.format( proto=protocol, site=stripped_site_name, path=reverse('course_root', kwargs={'course_id': course_key.to_deprecated_string()}) ) # We can't get the url to the course's About page if the marketing site is enabled. course_about_url = None if not settings.FEATURES.get('ENABLE_MKTG_SITE', False): course_about_url = u'{proto}://{site}{path}'.format( proto=protocol, site=stripped_site_name, path=reverse('about_course', kwargs={'course_id': course_key.to_deprecated_string()}) ) # Composition of email email_data = { 'site_name': stripped_site_name, 'registration_url': registration_url, 'course': course, 'auto_enroll': auto_enroll, 'course_url': course_url, 'course_about_url': course_about_url, 'is_shib_course': is_shib_course } for student in new_students: try: user = User.objects.get(email=student) except User.DoesNotExist: # Student not signed up yet, put in pending enrollment allowed table cea = CourseEnrollmentAllowed.objects.filter(email=student, course_id=course_key) # If enrollmentallowed already exists, update auto_enroll flag to however it was set in UI # Will be 0 or 1 records as there is a unique key on email + course_id if cea: cea[0].auto_enroll = auto_enroll cea[0].save() status[student] = 'user does not exist, enrollment already allowed, pending with auto enrollment ' \ + ('on' if auto_enroll else 'off') continue # EnrollmentAllowed doesn't exist so create it cea = CourseEnrollmentAllowed(email=student, course_id=course_key, auto_enroll=auto_enroll) cea.save() status[student] = 'user does not exist, enrollment allowed, pending with auto enrollment ' \ + ('on' if auto_enroll else 'off') if email_students: # User is allowed to enroll but has not signed up yet email_data['email_address'] = student email_data['message'] = 'allowed_enroll' send_mail_ret = send_mail_to_student(student, email_data) status[student] += (', email sent' if send_mail_ret else '') continue # Student has already registered if CourseEnrollment.is_enrolled(user, course_key): status[student] = 'already enrolled' continue try: # Not enrolled yet CourseEnrollment.enroll(user, course_key) status[student] = 'added' if email_students: # User enrolled for first time, populate dict with user specific info email_data['email_address'] = student email_data['full_name'] = user.profile.name email_data['message'] = 'enrolled_enroll' send_mail_ret = send_mail_to_student(student, email_data) status[student] += (', email sent' if send_mail_ret else '') except Exception: # pylint: disable=broad-except status[student] = 'rejected' datatable = {'header': ['StudentEmail', 'action']} datatable['data'] = [[x, status[x]] for x in sorted(status)] datatable['title'] = _('Enrollment of students') def sf(stat): # pylint: disable=invalid-name return [x for x in status if status[x] == stat] data = dict(added=sf('added'), rejected=sf('rejected') + sf('exists'), deleted=sf('deleted'), datatable=datatable) return data
def post(self, request): """ POST /api/user/v1/accounts/retire/ { 'username': '******' } Retires the user with the given username. This includes retiring this username, the associates email address, and any other PII associated with this user. """ username = request.data['username'] try: retirement_status = UserRetirementStatus.get_retirement_for_retirement_action( username) user = retirement_status.user retired_username = retirement_status.retired_username or get_retired_username_by_username( username) retired_email = retirement_status.retired_email or get_retired_email_by_email( user.email) original_email = retirement_status.original_email # Retire core user/profile information self.clear_pii_from_userprofile(user) self.delete_users_profile_images(user) self.delete_users_country_cache(user) # Retire data from Enterprise models self.retire_users_data_sharing_consent(username, retired_username) self.retire_sapsf_data_transmission(user) self.retire_degreed_data_transmission(user) self.retire_user_from_pending_enterprise_customer_user( user, retired_email) self.retire_entitlement_support_detail(user) # Retire misc. models that may contain PII of this user SoftwareSecurePhotoVerification.retire_user(user.id) PendingEmailChange.delete_by_user_value(user, field='user') UserOrgTag.delete_by_user_value(user, field='user') # Retire any objects linked to the user via their original email CourseEnrollmentAllowed.delete_by_user_value(original_email, field='email') UnregisteredLearnerCohortAssignments.delete_by_user_value( original_email, field='email') user.first_name = '' user.last_name = '' user.is_active = False user.username = retired_username user.save() except UserRetirementStatus.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) except RetirementStateError as exc: return Response(text_type(exc), status=status.HTTP_400_BAD_REQUEST) except Exception as exc: # pylint: disable=broad-except return Response(text_type(exc), status=status.HTTP_500_INTERNAL_SERVER_ERROR) return Response(status=status.HTTP_204_NO_CONTENT)