def confirm_email_change(request, key): """ User requested a new e-mail. This is called when the activation link is clicked. We confirm with the old e-mail, and update """ with transaction.atomic(): try: pec = PendingEmailChange.objects.get(activation_key=key) except PendingEmailChange.DoesNotExist: response = render_to_response("invalid_email_key.html", {}) transaction.set_rollback(True) return response user = pec.user address_context = { 'old_email': user.email, 'new_email': pec.new_email } if len(User.objects.filter(email=pec.new_email)) != 0: response = render_to_response("email_exists.html", {}) transaction.set_rollback(True) return response use_https = request.is_secure() if settings.FEATURES['ENABLE_MKTG_SITE']: contact_link = marketing_link('CONTACT') else: contact_link = '{protocol}://{site}{link}'.format( protocol='https' if use_https else 'http', site=configuration_helpers.get_value('SITE_NAME', settings.SITE_NAME), link=reverse('contact'), ) site = Site.objects.get_current() message_context = get_base_template_context(site) message_context.update({ 'old_email': user.email, 'new_email': pec.new_email, 'contact_link': contact_link, 'from_address': configuration_helpers.get_value('email_from_address', settings.DEFAULT_FROM_EMAIL), }) msg = EmailChangeConfirmation().personalize( recipient=Recipient(user.id, user.email), language=preferences_api.get_user_preference(user, LANGUAGE_KEY), user_context=message_context, ) u_prof = UserProfile.objects.get(user=user) meta = u_prof.get_meta() if 'old_emails' not in meta: meta['old_emails'] = [] meta['old_emails'].append([user.email, datetime.datetime.now(UTC).isoformat()]) u_prof.set_meta(meta) u_prof.save() # Send it to the old email... try: ace.send(msg) except Exception: # pylint: disable=broad-except log.warning('Unable to send confirmation email to old address', exc_info=True) response = render_to_response("email_change_failed.html", {'email': user.email}) transaction.set_rollback(True) return response user.email = pec.new_email user.save() pec.delete() # And send it to the new email... msg.recipient = Recipient(user.id, pec.new_email) try: ace.send(msg) except Exception: # pylint: disable=broad-except log.warning('Unable to send confirmation email to new address', exc_info=True) response = render_to_response("email_change_failed.html", {'email': pec.new_email}) transaction.set_rollback(True) return response response = render_to_response("email_change_successful.html", address_context) return response
def test_send_discussion_email_notification(self, user_subscribed): if user_subscribed: non_matching_id = 'not-a-match' # with per_page left with a default value of 1, this ensures # that we test a multiple page result when calling # comment_client.User.subscribed_threads() subscribed_thread_ids = [non_matching_id, self.discussion_id] else: subscribed_thread_ids = [] self.mock_request.side_effect = make_mock_responder( subscribed_thread_ids=subscribed_thread_ids, comment_data=self.comment, thread_data=self.thread, ) user = mock.Mock() comment = cc.Comment.find(id=self.comment['id']).retrieve() site = Site.objects.get_current() site_config = SiteConfigurationFactory.create(site=site) site_config.site_values[ENABLE_FORUM_NOTIFICATIONS_FOR_SITE_KEY] = True site_config.save() with mock.patch( 'lms.djangoapps.discussion.signals.handlers.get_current_site', return_value=site): comment_created.send(sender=None, user=user, post=comment) if user_subscribed: expected_message_context = get_base_template_context(site) expected_message_context.update({ 'comment_author_id': self.comment_author.id, 'comment_body': self.comment['body'], 'comment_created_at': ONE_HOUR_AGO, 'comment_id': self.comment['id'], 'comment_username': self.comment_author.username, 'course_id': self.course.id, 'thread_author_id': self.thread_author.id, 'thread_created_at': TWO_HOURS_AGO, 'thread_id': self.discussion_id, 'thread_title': 'thread-title', 'thread_username': self.thread_author.username, 'thread_commentable_id': self.thread['commentable_id'], 'post_link': 'https://{}{}'.format(site.domain, self.mock_permalink.return_value), 'site': site, 'site_id': site.id }) expected_recipient = Recipient(self.thread_author.username, self.thread_author.email) actual_message = self.mock_ace_send.call_args_list[0][0][0] assert expected_message_context == actual_message.context assert expected_recipient == actual_message.recipient assert self.course.language == actual_message.language self._assert_rendered_email(actual_message) else: assert not self.mock_ace_send.called
def do_email_change_request(user, new_email, activation_key=None, secondary_email_change_request=False): """ Given a new email for a user, does some basic verification of the new address and sends an activation message to the new address. If any issues are encountered with verification or sending the message, a ValueError will be thrown. """ # if activation_key is not passing as an argument, generate a random key if not activation_key: activation_key = uuid.uuid4().hex confirm_link = reverse('confirm_email_change', kwargs={'key': activation_key, }) if secondary_email_change_request: PendingSecondaryEmailChange.objects.update_or_create( user=user, defaults={ 'new_secondary_email': new_email, 'activation_key': activation_key, } ) confirm_link = reverse('activate_secondary_email', kwargs={'key': activation_key}) else: PendingEmailChange.objects.update_or_create( user=user, defaults={ 'new_email': new_email, 'activation_key': activation_key, } ) use_https = theming_helpers.get_current_request().is_secure() site = Site.objects.get_current() message_context = get_base_template_context(site) message_context.update({ 'old_email': user.email, 'new_email': new_email, 'confirm_link': '{protocol}://{site}{link}'.format( protocol='https' if use_https else 'http', site=configuration_helpers.get_value('SITE_NAME', settings.SITE_NAME), link=confirm_link, ), }) if secondary_email_change_request: msg = RecoveryEmailCreate().personalize( recipient=Recipient(user.id, new_email), language=preferences_api.get_user_preference(user, LANGUAGE_KEY), user_context=message_context, ) else: msg = EmailChange().personalize( recipient=Recipient(user.id, new_email), language=preferences_api.get_user_preference(user, LANGUAGE_KEY), user_context=message_context, ) try: ace.send(msg) except Exception: from_address = configuration_helpers.get_value('email_from_address', settings.DEFAULT_FROM_EMAIL) log.error('Unable to send email activation link to user from "%s"', from_address, exc_info=True) raise ValueError(_('Unable to send email activation link. Please try again later.')) # lint-amnesty, pylint: disable=raise-missing-from if not secondary_email_change_request: # When the email address change is complete, a "edx.user.settings.changed" event will be emitted. # But because changing the email address is multi-step, we also emit an event here so that we can # track where the request was initiated. tracker.emit( SETTING_CHANGE_INITIATED, { "setting": "email", "old": message_context['old_email'], "new": message_context['new_email'], "user_id": user.id, } )
def password_change_request_handler(request): """Handle password change requests originating from the account page. Uses the Account API to email the user a link to the password reset page. Note: The next step in the password reset process (confirmation) is currently handled by student.views.password_reset_confirm_wrapper, a custom wrapper around Django's password reset confirmation view. Args: request (HttpRequest) Returns: HttpResponse: 200 if the email was sent successfully HttpResponse: 400 if there is no 'email' POST parameter HttpResponse: 403 if the client has been rate limited HttpResponse: 405 if using an unsupported HTTP method Example usage: POST /account/password """ limiter = BadRequestRateLimiter() if limiter.is_rate_limit_exceeded(request): AUDIT_LOG.warning("Password reset rate limit exceeded") return HttpResponseForbidden() user = request.user # Prefer logged-in user's email email = user.email if user.is_authenticated else request.POST.get('email') if email: try: from openedx.core.djangoapps.user_api.accounts.api import request_password_change request_password_change(email, request.is_secure()) user = user if user.is_authenticated else User.objects.get( email=email) destroy_oauth_tokens(user) except UserNotFound: AUDIT_LOG.info("Invalid password reset attempt") # Increment the rate limit counter limiter.tick_bad_request_counter(request) # If enabled, send an email saying that a password reset was attempted, but that there is # no user associated with the email if configuration_helpers.get_value( 'ENABLE_PASSWORD_RESET_FAILURE_EMAIL', settings.FEATURES['ENABLE_PASSWORD_RESET_FAILURE_EMAIL']): site = get_current_site() message_context = get_base_template_context(site) message_context.update({ 'failed': True, 'request': request, # Used by google_analytics_tracking_pixel 'email_address': email, }) msg = PasswordReset().personalize( recipient=Recipient(username='', email_address=email), language=settings.LANGUAGE_CODE, user_context=message_context, ) ace.send(msg) except UserAPIInternalError as err: log.exception( 'Error occured during password change for user {email}: {error}' .format(email=email, error=err)) return HttpResponse(_( "Some error occured during password change. Please try again"), status=500) return HttpResponse(status=200) else: return HttpResponseBadRequest(_("No email address provided."))
def post(self, request): """ POST /api/user/v1/accounts/deactivate_logout/ Marks the user as having no password set for deactivation purposes, and logs the user out. """ user_model = get_user_model() try: # Get the username from the request and check that it exists verify_user_password_response = self._verify_user_password(request) if verify_user_password_response.status_code != status.HTTP_204_NO_CONTENT: return verify_user_password_response with transaction.atomic(): UserRetirementStatus.create_retirement(request.user) # Unlink LMS social auth accounts UserSocialAuth.objects.filter(user_id=request.user.id).delete() # Change LMS password & email user_email = request.user.email request.user.email = get_retired_email_by_email( request.user.email) request.user.save() _set_unusable_password(request.user) # TODO: Unlink social accounts & change password on each IDA. # Remove the activation keys sent by email to the user for account activation. Registration.objects.filter(user=request.user).delete() # Add user to retirement queue. # Delete OAuth tokens associated with the user. retire_dop_oauth2_models(request.user) retire_dot_oauth2_models(request.user) try: # Send notification email to user site = Site.objects.get_current() notification_context = get_base_template_context(site) notification_context.update( {'full_name': request.user.profile.name}) notification = DeletionNotificationMessage().personalize( recipient=Recipient(username='', email_address=user_email), language=request.user.profile.language, user_context=notification_context, ) ace.send(notification) except Exception as exc: log.exception( 'Error sending out deletion notification email') raise # Log the user out. logout(request) return Response(status=status.HTTP_204_NO_CONTENT) except KeyError: return Response(u'Username not specified.', status=status.HTTP_404_NOT_FOUND) except user_model.DoesNotExist: return Response(u'The user "{}" does not exist.'.format( request.user.username), status=status.HTTP_404_NOT_FOUND) except Exception as exc: # pylint: disable=broad-except return Response(text_type(exc), status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def send_mail_to_student(student, param_dict, language=None): """ Construct the email using templates and then send it. `student` is the student's email address (a `str`), `param_dict` is a `dict` with keys [ `site_name`: name given to edX instance (a `str`) `registration_url`: url for registration (a `str`) `display_name` : display name of a course (a `str`) `course_id`: id of course (a `str`) `auto_enroll`: user input option (a `str`) `course_url`: url of course (a `str`) `user_id`: LMS user ID of student (an `int`) - None if unknown `email_address`: email of student (a `str`) `full_name`: student full name (a `str`) `message_type`: type of email to send and template to use (a `str`) `is_shib_course`: (a `boolean`) ] `language` is the language used to render the email. If None the language of the currently-logged in user (that is, the user sending the email) will be used. Returns a boolean indicating whether the email was sent successfully. """ # Add some helpers and microconfig subsitutions if 'display_name' in param_dict: param_dict['course_name'] = param_dict['display_name'] elif 'course' in param_dict: param_dict['course_name'] = Text(param_dict['course'].display_name_with_default) param_dict['site_name'] = configuration_helpers.get_value( 'SITE_NAME', param_dict['site_name'] ) # Extract an LMS user ID for the student, if possible. # ACE needs the user ID to be able to send email via Braze. lms_user_id = 0 if 'user_id' in param_dict and param_dict['user_id'] is not None and param_dict['user_id'] > 0: lms_user_id = param_dict['user_id'] # see if there is an activation email template definition available as configuration, # if so, then render that message_type = param_dict['message_type'] ace_emails_dict = { 'account_creation_and_enrollment': AccountCreationAndEnrollment, 'add_beta_tester': AddBetaTester, 'allowed_enroll': AllowedEnroll, 'allowed_unenroll': AllowedUnenroll, 'enrolled_enroll': EnrollEnrolled, 'enrolled_unenroll': EnrolledUnenroll, 'remove_beta_tester': RemoveBetaTester, } message_class = ace_emails_dict[message_type] message = message_class().personalize( recipient=Recipient(lms_user_id=lms_user_id, email_address=student), language=language, user_context=param_dict, ) ace.send(message)
def test_send_discussion_email_notification(self, user_subscribed): with mock_the_things() as mocked_items: mock_request, mock_ace_send, mock_permalink = mocked_items if user_subscribed: non_matching_id = 'not-a-match' # with per_page left with a default value of 1, this ensures # that we test a multiple page result when calling # comment_client.User.subscribed_threads() mock_request.side_effect = make_mock_responder([non_matching_id, self.discussion_id]) else: mock_request.side_effect = make_mock_responder([]) now = datetime.utcnow() one_hour_ago = now - timedelta(hours=1) thread = mock.Mock( id=self.discussion_id, course_id=self.course.id, created_at=one_hour_ago, title='thread-title', user_id=self.thread_author.id, username=self.thread_author.username, commentable_id='thread-commentable-id' ) comment = mock.Mock( id='comment-id', body='comment-body', created_at=now, thread=thread, user_id=self.comment_author.id, username=self.comment_author.username ) user = mock.Mock() with waffle().override(FORUM_RESPONSE_NOTIFICATIONS): comment_created.send(sender=None, user=user, post=comment) if user_subscribed: expected_message_context = get_base_template_context(Site.objects.get_current()) expected_message_context.update({ 'comment_author_id': self.comment_author.id, 'comment_body': 'comment-body', 'comment_created_at': now, 'comment_id': 'comment-id', 'comment_username': self.comment_author.username, 'course_id': self.course.id, 'thread_author_id': self.thread_author.id, 'thread_created_at': one_hour_ago, 'thread_id': self.discussion_id, 'thread_title': 'thread-title', 'thread_username': self.thread_author.username, 'thread_commentable_id': 'thread-commentable-id', 'post_link': urljoin(Site.objects.get_current().domain, mock_permalink.return_value), 'site': Site.objects.get_current(), 'site_id': Site.objects.get_current().id, }) ga_tracking_pixel_url = _generate_ga_pixel_url(expected_message_context) expected_message_context.update({'ga_tracking_pixel_url': ga_tracking_pixel_url}) expected_recipient = Recipient(self.thread_author.username, self.thread_author.email) actual_message = mock_ace_send.call_args_list[0][0][0] self.assertEqual(expected_message_context, actual_message.context) self.assertEqual(expected_recipient, actual_message.recipient) self.assertEqual(self.course.language, actual_message.language) else: self.assertFalse(mock_ace_send.called)
def post(self, request, **kwargs): #from student.forms import send_password_reset_email_for_user from openedx.core.djangoapps.ace_common.template_context import get_base_template_context from openedx.core.djangoapps.theming.helpers import get_current_site from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from student.message_types import PasswordReset from django.contrib.auth.tokens import default_token_generator from django.utils.http import int_to_base36 from edx_ace.recipient import Recipient from openedx.core.djangoapps.user_api.preferences.api import get_user_preference from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY from edx_ace import ace self.data = request.POST.dict() if not ('uservalue' and 'sendotptype' in self.data): return JsonResponse({ "status": 400, "message": "Please enter Valid Mobile Number or Email Address or password", }) if self.data.get('sendotptype') == "mobile": mobile = self.data.get('uservalue') user = User.objects.get(extrafields__phone=mobile) email = user.email else: email = self.data.get('uservalue') if not email: return JsonResponse({ "status": 400, "message": "Email id can not be blank", }) user = User.objects.get(email=email) try: site = get_current_site() message_context = get_base_template_context(site) message_context.update({ 'request': request, # Used by google_analytics_tracking_pixel # TODO: This overrides `platform_name` from `get_base_template_context` to make the tests passes 'platform_name': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME), 'reset_link': '{protocol}://{site}{link}'.format( protocol='https', site=configuration_helpers.get_value('SITE_NAME', settings.SITE_NAME), link=reverse('password_reset_confirm', kwargs={ 'uidb36': int_to_base36(user.id), 'token': default_token_generator.make_token(user), }), ) }) msg = PasswordReset().personalize( recipient=Recipient(user.username, user.email), language=get_user_preference(user, LANGUAGE_KEY), user_context=message_context, ) ace.send(msg) return JsonResponse({ "status": 200, "message": "We sent mail in you email", }) except Exception as e: return JsonResponse({ "status": 400, "message": "Something error in sending mail", "error": "err2", })
def send_ace_message(goal): """ Send an email reminding users to stay on track for their learning goal in this course Arguments: goal (CourseGoal): Goal object """ user = goal.user try: course = CourseOverview.get_from_id(goal.course_key) except CourseOverview.DoesNotExist: log.error("Goal Reminder course not found.") course_name = course.display_name site = Site.objects.get_current() message_context = get_base_template_context(site) course_home_url = get_learning_mfe_home_url(course_key=goal.course_key, url_fragment='home') goals_unsubscribe_url = f'{settings.LEARNING_MICROFRONTEND_URL}/goal-unsubscribe/{goal.unsubscribe_token}' language = get_user_preference(user, LANGUAGE_KEY) # Code to allow displaying different banner images for different languages # However, we'll likely want to develop a better way to do this within edx-ace image_url = settings.STATIC_URL if image_url: # If the image url is a relative url prepend the LMS ROOT if 'http' not in image_url: image_url = settings.LMS_ROOT_URL + settings.STATIC_URL image_url += 'images/' if language and language in ['es', 'es-419']: image_url += 'spanish-' message_context.update({ 'email': user.email, 'platform_name': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME), 'course_name': course_name, 'days_per_week': goal.days_per_week, 'course_url': course_home_url, 'goals_unsubscribe_url': goals_unsubscribe_url, 'image_url': image_url, 'unsubscribe_url': None, # We don't want to include the default unsubscribe link 'omit_unsubscribe_link': True, 'courses_url': getattr(settings, 'ACE_EMAIL_COURSES_URL', None), 'programs_url': getattr(settings, 'ACE_EMAIL_PROGRAMS_URL', None), }) msg = Message( name="goalreminder", app_label="course_goals", recipient=Recipient(user.id, user.email), language=language, context=message_context, options={'transactional': True}, ) with emulate_http_request(site, user): ace.send(msg)
def test_send_discussion_email_notification(self, user_subscribed): if user_subscribed: non_matching_id = 'not-a-match' # with per_page left with a default value of 1, this ensures # that we test a multiple page result when calling # comment_client.User.subscribed_threads() subscribed_thread_ids = [non_matching_id, self.discussion_id] else: subscribed_thread_ids = [] self.mock_request.side_effect = make_mock_responder( subscribed_thread_ids=subscribed_thread_ids, comment_data=self.comment, thread_data=self.thread, ) user = mock.Mock() comment = cc.Comment.find(id=self.comment['id']).retrieve() site = Site.objects.get_current() with waffle().override(FORUM_RESPONSE_NOTIFICATIONS): with mock.patch( 'lms.djangoapps.discussion.signals.handlers.get_current_site', return_value=site): comment_created.send(sender=None, user=user, post=comment) if user_subscribed: expected_message_context = get_base_template_context(site) expected_message_context.update({ 'comment_author_id': self.comment_author.id, 'comment_body': self.comment['body'], 'comment_created_at': ONE_HOUR_AGO, 'comment_id': self.comment['id'], 'comment_username': self.comment_author.username, 'course_id': self.course.id, 'thread_author_id': self.thread_author.id, 'thread_created_at': TWO_HOURS_AGO, 'thread_id': self.discussion_id, 'thread_title': 'thread-title', 'thread_username': self.thread_author.username, 'thread_commentable_id': self.thread['commentable_id'], 'post_link': urljoin(site.domain, self.mock_permalink.return_value), 'site': site, 'site_id': site.id }) expected_message_context[ 'ga_tracking_pixel_url'] = _generate_ga_pixel_url( expected_message_context) expected_recipient = Recipient(self.thread_author.username, self.thread_author.email) actual_message = self.mock_ace_send.call_args_list[0][0][0] self.assertEqual(expected_message_context, actual_message.context) self.assertEqual(expected_recipient, actual_message.recipient) self.assertEqual(self.course.language, actual_message.language) else: self.assertFalse(self.mock_ace_send.called)