Example #1
0
def send_notification(key, data, subject=None, dest_emails=[]):
    """
    Send an email notification to provided email addresses.

    Arguments:
        key         - Email template will be selected on the basis of key
        data        - Dict containing context/data for the template
        subject     - Email subject
        dest_emails - List of destination emails

    """
    site = get_current_site()
    message_context = get_base_template_context(site)

    message_context.update(data)

    if subject:
        message_context['subject'] = subject

    for email in dest_emails:
        log.info('Sending support email with subject "{}" to {}'.format(
            subject, email
        ))

        msg = EMAIL_CLASSES[key]().personalize(
            recipient=Recipient(username='', email_address=email),
            language=settings.LANGUAGE_CODE,
            user_context=message_context,
        )
        ace.send(msg)
Example #2
0
    def schedules_for_bin(self):
        week_num = abs(self.day_offset) // 7
        schedules = self.get_schedules_with_target_date_by_bin_and_orgs(
            order_by='enrollment__course', )

        template_context = get_base_template_context(self.site)
        for schedule in schedules:
            enrollment = schedule.enrollment
            course = schedule.enrollment.course
            user = enrollment.user

            # (Weekly) Course Updates are only for Instructor-paced courses.
            # See CourseNextSectionUpdate for Self-paced updates.
            if course.self_paced:
                continue

            try:
                week_highlights = get_week_highlights(user,
                                                      enrollment.course_id,
                                                      week_num)
            except CourseUpdateDoesNotExist:
                LOG.warning(
                    'Weekly highlights for user {} in week {} of course {} does not exist or is disabled'
                    .format(user, week_num, enrollment.course_id))
                # continue to the next schedule, don't yield an email for this one
            else:
                unsubscribe_url = None
                if (COURSE_UPDATE_SHOW_UNSUBSCRIBE_WAFFLE_SWITCH.is_enabled()
                        and 'bulk_email_optout'
                        in settings.ACE_ENABLED_POLICIES):
                    unsubscribe_url = reverse('bulk_email_opt_out',
                                              kwargs={
                                                  'token':
                                                  UsernameCipher.encrypt(
                                                      user.username),
                                                  'course_id':
                                                  str(enrollment.course_id),
                                              })

                template_context.update({
                    'course_name':
                    schedule.enrollment.course.display_name,
                    'course_url':
                    _get_trackable_course_home_url(enrollment.course_id),
                    'week_num':
                    week_num,
                    'week_highlights':
                    week_highlights,

                    # This is used by the bulk email optout policy
                    'course_ids': [str(enrollment.course_id)],
                    'unsubscribe_url':
                    unsubscribe_url,
                })
                template_context.update(
                    _get_upsell_information_for_schedule(user, schedule))

                yield (user,
                       schedule.enrollment.course.closest_released_language,
                       template_context)
    def send_password_reset_email(self, user, site):
        """
        Send email to learner with reset password link
        :param user:
        :param site:
        """
        message_context = get_base_template_context(site)
        email = user.email
        message_context.update({
            'email':
            email,
            'platform_name':
            configuration_helpers.get_value('PLATFORM_NAME',
                                            settings.PLATFORM_NAME),
            'reset_link':
            '{protocol}://{site}{link}?track=pwreset'.format(
                protocol='http',
                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),
                             }),
            )
        })

        with emulate_http_request(site, user):
            msg = PasswordReset().personalize(
                recipient=Recipient(user.username, email),
                language=get_user_preference(user, LANGUAGE_KEY),
                user_context=message_context,
            )
            ace.send(msg)
Example #4
0
def send_password_reset_email_for_user(user, request, preferred_email=None):
    """
    Send out a password reset email for the given user.

    Arguments:
        user (User): Django User object
        request (HttpRequest): Django request object
        preferred_email (str): Send email to this address if present, otherwise fallback to user's email address.
    """
    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' if request.is_secure() else 'http',
            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, preferred_email or user.email),
        language=get_user_preference(user, LANGUAGE_KEY),
        user_context=message_context,
    )
    ace.send(msg)
Example #5
0
    def schedules_for_bin(self):
        schedules = self.get_schedules_with_target_date_by_bin_and_orgs()
        template_context = get_base_template_context(self.site)

        for (user, user_schedules) in groupby(schedules,
                                              lambda s: s.enrollment.user):
            user_schedules = list(user_schedules)
            course_id_strs = [
                str(schedule.enrollment.course_id)
                for schedule in user_schedules
            ]

            # This is used by the bulk email optout policy
            template_context['course_ids'] = course_id_strs

            first_schedule = user_schedules[0]
            try:
                template_context.update(
                    self.get_template_context(user, user_schedules))
            except InvalidContextError:
                continue

            yield (user,
                   first_schedule.enrollment.course.closest_released_language,
                   template_context)
Example #6
0
    def schedules_for_bin(self):
        week_num = abs(self.day_offset) / 7
        schedules = self.get_schedules_with_target_date_by_bin_and_orgs(
            order_by='enrollment__course',
        )

        template_context = get_base_template_context(self.site)
        for schedule in schedules:
            enrollment = schedule.enrollment
            user = enrollment.user

            try:
                week_highlights = get_week_highlights(user, enrollment.course_id, week_num)
            except CourseUpdateDoesNotExist:
                LOG.exception(
                    'Weekly highlights for user {} in week {} of course {} does not exist or is disabled'.format(
                        user, week_num, enrollment.course_id
                    )
                )

            template_context.update({
                'course_name': schedule.enrollment.course.display_name,
                'course_url': _get_trackable_course_home_url(enrollment.course_id),

                'week_num': week_num,
                'week_highlights': week_highlights,

                # This is used by the bulk email optout policy
                'course_ids': [str(enrollment.course_id)],
            })
            template_context.update(_get_upsell_information_for_schedule(user, schedule))

            yield (user, schedule.enrollment.course.closest_released_language, template_context)
