diff --git a/cms/envs/devstack.py b/cms/envs/devstack.py index 47aa91d929..8f675cdcf6 100644 --- a/cms/envs/devstack.py +++ b/cms/envs/devstack.py @@ -27,8 +27,7 @@ for pkg_name in ['track.contexts', 'track.middleware', 'dd.dogapi']: ################################ EMAIL ######################################## -EMAIL_BACKEND = 'django.core.mail.backends.filebased.EmailBackend' -EMAIL_FILE_PATH = '/edx/src/ace_messages/' +EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' ################################# LMS INTEGRATION ############################# diff --git a/common/djangoapps/student/forms.py b/common/djangoapps/student/forms.py index 5b5b85ae18..ae12931518 100644 --- a/common/djangoapps/student/forms.py +++ b/common/djangoapps/student/forms.py @@ -10,22 +10,15 @@ from django.contrib.auth.forms import PasswordResetForm from django.contrib.auth.hashers import UNUSABLE_PASSWORD_PREFIX from django.contrib.auth.models import User from django.contrib.auth.tokens import default_token_generator -from django.contrib.sites.models import Site from django.core.exceptions import ValidationError -from django.core.urlresolvers import reverse -from django.core.validators import RegexValidator, slug_re from django.forms import widgets +from django.template import loader from django.utils.http import int_to_base36 from django.utils.translation import ugettext_lazy as _ +from django.core.validators import RegexValidator, slug_re -from edx_ace import ace -from edx_ace.recipient import Recipient -from openedx.core.djangoapps.ace_common.template_context import get_base_template_context -from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangoapps.user_api import accounts as accounts_settings -from openedx.core.djangoapps.user_api.preferences.api import get_user_preference -from student.message_types import PasswordReset from student.models import CourseEnrollmentAllowed from util.password_policy_validators import password_max_length, password_min_length, validate_password @@ -54,39 +47,41 @@ class PasswordResetFormNoActive(PasswordResetForm): raise forms.ValidationError(self.error_messages['unusable']) return email - def save(self, # pylint: disable=arguments-differ - use_https=False, - token_generator=default_token_generator, - request=None, - **_kwargs): + def save( + self, + subject_template_name='emails/password_reset_subject.txt', + email_template_name='registration/password_reset_email.html', + use_https=False, + token_generator=default_token_generator, + from_email=configuration_helpers.get_value('email_from_address', settings.DEFAULT_FROM_EMAIL), + request=None + ): """ Generates a one-use only link for resetting password and sends to the user. """ + # This import is here because we are copying and modifying the .save from Django 1.4.5's + # django.contrib.auth.forms.PasswordResetForm directly, which has this import in this place. + from django.core.mail import send_mail 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, + site_name = configuration_helpers.get_value( + 'SITE_NAME', + settings.SITE_NAME ) - ace.send(msg) + context = { + 'email': user.email, + 'site_name': site_name, + 'uid': int_to_base36(user.id), + 'user': user, + 'token': token_generator.make_token(user), + 'protocol': 'https' if use_https else 'http', + 'platform_name': configuration_helpers.get_value('platform_name', settings.PLATFORM_NAME) + } + subject = loader.render_to_string(subject_template_name, context) + # Email subject *must not* contain newlines + subject = subject.replace('\n', '') + email = loader.render_to_string(email_template_name, context) + send_mail(subject, email, from_email, [user.email]) class TrueCheckbox(widgets.CheckboxInput): diff --git a/common/djangoapps/student/message_types.py b/common/djangoapps/student/message_types.py deleted file mode 100644 index 0aac833376..0000000000 --- a/common/djangoapps/student/message_types.py +++ /dev/null @@ -1,17 +0,0 @@ -""" -ACE message types for the student module. -""" -from django.conf import settings - -from edx_ace.message import MessageType -from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers - - -class PasswordReset(MessageType): - def __init__(self, *args, **kwargs): - super(PasswordReset, self).__init__(*args, **kwargs) - - self.options['transactional'] = True - self.options['from_address'] = configuration_helpers.get_value( - 'email_from_address', settings.DEFAULT_FROM_EMAIL - ) diff --git a/common/djangoapps/student/tests/test_configuration_overrides.py b/common/djangoapps/student/tests/test_configuration_overrides.py index 01fefe3a09..4b14a6c340 100644 --- a/common/djangoapps/student/tests/test_configuration_overrides.py +++ b/common/djangoapps/student/tests/test_configuration_overrides.py @@ -15,7 +15,6 @@ FAKE_SITE = { "university": "fakeuniversity", "course_org_filter": "fakeorg", "platform_name": "Fake University", - "PLATFORM_NAME": "Fake University", "email_from_address": "no-reply@fakeuniversity.com", "REGISTRATION_EXTRA_FIELDS": { "address1": "required", diff --git a/common/djangoapps/student/tests/test_reset_password.py b/common/djangoapps/student/tests/test_reset_password.py index 1755ecac8f..6e45685a09 100644 --- a/common/djangoapps/student/tests/test_reset_password.py +++ b/common/djangoapps/student/tests/test_reset_password.py @@ -11,7 +11,6 @@ from django.contrib.auth.hashers import UNUSABLE_PASSWORD_PREFIX from django.contrib.auth.models import User from django.contrib.auth.tokens import default_token_generator from django.core.cache import cache -from django.core import mail from django.core.urlresolvers import reverse from django.test.client import RequestFactory from django.test.utils import override_settings @@ -111,12 +110,13 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase): cache.clear() @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', "Test only valid in LMS") - @ddt.data('plain_text', 'html') - def test_reset_password_email(self, body_type): + @patch('django.core.mail.send_mail') + @patch('student.views.management.render_to_string', Mock(side_effect=mock_render_to_string, autospec=True)) + def test_reset_password_email(self, send_email): """Tests contents of reset password email, and that user is not active""" + good_req = self.request_factory.post('/password_reset/', {'email': self.user.email}) good_req.user = self.user - good_req.site = Mock(domain='example.com') dop_client = ClientFactory() dop_access_token = AccessTokenFactory(user=self.user, client=dop_client) RefreshTokenFactory(user=self.user, client=dop_client, access_token=dop_access_token) @@ -130,35 +130,26 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase): self.assertFalse(dot_models.AccessToken.objects.filter(user=self.user).exists()) self.assertFalse(dot_models.RefreshToken.objects.filter(user=self.user).exists()) obj = json.loads(good_resp.content) - self.assertTrue(obj['success']) - self.assertIn('e-mailed you instructions for setting your password', obj['value']) + self.assertEquals(obj, { + 'success': True, + 'value': "('registration/password_reset_done.html', [])", + }) - from_email = configuration_helpers.get_value('email_from_address', settings.DEFAULT_FROM_EMAIL) - sent_message = mail.outbox[0] - - bodies = { - 'plain_text': sent_message.body, - 'html': sent_message.alternatives[0][0], - } - - body = bodies[body_type] - - self.assertIn("Password reset", sent_message.subject) - self.assertIn("You're receiving this e-mail because you requested a password reset", body) - self.assertEquals(sent_message.from_email, from_email) - self.assertEquals(len(sent_message.to), 1) - self.assertIn(self.user.email, sent_message.to) + (subject, msg, from_addr, to_addrs) = send_email.call_args[0] + self.assertIn("Password reset", subject) + self.assertIn("You're receiving this e-mail because you requested a password reset", msg) + self.assertEquals(from_addr, configuration_helpers.get_value('email_from_address', settings.DEFAULT_FROM_EMAIL)) + self.assertEquals(len(to_addrs), 1) + self.assertIn(self.user.email, to_addrs) self.assert_event_emitted( SETTING_CHANGE_INITIATED, user_id=self.user.id, setting=u'password', old=None, new=None, ) - # Test that the user is not active + #test that the user is not active self.user = User.objects.get(pk=self.user.pk) self.assertFalse(self.user.is_active) - - self.assertIn('password_reset_confirm/', body) - re.search(r'password_reset_confirm/(?P[0-9A-Za-z]+)-(?P.+)/', body).groupdict() + re.search(r'password_reset_confirm/(?P[0-9A-Za-z]+)-(?P.+)/', msg).groupdict() @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', "Test only valid in LMS") @patch('django.core.mail.send_mail') @@ -171,7 +162,6 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase): req = self.request_factory.post( '/password_reset/', {'email': self.user.email} ) - req.site = Mock(domain='example.com') req.is_secure = Mock(return_value=is_secure) req.user = self.user password_reset(req) @@ -199,7 +189,6 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase): '/password_reset/', {'email': self.user.email} ) req.user = self.user - req.site = Mock(domain='example.com') password_reset(req) _, msg, _, _ = send_email.call_args[0] @@ -217,8 +206,8 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase): @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', "Test only valid in LMS") @patch("openedx.core.djangoapps.site_configuration.helpers.get_value", fake_get_value) - @ddt.data('plain_text', 'html') - def test_reset_password_email_configuration_override(self, body_type): + @patch('django.core.mail.send_mail') + def test_reset_password_email_configuration_override(self, send_email): """ Tests that the right url domain and platform name is included in the reset password email @@ -227,28 +216,18 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase): '/password_reset/', {'email': self.user.email} ) req.get_host = Mock(return_value=None) - req.site = Mock(domain='example.com') req.user = self.user + password_reset(req) + _, msg, from_addr, _ = send_email.call_args[0] - with patch('crum.get_current_request', return_value=req): - password_reset(req) + reset_msg = "you requested a password reset for your user account at {}".format(fake_get_value('platform_name')) - sent_message = mail.outbox[0] - bodies = { - 'plain_text': sent_message.body, - 'html': sent_message.alternatives[0][0], - } - - body = bodies[body_type] - - reset_msg = "you requested a password reset for your user account at {}".format(fake_get_value('PLATFORM_NAME')) - - self.assertIn(reset_msg, body) + self.assertIn(reset_msg, msg) self.assert_event_emitted( SETTING_CHANGE_INITIATED, user_id=self.user.id, setting=u'password', old=None, new=None ) - self.assertEqual(sent_message.from_email, "no-reply@fakeuniversity.com") + self.assertEqual(from_addr, "no-reply@fakeuniversity.com") @ddt.data( ('invalidUid', 'invalid_token'), @@ -372,7 +351,6 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase): '/password_reset/', {'email': self.user.email} ) req.user = self.user - req.site = Mock(domain='example.com') password_reset(req) subj, _, _, _ = send_email.call_args[0] diff --git a/common/templates/student/edx_ace/passwordreset/email/body.html b/common/templates/student/edx_ace/passwordreset/email/body.html deleted file mode 100644 index 1b67cf1c63..0000000000 --- a/common/templates/student/edx_ace/passwordreset/email/body.html +++ /dev/null @@ -1,40 +0,0 @@ -{% extends 'ace_common/edx_ace/common/base_body.html' %} - -{% load i18n %} -{% load static %} -{% block content %} - - - - -
-