Example #7
0
def generate_activation_email_context(user, registration):
    """
    Constructs a dictionary for use in activation email contexts

    Arguments:
        user (User): Currently logged-in user
        registration (Registration): Registration object for the currently logged-in user
    """
    context = get_base_template_context(None)
    context.update({
        'name':
        user.profile.name,
        'key':
        registration.activation_key,
        'lms_url':
        configuration_helpers.get_value('LMS_ROOT_URL', settings.LMS_ROOT_URL),
        'platform_name':
        configuration_helpers.get_value('PLATFORM_NAME',
                                        settings.PLATFORM_NAME),
        'contact_mailing_address':
        configuration_helpers.get_value('contact_mailing_address',
                                        settings.CONTACT_MAILING_ADDRESS),
        'support_url':
        configuration_helpers.get_value('ACTIVATION_EMAIL_SUPPORT_LINK',
                                        settings.ACTIVATION_EMAIL_SUPPORT_LINK)
        or settings.SUPPORT_SITE_LINK,
        'support_email':
        configuration_helpers.get_value('CONTACT_EMAIL',
                                        settings.CONTACT_EMAIL),
        'site_configuration_values':
        configuration_helpers.get_current_site_configuration_values(),
    })
    return context
Example #8
0
    def schedules_for_bin(self):
        week_num = abs(self.day_offset) / 7
        schedules = self.get_schedules_with_target_date_by_bin_and_orgs(
            order_by='enrollment__course',
        )

        template_context = get_base_template_context(self.site)
        for schedule in schedules:
            enrollment = schedule.enrollment
            user = enrollment.user

            try:
                week_highlights = get_week_highlights(user, enrollment.course_id, week_num)
            except CourseUpdateDoesNotExist:
                LOG.warning(
                    'Weekly highlights for user {} in week {} of course {} does not exist or is disabled'.format(
                        user, week_num, enrollment.course_id
                    )
                )
                # continue to the next schedule, don't yield an email for this one
            else:
                template_context.update({
                    'course_name': schedule.enrollment.course.display_name,
                    'course_url': _get_trackable_course_home_url(enrollment.course_id),

                    'week_num': week_num,
                    'week_highlights': week_highlights,

                    # This is used by the bulk email optout policy
                    'course_ids': [str(enrollment.course_id)],
                })
                template_context.update(_get_upsell_information_for_schedule(user, schedule))

                yield (user, schedule.enrollment.course.closest_released_language, template_context)
Example #9
0
def send_account_recovery_email_for_user(user, request, email=None):
    """
    Send out a account recovery email for the given user.

    Arguments:
        user (User): Django User object
        request (HttpRequest): Django request object
        email (str): Send email to this address.
    """
    site = get_current_site()
    message_context = get_base_template_context(site)
    message_context.update({
        'request': request,  # Used by google_analytics_tracking_pixel
        'email': email,
        'platform_name': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME),
        'reset_link': '{protocol}://{site}{link}?is_account_recovery=true'.format(
            protocol='https' if request.is_secure() else 'http',
            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 = AccountRecoveryMessage().personalize(
        recipient=Recipient(user.username, email),
        language=get_user_preference(user, LANGUAGE_KEY),
        user_context=message_context,
    )
    ace.send(msg)
Example #10
0
def send_password_reset_email_for_user(user, request, preferred_email=None):
    """
    Send out a password reset email for the given user.

    Arguments:
        user (User): Django User object
        request (HttpRequest): Django request object
        preferred_email (str): Send email to this address if present, otherwise fallback to user's email address.
    """
    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' if request.is_secure() else 'http',
            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, preferred_email or user.email),
        language=get_user_preference(user, LANGUAGE_KEY),
        user_context=message_context,
    )
    ace.send(msg)
Example #11
0
    def save(self,  # pylint: disable=arguments-differ
             use_https=False,
             token_generator=default_token_generator,
             request=None,
             **_kwargs):
        """
        Generates a one-use only link for resetting password and sends to the
        user.
        """
        for user in self.users_cache:
            site = Site.objects.get_current()
            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' if use_https else 'http',
                    site=configuration_helpers.get_value('SITE_NAME', settings.SITE_NAME),
                    link=reverse('password_reset_confirm', kwargs={
                        'uidb36': int_to_base36(user.id),
                        'token': 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)
Example #12
0
def send_account_recovery_email_for_user(user, request, email=None):
    """
    Send out a account recovery email for the given user.

    Arguments:
        user (User): Django User object
        request (HttpRequest): Django request object
        email (str): Send email to this address.
    """
    site = get_current_site()
    message_context = get_base_template_context(site)
    message_context.update({
        'request': request,  # Used by google_analytics_tracking_pixel
        'platform_name': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME),
        'reset_link': '{protocol}://{site}{link}'.format(
            protocol='https' if request.is_secure() else 'http',
            site=configuration_helpers.get_value('SITE_NAME', settings.SITE_NAME),
            link=reverse('account_recovery_confirm', kwargs={
                'uidb36': int_to_base36(user.id),
                'token': default_token_generator.make_token(user),
            }),
        )
    })

    msg = AccountRecoveryMessage().personalize(
        recipient=Recipient(user.username, email),
        language=get_user_preference(user, LANGUAGE_KEY),
        user_context=message_context,
    )
    ace.send(msg)
Example #13
0
    def save(self,  # pylint: disable=arguments-differ
             use_https=False,
             token_generator=default_token_generator,
             request=None,
             **_kwargs):
        """
        Generates a one-use only link for resetting password and sends to the
        user.
        """
        for user in self.users_cache:
            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' if use_https else 'http',
                    site=configuration_helpers.get_value('SITE_NAME', settings.SITE_NAME),
                    link=reverse('password_reset_confirm', kwargs={
                        'uidb36': int_to_base36(user.id),
                        'token': 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)
Example #14
0
    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, self.question_id]
        else:
            subscribed_thread_ids = []

        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()

        examples = [
            (self.discussion_thread, self.discussion_comment),
            (self.question_thread, self.question_comment),
        ]
        for thread, comment in examples:
            self.mock_ace_send.reset_mock()
            self.mock_request.side_effect = make_mock_responder(
                subscribed_thread_ids=subscribed_thread_ids,
                comment_data=comment,
                thread_data=thread,
            )
            user = mock.Mock()
            comment = cc.Comment.find(id=comment['id']).retrieve()
            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': comment['body'],
                    'comment_created_at': ONE_HOUR_AGO,
                    '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': TWO_HOURS_AGO,
                    'thread_id': thread['id'],
                    'thread_title': thread['title'],
                    'thread_username': self.thread_author.username,
                    'thread_commentable_id': thread['commentable_id'],
                    'post_link': f'https://{site.domain}{self.mock_permalink.return_value}',
                    'site': site,
                    'site_id': site.id
                })
                expected_recipient = Recipient(self.thread_author.id, 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, comment)

            else:
                assert not self.mock_ace_send.called
Example #15
0
def get_user_default_email_params(user):
    """
    Get default email params for the user.
    """
    site = get_current_site()
    message_context = get_base_template_context(site)
    user_language_pref = get_user_preference(user, LANGUAGE_KEY)
    return [message_context, user_language_pref]
Example #16
0
def _build_message_context_for_reported_content(context, moderator):  # lint-amnesty, pylint: disable=missing-function-docstring
    message_context = get_base_template_context(context['site'])
    message_context.update(context)
    message_context.update({
        'post_link': _get_thread_url(context),
        'moderator_email': moderator.email,
    })
    return message_context
Example #17
0
    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)
Example #18
0
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 = reverse(course_home_url_name(course.id),
                              args=[str(course.id)])
    course_home_absolute_url_parts = ("https", site.name, course_home_url, '',
                                      '', '')
    course_home_absolute_url = six.moves.urllib.parse.urlunparse(
        course_home_absolute_url_parts)

    goals_unsubscribe_url = reverse('course-home:unsubscribe-from-course-goal',
                                    kwargs={'token': goal.unsubscribe_token})

    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_absolute_url,
        'goals_unsubscribe_url':
        goals_unsubscribe_url,
        'unsubscribe_url':
        None,  # We don't want to include the default unsubscribe link
    })

    msg = Message(name="goalreminder",
                  app_label="course_goals",
                  recipient=Recipient(user.id, user.email),
                  language=get_user_preference(user, LANGUAGE_KEY),
                  context=message_context)

    with emulate_http_request(site, user):
        ace.send(msg)
    def send_verification_expiry_email(self, batch_verifications,
                                       email_config):
        """
        Sends verification expiry email to the learners in the batch using edx_ace
        If the email is successfully sent change the expiry_email_date to reflect when the
        email was sent

        :param batch_verifications: verification objects for which email will be sent
        :param email_config: Contains configuration like dry-run flag value, which determines whether actual email will
                             be sent or not
        """
        if email_config['dry_run']:
            logger.info(
                u"This was a dry run, no email was sent. For the actual run email would have been sent "
                u"to {} learner(s)".format(len(batch_verifications)))
            return True

        site = Site.objects.get_current()
        message_context = get_base_template_context(site)
        message_context.update({
            'platform_name':
            settings.PLATFORM_NAME,
            'lms_verification_link':
            '{}{}'.format(settings.LMS_ROOT_URL,
                          reverse("verify_student_reverify")),
            'help_center_link':
            settings.ID_VERIFICATION_SUPPORT_LINK
        })

        expiry_email = VerificationExpiry(context=message_context)
        users = User.objects.filter(pk__in=[
            verification.user_id for verification in batch_verifications
        ])

        success = True
        for verification in batch_verifications:
            try:
                user = users.get(pk=verification.user_id)
                with emulate_http_request(site=site, user=user):
                    msg = expiry_email.personalize(
                        recipient=Recipient(user.username, user.email),
                        language=get_user_preference(user, LANGUAGE_KEY),
                        user_context={
                            'full_name': user.profile.name,
                        })
                    ace.send(msg)
                    self._set_email_expiry_date(verification, user,
                                                email_config)
            except Exception:  # pylint: disable=broad-except
                logger.exception('Could not send email for verification id %d',
                                 verification.id)
                success = False

        return success