- {% trans "Password Reset" %} -

-

- {% blocktrans %}You're receiving this e-mail because you requested a password reset for your user account at {{ platform_name }}.{% endblocktrans %} -
-

- - {% if failed %} -

- {% blocktrans %}However, there is currently no user account associated with your email address: {{ email_address }}.{% endblocktrans %} -
-

- -

- {% trans "If you did not request this change, you can ignore this email." %} -
-

- {% else %} -

- {% trans "If you didn't request this change, you can disregard this email - we have not yet reset your password." %} -
-

- - {% trans "Change my Password" as course_cta_text %} - - {% include "ace_common/edx_ace/common/return_to_course_cta.html" with course_cta_text=course_cta_text course_cta_url=reset_link %} - {% endif %} -
-{% endblock %} diff --git a/common/templates/student/edx_ace/passwordreset/email/from_name.txt b/common/templates/student/edx_ace/passwordreset/email/from_name.txt deleted file mode 100644 index dcbc23c004..0000000000 --- a/common/templates/student/edx_ace/passwordreset/email/from_name.txt +++ /dev/null @@ -1 +0,0 @@ -{{ platform_name }} diff --git a/common/templates/student/edx_ace/passwordreset/email/head.html b/common/templates/student/edx_ace/passwordreset/email/head.html deleted file mode 100644 index 366ada7ad9..0000000000 --- a/common/templates/student/edx_ace/passwordreset/email/head.html +++ /dev/null @@ -1 +0,0 @@ -{% extends 'ace_common/edx_ace/common/base_head.html' %} diff --git a/common/templates/student/edx_ace/passwordreset/email/subject.txt b/common/templates/student/edx_ace/passwordreset/email/subject.txt deleted file mode 100644 index a568d46de6..0000000000 --- a/common/templates/student/edx_ace/passwordreset/email/subject.txt +++ /dev/null @@ -1,4 +0,0 @@ -{% load i18n %} -{% autoescape off %} -{% blocktrans trimmed %}Password reset on {{ platform_name }}{% endblocktrans %} -{% endautoescape %} diff --git a/lms/djangoapps/student_account/test/test_views.py b/lms/djangoapps/student_account/test/test_views.py index e5f2b207ec..778f7cc520 100644 --- a/lms/djangoapps/student_account/test/test_views.py +++ b/lms/djangoapps/student_account/test/test_views.py @@ -164,18 +164,13 @@ class StudentAccountUpdateTest(CacheIsolationTestCase, UrlResetMixin): self.assertEqual(len(mail.outbox), 1) # Verify that the body contains the failed password reset message - sent_message = mail.outbox[0] - text_body = sent_message.body - html_body = sent_message.alternatives[0][0] - - for email_body in [text_body, html_body]: - msg = 'However, there is currently no user account associated with your email address: {email}'.format( + email_body = mail.outbox[0].body + self.assertIn( + 'However, there is currently no user account associated with your email address: {email}'.format( email=bad_email - ) - - assert u'reset for your user account at {}'.format(settings.PLATFORM_NAME) in email_body - assert 'password_reset_confirm' not in email_body, 'The link should not be added if user was not found' - assert msg in email_body + ), + email_body, + ) @ddt.data(True, False) def test_password_change_logged_out(self, send_email): diff --git a/lms/djangoapps/student_account/views.py b/lms/djangoapps/student_account/views.py index 1a9d0e9dfa..3b99866d5e 100644 --- a/lms/djangoapps/student_account/views.py +++ b/lms/djangoapps/student_account/views.py @@ -9,22 +9,19 @@ from django.conf import settings from django.contrib import messages from django.contrib.auth import get_user_model from django.contrib.auth.decorators import login_required -from django.contrib.sites.models import Site -from django.core.urlresolvers import reverse +from django.core.mail import send_mail +from django.core.urlresolvers import resolve, reverse from django.http import HttpRequest, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden from django.shortcuts import redirect +from django.template import loader from django.utils.translation import ugettext as _ from django.views.decorators.csrf import ensure_csrf_cookie from django.views.decorators.http import require_http_methods from django_countries import countries import third_party_auth - -from edx_ace import ace -from edx_ace.recipient import Recipient from edxmako.shortcuts import render_to_response from lms.djangoapps.commerce.models import CommerceConfiguration from lms.djangoapps.commerce.utils import EcommerceService -from openedx.core.djangoapps.ace_common.template_context import get_base_template_context from openedx.core.djangoapps.commerce.utils import ecommerce_api_client from openedx.core.djangoapps.external_auth.login_and_register import login as external_auth_login from openedx.core.djangoapps.external_auth.login_and_register import register as external_auth_register @@ -51,7 +48,6 @@ from openedx.features.enterprise_support.utils import ( update_account_settings_context_for_enterprise, ) from student.helpers import destroy_oauth_tokens, get_next_url_for_login_page -from student.message_types import PasswordReset from student.models import UserProfile from student.views import register_user as old_register_view, signin_user as old_login_view from third_party_auth import pipeline @@ -59,7 +55,6 @@ from third_party_auth.decorators import xframe_allow_whitelisted from util.bad_request_rate_limiter import BadRequestRateLimiter from util.date_utils import strftime_localized - AUDIT_LOG = logging.getLogger("audit") log = logging.getLogger(__name__) User = get_user_model() # pylint:disable=invalid-name @@ -227,23 +222,20 @@ def password_change_request_handler(request): # 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({ + context = { 'failed': True, - 'request': request, # Used by google_analytics_tracking_pixel 'email_address': email, - }) + 'platform_name': configuration_helpers.get_value('platform_name', settings.PLATFORM_NAME), - msg = PasswordReset().personalize( - recipient=Recipient(username='', email_address=email), - language=settings.LANGUAGE_CODE, - user_context=message_context, - ) - - ace.send(msg) + } + subject = loader.render_to_string('emails/password_reset_subject.txt', context) + subject = ''.join(subject.splitlines()) + message = loader.render_to_string('registration/password_reset_email.html', context) + from_email = configuration_helpers.get_value('email_from_address', settings.DEFAULT_FROM_EMAIL) + try: + send_mail(subject, message, from_email, [email]) + except Exception: # pylint: disable=broad-except + log.exception(u'Unable to send password reset failure email notification from "%s"', from_email) except UserAPIInternalError as err: log.exception('Error occured during password change for user {email}: {error}' .format(email=email, error=err)) diff --git a/lms/envs/devstack.py b/lms/envs/devstack.py index 7e674a5fac..e4da200040 100644 --- a/lms/envs/devstack.py +++ b/lms/envs/devstack.py @@ -39,8 +39,7 @@ for log_name, log_level in LOG_OVERRIDES: ################################ EMAIL ######################################## -EMAIL_BACKEND = 'django.core.mail.backends.filebased.EmailBackend' -EMAIL_FILE_PATH = '/edx/src/ace_messages/' +EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' ############################ PYFS XBLOCKS SERVICE ############################# # Set configuration for Django pyfilesystem diff --git a/lms/templates/emails/password_reset_subject.txt b/lms/templates/emails/password_reset_subject.txt new file mode 100644 index 0000000000..cf927ce629 --- /dev/null +++ b/lms/templates/emails/password_reset_subject.txt @@ -0,0 +1,4 @@ +{% load i18n %} +{% autoescape off %} +{% blocktrans %}Password reset on {{ platform_name }}{% endblocktrans %} +{% endautoescape %} diff --git a/common/templates/student/edx_ace/passwordreset/email/body.txt b/lms/templates/registration/password_reset_email.html similarity index 85% rename from common/templates/student/edx_ace/passwordreset/email/body.txt rename to lms/templates/registration/password_reset_email.html index 2b316582f2..611863b5b1 100644 --- a/common/templates/student/edx_ace/passwordreset/email/body.txt +++ b/lms/templates/registration/password_reset_email.html @@ -7,12 +7,15 @@ {% trans "If you did not request this change, you can ignore this email." %} {% else %} {% trans "Please go to the following page and choose a new password:" %} - -{{ reset_link }} +{% block reset_link %} +{{ protocol }}://{{ site_name }}{% url 'password_reset_confirm' uidb36=uid token=token %} +{% endblock %} {% trans "If you didn't request this change, you can disregard this email - we have not yet reset your password." %} {% trans "Thanks for using our site!" %} {% endif %} + {% blocktrans %}The {{ platform_name }} Team{% endblocktrans %} + {% endautoescape %} diff --git a/openedx/core/djangoapps/ace_common/apps.py b/openedx/core/djangoapps/ace_common/apps.py index 3c671cc070..68d49deee8 100644 --- a/openedx/core/djangoapps/ace_common/apps.py +++ b/openedx/core/djangoapps/ace_common/apps.py @@ -19,7 +19,7 @@ class AceCommonConfig(AppConfig): ProjectType.LMS: { SettingsType.AWS: {PluginSettings.RELATIVE_PATH: u'settings.aws'}, SettingsType.COMMON: {PluginSettings.RELATIVE_PATH: u'settings.common'}, - SettingsType.DEVSTACK: {PluginSettings.RELATIVE_PATH: u'settings.devstack'}, + SettingsType.DEVSTACK: {PluginSettings.RELATIVE_PATH: u'settings.common'}, } } } diff --git a/openedx/core/djangoapps/ace_common/settings/aws.py b/openedx/core/djangoapps/ace_common/settings/aws.py index bd8c5899ce..56f149f247 100644 --- a/openedx/core/djangoapps/ace_common/settings/aws.py +++ b/openedx/core/djangoapps/ace_common/settings/aws.py @@ -14,10 +14,3 @@ def plugin_settings(settings): 'ACE_CHANNEL_SAILTHRU_API_SECRET', settings.ACE_CHANNEL_SAILTHRU_API_SECRET, ) settings.ACE_ROUTING_KEY = settings.ENV_TOKENS.get('ACE_ROUTING_KEY', settings.ACE_ROUTING_KEY) - - settings.ACE_CHANNEL_DEFAULT_EMAIL = settings.ENV_TOKENS.get( - 'ACE_CHANNEL_DEFAULT_EMAIL', settings.ACE_CHANNEL_DEFAULT_EMAIL - ) - settings.ACE_CHANNEL_TRANSACTIONAL_EMAIL = settings.ENV_TOKENS.get( - 'ACE_CHANNEL_TRANSACTIONAL_EMAIL', settings.ACE_CHANNEL_TRANSACTIONAL_EMAIL - ) diff --git a/openedx/core/djangoapps/ace_common/settings/common.py b/openedx/core/djangoapps/ace_common/settings/common.py index 994034f7b9..c825505c2a 100644 --- a/openedx/core/djangoapps/ace_common/settings/common.py +++ b/openedx/core/djangoapps/ace_common/settings/common.py @@ -1,6 +1,6 @@ def plugin_settings(settings): settings.ACE_ENABLED_CHANNELS = [ - 'django_email' + 'file_email' ] settings.ACE_ENABLED_POLICIES = [ 'bulk_email_optout' @@ -9,8 +9,6 @@ def plugin_settings(settings): settings.ACE_CHANNEL_SAILTHRU_TEMPLATE_NAME = 'Automated Communication Engine Email' settings.ACE_CHANNEL_SAILTHRU_API_KEY = None settings.ACE_CHANNEL_SAILTHRU_API_SECRET = None - settings.ACE_CHANNEL_DEFAULT_EMAIL = 'django_email' - settings.ACE_CHANNEL_TRANSACTIONAL_EMAIL = 'django_email' settings.ACE_ROUTING_KEY = 'edx.core.low' diff --git a/openedx/core/djangoapps/ace_common/settings/devstack.py b/openedx/core/djangoapps/ace_common/settings/devstack.py deleted file mode 100644 index b94f7f41a5..0000000000 --- a/openedx/core/djangoapps/ace_common/settings/devstack.py +++ /dev/null @@ -1,19 +0,0 @@ -""" -Settings for edX ACE on devstack. -""" - -from openedx.core.djangoapps.ace_common.settings import common - - -def plugin_settings(settings): - """ - Override common settings and use `file_email` for better debugging. - """ - common.plugin_settings(settings) - - settings.ACE_ENABLED_CHANNELS = [ - 'file_email' - ] - - settings.ACE_CHANNEL_DEFAULT_EMAIL = 'file_email' - settings.ACE_CHANNEL_TRANSACTIONAL_EMAIL = 'file_email' diff --git a/openedx/core/djangoapps/ace_common/templates/ace_common/edx_ace/common/base_body.html b/openedx/core/djangoapps/ace_common/templates/ace_common/edx_ace/common/base_body.html index 2c5d3efc24..db47420c9b 100644 --- a/openedx/core/djangoapps/ace_common/templates/ace_common/edx_ace/common/base_body.html +++ b/openedx/core/djangoapps/ace_common/templates/ace_common/edx_ace/common/base_body.html @@ -20,9 +20,9 @@ {% block preview_text %}{% endblock %} -{% for image_src in channel.tracker_image_sources %} - -{% endfor %} +{# Note {beacon_src} is not a template variable that is evaluated by the Django template engine. It is evaluated by #} +{# Sailthru when the email is sent. Other email providers would need to replace this variable in the HTML as well. #} + {% google_analytics_tracking_pixel %} @@ -157,13 +157,17 @@ - {% for action_link_url, action_link_text in channel.action_links %} -