Example #20
0
def deactivate_user(user):
    """
    Deactivate and retire the given user
    """
    user_model = get_user_model()
    try:
        with transaction.atomic():
            UserRetirementStatus.create_retirement(user)
            # Unlink LMS social auth accounts
            UserSocialAuth.objects.filter(user_id=user.id).delete()
            # Change LMS password & email
            user_email = user.email
            user.email = get_retired_email_by_email(user.email)
            user.save()
            _set_unusable_password(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=user).delete()
            # Add user to retirement queue.
            # Delete OAuth tokens associated with the user.
            retire_dop_oauth2_models(user)
            retire_dot_oauth2_models(user)
            try:
                # Send notification email to user
                site = Site.objects.get_current()
                notification_context = get_base_template_context(site)
                notification_context.update({'full_name': user.profile.name})
                notification_context.update({
                    'reset_password_link':
                    urlparse.urljoin(
                        settings.PROGS_URLS.get("ROOT"),
                        settings.PROGS_URLS.get("PROG_RESET_PASSWORD",
                                                "reset_password"))
                })
                notification = DeletionNotificationMessage().personalize(
                    recipient=Recipient(username='', email_address=user_email),
                    language=user.profile.language,
                    user_context=notification_context,
                )
                ace.send(notification)
            except Exception as exc:
                log.exception('Error sending out deletion notification email')
                raise
        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(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)
Example #21
0
    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.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': 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]
            self.assertEqual(expected_message_context, actual_message.context)
            self.assertEqual(expected_recipient, actual_message.recipient)
            self.assertEqual(self.course.language, actual_message.language)
            self._assert_rendered_email(actual_message)

        else:
            self.assertFalse(self.mock_ace_send.called)
def _build_message_context(context):  # lint-amnesty, pylint: disable=missing-function-docstring
    message_context = get_base_template_context(context['site'])
    message_context.update(context)
    thread_author = User.objects.get(id=context['thread_author_id'])
    comment_author = User.objects.get(id=context['comment_author_id'])
    message_context.update({
        'thread_username': thread_author.username,
        'comment_username': comment_author.username,
        'post_link': _get_thread_url(context),
        'comment_created_at': date.deserialize(context['comment_created_at']),
        'thread_created_at': date.deserialize(context['thread_created_at'])
    })
    return message_context
Example #23
0
def _build_message_context(context):
    message_context = get_base_template_context(context['site'])
    message_context.update(context)
    thread_author = User.objects.get(id=context['thread_author_id'])
    comment_author = User.objects.get(id=context['comment_author_id'])
    message_context.update({
        'thread_username': thread_author.username,
        'comment_username': comment_author.username,
        'post_link': _get_thread_url(context),
        'comment_created_at': date.deserialize(context['comment_created_at']),
        'thread_created_at': date.deserialize(context['thread_created_at'])
    })
    return message_context
Example #24
0
def _build_message_context(context):
    message_context = get_base_template_context(context['site'])
    message_context.update(context)
    thread_author = User.objects.get(id=context['thread_author_id'])
    comment_author = User.objects.get(id=context['comment_author_id'])
    message_context.update({
        'thread_username': thread_author.username,
        'comment_username': comment_author.username,
        'post_link': _get_thread_url(context),
        'comment_created_at': date.deserialize(context['comment_created_at']),
        'thread_created_at': date.deserialize(context['thread_created_at'])
    })
    return message_context
Example #25
0
    def get_schedules(self):
        course_key = CourseKey.from_string(self.course_id)
        target_date = self.target_datetime.date()
        schedules = get_schedules_with_due_date(course_key, target_date).filter(
            self.experience_filter,
            active=True,
            enrollment__user__is_active=True,
        )

        template_context = get_base_template_context(self.site)
        for schedule in schedules:
            enrollment = schedule.enrollment
            course = schedule.enrollment.course
            user = enrollment.user
            start_date = schedule.start_date
            LOG.info(u'Received a schedule for user {} in course {} for date {}'.format(
                user.username,
                self.course_id,
                target_date,
            ))

            try:
                week_highlights, week_num = get_next_section_highlights(user, course.id, start_date, target_date)
            except CourseUpdateDoesNotExist:
                LOG.warning(
                    u'Weekly highlights for user {} of course {} does not exist or is disabled'.format(
                        user, course.id
                    )
                )
                # continue to the next schedule, don't yield an email for this one
                continue
            unsubscribe_url = None
            if (COURSE_UPDATE_SHOW_UNSUBSCRIBE_WAFFLE_SWITCH.is_enabled() and
                    'bulk_email_optout' in settings.ACE_ENABLED_POLICIES):
                unsubscribe_url = reverse('bulk_email_opt_out', kwargs={
                    'token': UsernameCipher.encrypt(user.username),
                    'course_id': str(enrollment.course_id),
                })

            template_context.update({
                'course_name': course.display_name,
                'course_url': _get_trackable_course_home_url(enrollment.course_id),
                'week_num': week_num,
                'week_highlights': week_highlights,
                # This is used by the bulk email optout policy
                'course_ids': [str(enrollment.course_id)],
                'unsubscribe_url': unsubscribe_url,
            })
            template_context.update(_get_upsell_information_for_schedule(user, schedule))

            yield (user, enrollment.course.closest_released_language, template_context, course.self_paced)
Example #26
0
def send_course_enrollment_email_for_user(self, site_id, user_id, course_id,
                                          message_type):
    """
    Send out a course enrollment email for the given user.
    Arguments:
        site_id: Django Site object id
        user_id: Django User object id
        course_id: Course id
        message_type: 'enroll' or 'unenroll'
    """
    site = Site.objects.get(id=site_id)
    user = User.objects.get(id=user_id)
    with emulate_http_request(site=site, user=user):
        course_key = CourseKey.from_string(course_id)
        course = modulestore().get_course(course_key)
        request = get_current_request()
        message_context = get_base_template_context(site)
        message_context.update({
            'request':
            request,
            'platform_name':
            configuration_helpers.get_value('PLATFORM_NAME',
                                            settings.PLATFORM_NAME),
            'site_name':
            configuration_helpers.get_value('SITE_NAME', settings.SITE_NAME),
            'username':
            user.username,
            'course_name':
            course.display_name,
            'course_url':
            '{protocol}://{site}{link}'.format(
                protocol='https' if request.is_secure() else 'http',
                site=configuration_helpers.get_value('SITE_NAME',
                                                     settings.SITE_NAME),
                link=reverse('course_root', kwargs={'course_id': course_id})),
        })

        ace_messages_dict = {
            'enroll': CourseEnrollment,
            'unenroll': CourseUnenrollment,
        }
        message_class = ace_messages_dict[message_type]
        msg = message_class().personalize(
            recipient=Recipient(user.username, user.email),
            language=preferences_api.get_user_preference(user, LANGUAGE_KEY),
            user_context=message_context,
        )
        ace.send(msg)
Example #27
0
    def schedules_for_bin(self):
        schedules = self.get_schedules_with_target_date_by_bin_and_orgs()
        template_context = get_base_template_context(self.site)

        for (user, user_schedules) in groupby(schedules, lambda s: s.enrollment.user):
            user_schedules = list(user_schedules)
            course_id_strs = [str(schedule.enrollment.course_id) for schedule in user_schedules]

            # This is used by the bulk email optout policy
            template_context['course_ids'] = course_id_strs

            first_schedule = user_schedules[0]
            try:
                template_context.update(self.get_template_context(user, user_schedules))
            except InvalidContextError:
                continue

            yield (user, first_schedule.enrollment.course.closest_released_language, template_context)
Example #28
0
def _build_message_context(context):
    message_context = get_base_template_context(context['site'])
    message_context.update(context)
    thread_author = User.objects.get(id=context['thread_author_id'])
    comment_author = User.objects.get(id=context['comment_author_id'])
    message_context.update({
        'thread_username':
        thread_author.username,
        'comment_username':
        comment_author.username,
        # 'post_link': _get_thread_url(context),    #Added by Mahendra
        'comment_created_at':
        date.deserialize(context['comment_created_at']),
        'thread_created_at':
        date.deserialize(context['thread_created_at']),
        'from_address':
        'DocMode <*****@*****.**> '
    })
    return message_context
Example #29
0
def send_verification_confirmation_email(context):
    """Send an email confirming that the user submitted photos for initial verification."""
    site = Site.objects.get_current()
    message_context = get_base_template_context(site)
    message_context.update(context)
    user = context['user']
    try:
        with emulate_http_request(site=site, user=user):
            msg = VerificationSubmitted(context=message_context).personalize(
                recipient=Recipient(user.id, user.email),
                language=get_user_preference(user, LANGUAGE_KEY),
                user_context={'full_name': user.profile.name}
            )
            ace.send(msg)
            log.info('Verification confirmation email sent to user: %r', user.username)
            return True
    except Exception:  # pylint: disable=broad-except
        log.exception('Could not send email for verification confirmation to user %s', user.username)
        return False
Example #30
0
def send_proctoring_requirements_email(context):
    """Send an email with proctoring requirements for a course enrollment"""
    site = Site.objects.get_current()
    message_context = get_base_template_context(site)
    message_context.update(context)
    user = context['user']
    try:
        msg = ProctoringRequirements(context=message_context).personalize(
            recipient=Recipient(user.username, user.email),
            language=settings.LANGUAGE_CODE,
            user_context={'full_name': user.profile.name})
        ace.send(msg)
        log.info('Proctoring requirements email sent to user: %r',
                 user.username)
        return True
    except Exception:  # pylint: disable=broad-except
        log.exception(
            'Could not send email for proctoring requirements to user %s',
            user.username)
        return False
Example #31
0
def send_verification_expiry_email(batch_verifications, dry_run=False):
    """
    Spins a task to send verification expiry email to the learners in the batch using edx_ace
    If the email is successfully sent change the expiry_email_date to reflect when the
    email was sent
    """
    if dry_run:
        logger.info(
            u"This was a dry run, no email was sent. For the actual run email would have been sent "
            u"to {} learner(s)".format(len(batch_verifications)))
        return

    site = Site.objects.get_current()
    message_context = get_base_template_context(site)
    message_context.update({
        'platform_name':
        settings.PLATFORM_NAME,
        'lms_verification_link':
        '{}{}'.format(settings.LMS_ROOT_URL,
                      reverse("verify_student_reverify")),
        'help_center_link':
        settings.ID_VERIFICATION_SUPPORT_LINK
    })

    expiry_email = VerificationExpiry(context=message_context)
    users = User.objects.filter(
        pk__in=[verification.user_id for verification in batch_verifications])

    for verification in batch_verifications:
        user = users.get(pk=verification.user_id)
        with emulate_http_request(site=site, user=user):
            msg = expiry_email.personalize(
                recipient=Recipient(user.username, user.email),
                language=get_user_preference(user, LANGUAGE_KEY),
                user_context={
                    'full_name': user.profile.name,
                })
            ace.send(msg)
            verification_qs = SoftwareSecurePhotoVerification.objects.filter(
                pk=verification.pk)
            verification_qs.update(expiry_email_date=datetime.now(UTC))
Example #32
0
def send_verification_approved_email(context):
    """
    Sends email to a learner when ID verification has been approved.
    """
    site = Site.objects.get_current()
    message_context = get_base_template_context(site)
    message_context.update(context)
    user = context['user']
    try:
        with emulate_http_request(site=site, user=user):
            msg = VerificationApproved(context=message_context).personalize(
                recipient=Recipient(user.id, user.email),
                language=get_user_preference(user, LANGUAGE_KEY),
                user_context={'full_name': user.profile.name}
            )
            ace.send(msg)
            log.info('Verification approved email sent to user: %r', user.username)
            return True
    except Exception:  # pylint: disable=broad-except
        log.exception('Could not send email for verification approved to user %s', user.username)
        return False