- - {{ action_link_text }} - -

- {% endfor %} + {# Note that these variables are evaluated by Sailthru, not the Django template engine #} +

+ + {% trans "View on Web" %} + +

+

+ + {% trans "Unsubscribe from this list" %} + +

diff --git a/openedx/core/djangoapps/schedules/management/commands/tests/send_email_base.py b/openedx/core/djangoapps/schedules/management/commands/tests/send_email_base.py index 8418bc01f3..b682f149dd 100644 --- a/openedx/core/djangoapps/schedules/management/commands/tests/send_email_base.py +++ b/openedx/core/djangoapps/schedules/management/commands/tests/send_email_base.py @@ -7,8 +7,8 @@ import attr import ddt import pytz from django.conf import settings -from edx_ace.channel import ChannelMap, ChannelType -from edx_ace.test_utils import StubPolicy, patch_policies +from edx_ace.channel import ChannelType +from edx_ace.test_utils import StubPolicy, patch_channels, patch_policies from edx_ace.utils.date import serialize from freezegun import freeze_time from mock import Mock, patch @@ -383,16 +383,11 @@ class ScheduleSendEmailTestMixin(FilteredQueryCountMixin): ) patch_policies(self, [StubPolicy([ChannelType.PUSH])]) - mock_channel = Mock( - channel_type=ChannelType.EMAIL, - action_links=[], - tracker_image_sources=[], + name='test_channel', + channel_type=ChannelType.EMAIL ) - - channel_map = ChannelMap([ - ['sailthru', mock_channel], - ]) + patch_channels(self, [mock_channel]) sent_messages = [] with self.settings(TEMPLATES=self._get_template_overrides()): @@ -416,9 +411,8 @@ class ScheduleSendEmailTestMixin(FilteredQueryCountMixin): with self.assertNumQueries(NUM_QUERIES_PER_MESSAGE_DELIVERY): with patch('analytics.track') as mock_analytics_track: - with patch('edx_ace.channel.channels', return_value=channel_map): - self.deliver_task(*sent_messages[0]) - self.assertEqual(mock_analytics_track.call_count, 1) + self.deliver_task(*sent_messages[0]) + self.assertEqual(mock_analytics_track.call_count, 1) self.assertEqual(mock_channel.deliver.call_count, 1) for (_name, (_msg, email), _kwargs) in mock_channel.deliver.mock_calls: diff --git a/openedx/core/djangoapps/user_api/accounts/api.py b/openedx/core/djangoapps/user_api/accounts/api.py index 40918c181a..3520eeacff 100644 --- a/openedx/core/djangoapps/user_api/accounts/api.py +++ b/openedx/core/djangoapps/user_api/accounts/api.py @@ -11,7 +11,6 @@ from django.core.exceptions import ObjectDoesNotExist from django.conf import settings from django.core.validators import validate_email, ValidationError from django.http import HttpResponseForbidden -from openedx.core.djangoapps.theming.helpers import get_current_request from six import text_type from student.models import User, UserProfile, Registration @@ -433,8 +432,7 @@ def request_password_change(email, is_secure): # and email it to the user. form.save( from_email=configuration_helpers.get_value('email_from_address', settings.DEFAULT_FROM_EMAIL), - use_https=is_secure, - request=get_current_request(), + use_https=is_secure ) else: # No user with the provided email address exists. diff --git a/openedx/core/djangoapps/user_api/accounts/tests/test_api.py b/openedx/core/djangoapps/user_api/accounts/tests/test_api.py index 6e3c08bf71..65f4e68b7b 100644 --- a/openedx/core/djangoapps/user_api/accounts/tests/test_api.py +++ b/openedx/core/djangoapps/user_api/accounts/tests/test_api.py @@ -19,7 +19,6 @@ from nose.plugins.attrib import attr from nose.tools import raises from six import iteritems -from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory from openedx.core.djangoapps.user_api.accounts import PRIVATE_VISIBILITY, USERNAME_MAX_LENGTH from openedx.core.djangoapps.user_api.accounts.api import ( activate_account, @@ -432,13 +431,8 @@ class AccountCreationActivationAndPasswordChangeTest(TestCase): activation_key = create_account(self.USERNAME, self.PASSWORD, self.EMAIL) activate_account(activation_key) - request = RequestFactory().post('/password') - request.user = Mock() - request.site = SiteFactory() - - with patch('crum.get_current_request', return_value=request): - # Request a password change - request_password_change(self.EMAIL, self.IS_SECURE) + # Request a password change + request_password_change(self.EMAIL, self.IS_SECURE) # Verify that one email message has been sent self.assertEqual(len(mail.outbox), 1) @@ -462,12 +456,7 @@ class AccountCreationActivationAndPasswordChangeTest(TestCase): # Create an account, but do not activate it create_account(self.USERNAME, self.PASSWORD, self.EMAIL) - request = RequestFactory().post('/password') - request.user = Mock() - request.site = SiteFactory() - - with patch('crum.get_current_request', return_value=request): - request_password_change(self.EMAIL, self.IS_SECURE) + request_password_change(self.EMAIL, self.IS_SECURE) # Verify that the activation email was still sent self.assertEqual(len(mail.outbox), 1) diff --git a/requirements/edx/base.in b/requirements/edx/base.in index 27ed86bf2d..f2f1573d34 100644 --- a/requirements/edx/base.in +++ b/requirements/edx/base.in @@ -64,7 +64,7 @@ django-waffle==0.12.0 django-webpack-loader==0.4.1 djangorestframework-jwt dogapi==1.2.1 # Python bindings to Datadog's API, for metrics gathering -edx-ace==0.1.7 +edx-ace==0.1.6 edx-analytics-data-api-client edx-ccx-keys edx-celeryutils diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 179770d68f..d3208f14fa 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -109,7 +109,7 @@ dm.xmlsec.binding==1.3.3 # via python-saml docopt==0.6.2 docutils==0.14 # via botocore dogapi==1.2.1 -edx-ace==0.1.7 +edx-ace==0.1.6 edx-analytics-data-api-client==0.14.4 edx-ccx-keys==0.2.1 edx-celeryutils==0.2.7 diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index c7165a2b2c..bc802b3aff 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -128,7 +128,7 @@ dm.xmlsec.binding==1.3.3 docopt==0.6.2 docutils==0.14 dogapi==1.2.1 -edx-ace==0.1.7 +edx-ace==0.1.6 edx-analytics-data-api-client==0.14.4 edx-ccx-keys==0.2.1 edx-celeryutils==0.2.7 diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index fa02fdb910..133e5e2173 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -123,7 +123,7 @@ dm.xmlsec.binding==1.3.3 docopt==0.6.2 docutils==0.14 dogapi==1.2.1 -edx-ace==0.1.7 +edx-ace==0.1.6 edx-analytics-data-api-client==0.14.4 edx-ccx-keys==0.2.1 edx-celeryutils==0.2.7 diff --git a/themes/red-theme/lms/templates/ace_common/edx_ace/common/base_body.html b/themes/red-theme/lms/templates/ace_common/edx_ace/common/base_body.html index 548fa6dc87..f89bf4aad6 100644 --- a/themes/red-theme/lms/templates/ace_common/edx_ace/common/base_body.html +++ b/themes/red-theme/lms/templates/ace_common/edx_ace/common/base_body.html @@ -22,9 +22,9 @@ -{% for image_src in channel.tracker_image_sources %} - -{% endfor %} +{# Note {beacon_src} is not a template variable that is evaluated by the Django template engine. It is evaluated by #} +{# Sailthru when the email is sent. Other email providers would need to replace this variable in the HTML as well. #} + {% google_analytics_tracking_pixel %} @@ -159,13 +159,17 @@ - {% for action_link_url, action_link_text in channel.action_links %} -

- - {{ action_link_text }} - -

- {% endfor %} + {# Note that these variables are evaluated by Sailthru, not the Django template engine #} +

+ + {% trans "View on Web" %} + +

+

+ + {% trans "Unsubscribe from this list" %} + +