def send_verification_expiry_email(batch_verifications, dry_run=False):
    """
    Spins a task to send verification expiry email to the learners in the batch using edx_ace
    If the email is successfully sent change the expiry_email_date to reflect when the
    email was sent
    """
    if dry_run:
        logger.info(
            u"This was a dry run, no email was sent. For the actual run email would have been sent "
            u"to {} learner(s)".format(len(batch_verifications))
        )
        return

    site = Site.objects.get_current()
    message_context = get_base_template_context(site)
    message_context.update({
        'platform_name': settings.PLATFORM_NAME,
        'lms_verification_link': '{}{}'.format(settings.LMS_ROOT_URL, reverse("verify_student_reverify")),
        'help_center_link': settings.ID_VERIFICATION_SUPPORT_LINK
    })

    expiry_email = VerificationExpiry(context=message_context)
    users = User.objects.filter(pk__in=[verification.user_id for verification in batch_verifications])

    for verification in batch_verifications:
        user = users.get(pk=verification.user_id)
        with emulate_http_request(site=site, user=user):
            msg = expiry_email.personalize(
                recipient=Recipient(user.username, user.email),
                language=get_user_preference(user, LANGUAGE_KEY),
                user_context={
                    'full_name': user.profile.name,
                }
            )
            ace.send(msg)
            verification_qs = SoftwareSecurePhotoVerification.objects.filter(pk=verification.pk)
            verification_qs.update(expiry_email_date=now())
Example #34
0
def _send_course_email(entry_id, email_id, to_list, global_email_context, subtask_status):  # lint-amnesty, pylint: disable=too-many-statements
    """
    Performs the email sending task.

    Sends an email to a list of recipients.

    Inputs are:
      * `entry_id`: id of the InstructorTask object to which progress should be recorded.
      * `email_id`: id of the CourseEmail model that is to be emailed.
      * `to_list`: list of recipients.  Each is represented as a dict with the following keys:
        - 'profile__name': full name of User.
        - 'email': email address of User.
        - 'pk': primary key of User model.
      * `global_email_context`: dict containing values that are unique for this email but the same
        for all recipients of this email.  This dict is to be used to fill in slots in email
        template.  It does not include 'name' and 'email', which will be provided by the to_list.
      * `subtask_status` : object of class SubtaskStatus representing current status.

    Sends to all addresses contained in to_list that are not also in the Optout table.
    Emails are sent multi-part, in both plain text and html.

    Returns a tuple of two values:
      * First value is a SubtaskStatus object which represents current progress at the end of this call.

      * Second value is an exception returned by the innards of the method, indicating a fatal error.
        In this case, the number of recipients that were not sent have already been added to the
        'failed' count above.
    """
    # Get information from current task's request:
    parent_task_id = InstructorTask.objects.get(pk=entry_id).task_id
    task_id = subtask_status.task_id
    total_recipients = len(to_list)
    recipient_num = 0
    total_recipients_successful = 0
    total_recipients_failed = 0
    recipients_info = Counter()

    log.info(
        f"BulkEmail ==> Task: {parent_task_id}, SubTask: {task_id}, EmailId: {email_id}, "
        f"TotalRecipients: {total_recipients}"
    )

    try:
        course_email = CourseEmail.objects.get(id=email_id)
    except CourseEmail.DoesNotExist as exc:
        log.exception(
            f"BulkEmail ==> Task: {parent_task_id}, SubTask: {task_id}, EmailId: {email_id}, Could not find email to "
            "send."
        )
        raise exc

    # Exclude optouts (if not a retry):
    # Note that we don't have to do the optout logic at all if this is a retry,
    # because we have presumably already performed the optout logic on the first
    # attempt.  Anyone on the to_list on a retry has already passed the filter
    # that existed at that time, and we don't need to keep checking for changes
    # in the Optout list.
    if subtask_status.get_retry_count() == 0:
        to_list, num_optout = _filter_optouts_from_recipients(to_list, course_email.course_id)
        subtask_status.increment(skipped=num_optout)

    course_title = global_email_context['course_title']
    course_language = global_email_context['course_language']

    # If EMAIL_USE_COURSE_ID_FROM_FOR_BULK is False, use the default email from address.
    # Otherwise compute a custom from address
    if not is_email_use_course_id_from_for_bulk_enabled():
        from_addr = settings.BULK_EMAIL_DEFAULT_FROM_EMAIL or settings.DEFAULT_FROM_EMAIL
    else:
        # use the email from address in the CourseEmail, if it is present, otherwise compute it.
        from_addr = course_email.from_addr or _get_source_address(course_email.course_id, course_title, course_language)

    site = Site.objects.get_current()
    try:
        connection = get_connection()
        connection.open()

        # Define context values to use in all course emails:
        email_context = {'name': '', 'email': '', 'course_email': course_email, 'from_address': from_addr}
        template_context = get_base_template_context(site)
        email_context.update(global_email_context)
        email_context.update(template_context)

        start_time = time.time()
        while to_list:
            # Update context with user-specific values from the user at the end of the list.
            # At the end of processing this user, they will be popped off of the to_list.
            # That way, the to_list will always contain the recipients remaining to be emailed.
            # This is convenient for retries, which will need to send to those who haven't
            # yet been emailed, but not send to those who have already been sent to.
            recipient_num += 1
            current_recipient = to_list[-1]
            email = current_recipient['email']
            user_id = current_recipient['pk']
            profile_name = current_recipient['profile__name']
            if _has_non_ascii_characters(email):
                to_list.pop()
                total_recipients_failed += 1
                log.warning(
                    f"BulkEmail ==> Skipping course email to user {current_recipient['pk']} with email_id {email_id}. "
                    "The email address contains non-ASCII characters."
                )
                subtask_status.increment(failed=1)
                continue

            email_context['email'] = email
            email_context['name'] = profile_name
            email_context['user_id'] = user_id
            email_context['course_id'] = course_email.course_id
            email_context['unsubscribe_link'] = get_unsubscribed_link(current_recipient['username'],
                                                                      str(course_email.course_id))

            if is_bulk_email_edx_ace_enabled():
                message = ACEEmail(site, email_context)
            else:
                message = DjangoEmail(connection, course_email, email_context)
            # Throttle if we have gotten the rate limiter.  This is not very high-tech,
            # but if a task has been retried for rate-limiting reasons, then we sleep
            # for a period of time between all emails within this task.  Choice of
            # the value depends on the number of workers that might be sending email in
            # parallel, and what the SES throttle rate is.
            if subtask_status.retried_nomax > 0:
                sleep(settings.BULK_EMAIL_RETRY_DELAY_BETWEEN_SENDS)

            try:
                log.info(
                    f"BulkEmail ==> Task: {parent_task_id}, SubTask: {task_id}, EmailId: {email_id}, Recipient num: "
                    f"{recipient_num}/{total_recipients}, Recipient UserId: {current_recipient['pk']}"
                )
                message.send()
            except (SMTPDataError, SMTPSenderRefused) as exc:
                # According to SMTP spec, we'll retry error codes in the 4xx range.  5xx range indicates hard failure.
                total_recipients_failed += 1
                log.exception(
                    f"BulkEmail ==> Status: Failed({exc.smtp_error}), Task: {parent_task_id}, SubTask: {task_id}, "
                    f"EmailId: {email_id}, Recipient num: {recipient_num}/{total_recipients}, Recipient UserId: "
                    f"{current_recipient['pk']}"
                )
                if exc.smtp_code >= 400 and exc.smtp_code < 500:  # lint-amnesty, pylint: disable=no-else-raise
                    # This will cause the outer handler to catch the exception and retry the entire task.
                    raise exc
                else:
                    # This will fall through and not retry the message.
                    log.warning(
                        f"BulkEmail ==> Task: {parent_task_id}, SubTask: {task_id}, EmailId: {email_id}, Recipient "
                        f"num: {recipient_num}/{total_recipients}, Email not delievered to user "
                        f"{current_recipient['pk']} due to error: {exc.smtp_error}"
                    )
                    subtask_status.increment(failed=1)

            except SINGLE_EMAIL_FAILURE_ERRORS as exc:
                # This will fall through and not retry the message.
                total_recipients_failed += 1
                log.exception(
                    f"BulkEmail ==> Status: Failed(SINGLE_EMAIL_FAILURE_ERRORS), Task: {parent_task_id}, SubTask: "
                    f"{task_id}, EmailId: {email_id}, Recipient num: {recipient_num}/{total_recipients}, Recipient "
                    f"UserId: {current_recipient['pk']}"
                )
                subtask_status.increment(failed=1)

            else:
                total_recipients_successful += 1
                log.info(
                    f"BulkEmail ==> Status: Success, Task: {parent_task_id}, SubTask: {task_id}, EmailId: {email_id}, "
                    f"Recipient num: {recipient_num}/{total_recipients}, Recipient UserId: {current_recipient['pk']}"
                )
                if settings.BULK_EMAIL_LOG_SENT_EMAILS:
                    log.info(f"Email with id {email_id} sent to user {current_recipient['pk']}")
                else:
                    log.debug(f"Email with id {email_id} sent to user {current_recipient['pk']}")
                subtask_status.increment(succeeded=1)

            # Pop the user that was emailed off the end of the list only once they have
            # successfully been processed.  (That way, if there were a failure that
            # needed to be retried, the user is still on the list.)
            recipients_info[email] += 1
            to_list.pop()

        log.info(
            f"BulkEmail ==> Task: {parent_task_id}, SubTask: {task_id}, EmailId: {email_id}, Total Successful "
            f"Recipients: {total_recipients_successful}/{total_recipients}, Failed Recipients: "
            f"{total_recipients_failed}/{total_recipients}, Time Taken: {time.time() - start_time}"
        )

        duplicate_recipients = [f"{email} ({repetition})"
                                for email, repetition in recipients_info.most_common() if repetition > 1]
        if duplicate_recipients:
            log.info(
                f"BulkEmail ==> Task: {parent_task_id}, SubTask: {task_id}, EmailId: {email_id}, Total Duplicate "
                f"Recipients [{len(duplicate_recipients)}]"
            )

    except INFINITE_RETRY_ERRORS as exc:
        # Increment the "retried_nomax" counter, update other counters with progress to date,
        # and set the state to RETRY:
        subtask_status.increment(retried_nomax=1, state=RETRY)
        return _submit_for_retry(
            entry_id, email_id, to_list, global_email_context, exc, subtask_status, skip_retry_max=True
        )

    except LIMITED_RETRY_ERRORS as exc:
        # Errors caught here cause the email to be retried.  The entire task is actually retried
        # without popping the current recipient off of the existing list.
        # Errors caught are those that indicate a temporary condition that might succeed on retry.
        # Increment the "retried_withmax" counter, update other counters with progress to date,
        # and set the state to RETRY:
        subtask_status.increment(retried_withmax=1, state=RETRY)
        return _submit_for_retry(
            entry_id, email_id, to_list, global_email_context, exc, subtask_status, skip_retry_max=False
        )

    except BULK_EMAIL_FAILURE_ERRORS as exc:
        num_pending = len(to_list)
        log.exception(
            f"Task {task_id}: email with id {email_id} caused send_course_email task to fail with 'fatal' exception. "
            f"{num_pending} emails unsent."
        )
        # Update counters with progress to date, counting unsent emails as failures,
        # and set the state to FAILURE:
        subtask_status.increment(failed=num_pending, state=FAILURE)
        return subtask_status, exc

    except Exception as exc:  # pylint: disable=broad-except
        # Errors caught here cause the email to be retried.  The entire task is actually retried
        # without popping the current recipient off of the existing list.
        # These are unexpected errors.  Since they might be due to a temporary condition that might
        # succeed on retry, we give them a retry.
        log.exception(
            f"Task {task_id}: email with id {email_id} caused send_course_email task to fail with unexpected "
            "exception. Generating retry."
        )
        # Increment the "retried_withmax" counter, update other counters with progress to date,
        # and set the state to RETRY:
        subtask_status.increment(retried_withmax=1, state=RETRY)
        return _submit_for_retry(
            entry_id, email_id, to_list, global_email_context, exc, subtask_status, skip_retry_max=False
        )

    else:
        # All went well.  Update counters with progress to date,
        # and set the state to SUCCESS:
        subtask_status.increment(state=SUCCESS)
        # Successful completion is marked by an exception value of None.
        return subtask_status, None
    finally:
        # Clean up at the end.
        connection.close()
Example #35
0
def confirm_email_change(request, key):  # pylint: disable=unused-argument
    """
    User requested a new e-mail. This is called when the activation
    link is clicked. We confirm with the old e-mail, and update
    """
    if waffle().is_enabled(PREVENT_AUTH_USER_WRITES):
        return render_to_response('email_change_failed.html',
                                  {'err_msg': SYSTEM_MAINTENANCE_MSG})

    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.username, 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.username, 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
Example #36
0
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.username, new_email),
            language=preferences_api.get_user_preference(user, LANGUAGE_KEY),
            user_context=message_context,
        )
    else:
        msg = EmailChange().personalize(
            recipient=Recipient(user.username, 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(u'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.'))

    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,
            })
Example #37
0
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:
            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."))
Example #38
0
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:
            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 = Site.objects.get_current()
                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."))
Example #39
0
    def get_schedules(self):
        """
        Grabs possible schedules that could receive a Course Next Section Update and if a
        next section highlight is applicable for the user, yields information needed to
        send the next section highlight email.
        """
        target_date = self.target_datetime.date()
        course_duration = get_expected_duration(self.course_id)
        schedules = Schedule.objects.select_related('enrollment').filter(
            self.experience_filter,
            enrollment__is_active=True,
            enrollment__course_id=self.course_id,
            enrollment__user__is_active=True,
            start_date__gte=target_date - course_duration,
            start_date__lt=target_date,
        )

        template_context = get_base_template_context(self.site)
        for schedule in schedules:
            course = schedule.enrollment.course
            # We don't want to show any updates if the course has ended so we short circuit here.
            if course.end and course.end.date() <= target_date:
                return

            # Next Section Updates are only for Self-paced courses since it uses Personalized
            # Learner Schedule logic. See CourseUpdateResolver for Instructor-paced updates
            if not course.self_paced:
                continue

            user = schedule.enrollment.user
            start_date = max(filter(None, (schedule.start_date, course.start)))
            LOG.info('Received a schedule for user {} in course {} for date {}'.format(
                user.username, self.course_id, target_date,
            ))

            try:
                week_highlights, week_num = get_next_section_highlights(user, course.id, start_date, target_date)
                # (None, None) is returned when there is no section with a due date of the target_date
                if week_highlights is None:
                    continue
            except CourseUpdateDoesNotExist as e:
                log_message = self.log_prefix + ': ' + str(e)
                LOG.warning(log_message)
                # continue to the next schedule, don't yield an email for this one
                continue
            unsubscribe_url = None
            if (COURSE_UPDATE_SHOW_UNSUBSCRIBE_WAFFLE_SWITCH.is_enabled() and
                    'bulk_email_optout' in settings.ACE_ENABLED_POLICIES):
                unsubscribe_url = reverse('bulk_email_opt_out', kwargs={
                    'token': UsernameCipher.encrypt(user.username),
                    'course_id': str(course.id),
                })

            template_context.update({
                'course_name': course.display_name,
                'course_url': _get_trackable_course_home_url(course.id),
                'week_num': week_num,
                'week_highlights': week_highlights,
                # This is used by the bulk email optout policy
                'course_ids': [str(course.id)],
                'unsubscribe_url': unsubscribe_url,
            })
            template_context.update(_get_upsell_information_for_schedule(user, schedule))

            yield (user, course.closest_released_language, template_context)
Example #40
0
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.username, new_email),
            language=preferences_api.get_user_preference(user, LANGUAGE_KEY),
            user_context=message_context,
        )
    else:
        msg = EmailChange().personalize(
            recipient=Recipient(user.username, 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(u'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.'))

    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,
            }
        )
Example #41
0
def confirm_email_change(request, key):  # pylint: disable=unused-argument
    """
    User requested a new e-mail. This is called when the activation
    link is clicked. We confirm with the old e-mail, and update
    """
    if waffle().is_enabled(PREVENT_AUTH_USER_WRITES):
        return render_to_response('email_change_failed.html', {'err_msg': SYSTEM_MAINTENANCE_MSG})

    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.username, 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.username, 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