Commit e61b0c23 by Matt Drayer

Merge pull request #715 from edx/mattdrayer/override-oscar-from-email

mattdrayer/override-oscar-from-email: Add new SiteConfiguration field
parents 88eba832 b13d2fa0
...@@ -82,6 +82,12 @@ class Command(BaseCommand): ...@@ -82,6 +82,12 @@ class Command(BaseCommand):
type=str, type=str,
required=False, required=False,
help='segment key') help='segment key')
parser.add_argument('--from-email',
action='store',
dest='from_email',
type=str,
required=True,
help='from email')
def handle(self, *args, **options): def handle(self, *args, **options):
site_id = options.get('site_id') site_id = options.get('site_id')
...@@ -93,6 +99,7 @@ class Command(BaseCommand): ...@@ -93,6 +99,7 @@ class Command(BaseCommand):
client_id = options.get('client_id') client_id = options.get('client_id')
client_secret = options.get('client_secret') client_secret = options.get('client_secret')
segment_key = options.get('segment_key') segment_key = options.get('segment_key')
from_email = options.get('from_email')
try: try:
site = Site.objects.get(id=site_id) site = Site.objects.get(id=site_id)
...@@ -121,6 +128,7 @@ class Command(BaseCommand): ...@@ -121,6 +128,7 @@ class Command(BaseCommand):
'theme_scss_path': options['theme_scss_path'], 'theme_scss_path': options['theme_scss_path'],
'payment_processors': options['payment_processors'], 'payment_processors': options['payment_processors'],
'segment_key': segment_key, 'segment_key': segment_key,
'from_email': from_email,
'oauth_settings': { 'oauth_settings': {
'SOCIAL_AUTH_EDX_OIDC_URL_ROOT': '{lms_url_root}/oauth2'.format(lms_url_root=lms_url_root), 'SOCIAL_AUTH_EDX_OIDC_URL_ROOT': '{lms_url_root}/oauth2'.format(lms_url_root=lms_url_root),
'SOCIAL_AUTH_EDX_OIDC_KEY': client_id, 'SOCIAL_AUTH_EDX_OIDC_KEY': client_id,
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0014_enrollment_code_switch'),
]
operations = [
migrations.AddField(
model_name='siteconfiguration',
name='from_email',
field=models.CharField(default='oscar@example.com', help_text='Address from which emails are sent.', max_length=255, verbose_name='From email'),
preserve_default=False,
),
]
...@@ -60,6 +60,13 @@ class SiteConfiguration(models.Model): ...@@ -60,6 +60,13 @@ class SiteConfiguration(models.Model):
null=True, null=True,
blank=True blank=True
) )
from_email = models.CharField(
verbose_name=_('From email'),
help_text=_('Address from which emails are sent.'),
max_length=255,
null=False,
blank=False
)
class Meta(object): class Meta(object):
unique_together = ('site', 'partner') unique_together = ('site', 'partner')
...@@ -121,6 +128,16 @@ class SiteConfiguration(models.Model): ...@@ -121,6 +128,16 @@ class SiteConfiguration(models.Model):
if processor.NAME in self.payment_processors_set and processor.is_enabled() if processor.NAME in self.payment_processors_set and processor.is_enabled()
] ]
def get_from_email(self):
"""
Returns the configured from_email value for the specified site. If no from_email is
available we return the base OSCAR_FROM_EMAIL setting
Returns:
string: Returns sender address for use in customer emails/alerts
"""
return self.from_email or settings.OSCAR_FROM_EMAIL
def clean_fields(self, exclude=None): def clean_fields(self, exclude=None):
""" Validates model fields """ """ Validates model fields """
if not exclude or 'payment_processors' not in exclude: if not exclude or 'payment_processors' not in exclude:
......
...@@ -12,7 +12,8 @@ Partner = get_model('partner', 'Partner') ...@@ -12,7 +12,8 @@ Partner = get_model('partner', 'Partner')
@ddt @ddt
class CreateOrUpdateSiteCommandTests(TestCase): class CreateOrUpdateSiteCommandTests(TestCase):
command = 'create_or_update_site'
command_name = 'create_or_update_site'
def setUp(self): def setUp(self):
super(CreateOrUpdateSiteCommandTests, self).setUp() super(CreateOrUpdateSiteCommandTests, self).setUp()
...@@ -24,6 +25,7 @@ class CreateOrUpdateSiteCommandTests(TestCase): ...@@ -24,6 +25,7 @@ class CreateOrUpdateSiteCommandTests(TestCase):
self.client_id = 'ecommerce-key' self.client_id = 'ecommerce-key'
self.client_secret = 'ecommerce-secret' self.client_secret = 'ecommerce-secret'
self.segment_key = 'test-segment-key' self.segment_key = 'test-segment-key'
self.from_email = 'site_from_email@example.com'
def _check_site_configuration(self, site, partner): def _check_site_configuration(self, site, partner):
site_configuration = site.siteconfiguration site_configuration = site.siteconfiguration
...@@ -35,20 +37,53 @@ class CreateOrUpdateSiteCommandTests(TestCase): ...@@ -35,20 +37,53 @@ class CreateOrUpdateSiteCommandTests(TestCase):
self.assertEqual(site_configuration.oauth_settings['SOCIAL_AUTH_EDX_OIDC_KEY'], self.client_id) self.assertEqual(site_configuration.oauth_settings['SOCIAL_AUTH_EDX_OIDC_KEY'], self.client_id)
self.assertEqual(site_configuration.oauth_settings['SOCIAL_AUTH_EDX_OIDC_SECRET'], self.client_secret) self.assertEqual(site_configuration.oauth_settings['SOCIAL_AUTH_EDX_OIDC_SECRET'], self.client_secret)
self.assertEqual(site_configuration.segment_key, self.segment_key) self.assertEqual(site_configuration.segment_key, self.segment_key)
self.assertEqual(site_configuration.from_email, self.from_email)
def _call_command(self, site_domain, partner_code, lms_url_root, client_id, client_secret, from_email,
site_id=None, site_name=None, partner_name=None, theme_scss_path=None,
payment_processors=None, segment_key=None):
"""
Internal helper method for interacting with the create_or_update_site management command
"""
# Required arguments
command_args = [
'--site-domain={site_domain}'.format(site_domain=site_domain),
'--partner-code={partner_code}'.format(partner_code=partner_code),
'--lms-url-root={lms_url_root}'.format(lms_url_root=lms_url_root),
'--client-id={client_id}'.format(client_id=client_id),
'--client-secret={client_secret}'.format(client_secret=client_secret),
'--from-email={from_email}'.format(from_email=from_email)
]
# Optional arguments
if site_id:
command_args.append('--site-id={site_id}'.format(site_id=site_id))
if site_name:
command_args.append('--site-name={site_name}'.format(site_name=site_name))
if partner_name:
command_args.append('--partner-name={partner_name}'.format(partner_name=partner_name))
if theme_scss_path:
command_args.append('--theme-scss-path={theme_scss_path}'.format(theme_scss_path=theme_scss_path))
if payment_processors:
command_args.append('--payment-processors={payment_processors}'.format(payment_processors=payment_processors)) # pylint: disable=line-too-long
if segment_key:
command_args.append('--segment-key={segment_key}'.format(segment_key=segment_key))
call_command(self.command_name, *command_args)
def test_create_site(self): def test_create_site(self):
""" Verify the command creates Site, Partner, and SiteConfiguration. """ """ Verify the command creates Site, Partner, and SiteConfiguration. """
site_domain = 'ecommerce-fake1.server' site_domain = 'ecommerce-fake1.server'
call_command(
self.command, self._call_command(
'--site-domain={domain}'.format(domain=site_domain), site_domain=site_domain,
'--partner-code={partner}'.format(partner=self.partner), partner_code=self.partner,
'--lms-url-root={lms_url_root}'.format(lms_url_root=self.lms_url_root), lms_url_root=self.lms_url_root,
'--theme-scss-path={theme_scss_path}'.format(theme_scss_path=self.theme_scss_path), theme_scss_path=self.theme_scss_path,
'--payment-processors={payment_processors}'.format(payment_processors=self.payment_processors), payment_processors=self.payment_processors,
'--client-id={client_id}'.format(client_id=self.client_id), client_id=self.client_id,
'--client-secret={client_secret}'.format(client_secret=self.client_secret), client_secret=self.client_secret,
'--segment-key={segment_key}'.format(segment_key=self.segment_key) segment_key=self.segment_key,
from_email=self.from_email
) )
site = Site.objects.get(domain=site_domain) site = Site.objects.get(domain=site_domain)
...@@ -62,18 +97,19 @@ class CreateOrUpdateSiteCommandTests(TestCase): ...@@ -62,18 +97,19 @@ class CreateOrUpdateSiteCommandTests(TestCase):
updated_site_domain = 'ecommerce-fake3.server' updated_site_domain = 'ecommerce-fake3.server'
updated_site_name = 'Fake Ecommerce Server' updated_site_name = 'Fake Ecommerce Server'
site = Site.objects.create(domain=site_domain) site = Site.objects.create(domain=site_domain)
call_command(
self.command, self._call_command(
'--site-id={site_id}'.format(site_id=site.id), site_id=site.id,
'--site-domain={domain}'.format(domain=updated_site_domain), site_domain=updated_site_domain,
'--site-name={site_name}'.format(site_name=updated_site_name), site_name=updated_site_name,
'--partner-code={partner}'.format(partner=self.partner), partner_code=self.partner,
'--lms-url-root={lms_url_root}'.format(lms_url_root=self.lms_url_root), lms_url_root=self.lms_url_root,
'--theme-scss-path={theme_scss_path}'.format(theme_scss_path=self.theme_scss_path), theme_scss_path=self.theme_scss_path,
'--payment-processors={payment_processors}'.format(payment_processors=self.payment_processors), payment_processors=self.payment_processors,
'--client-id={client_id}'.format(client_id=self.client_id), client_id=self.client_id,
'--client-secret={client_secret}'.format(client_secret=self.client_secret), client_secret=self.client_secret,
'--segment-key={segment_key}'.format(segment_key=self.segment_key) segment_key=self.segment_key,
from_email=self.from_email
) )
site = Site.objects.get(id=site.id) site = Site.objects.get(id=site.id)
...@@ -85,14 +121,17 @@ class CreateOrUpdateSiteCommandTests(TestCase): ...@@ -85,14 +121,17 @@ class CreateOrUpdateSiteCommandTests(TestCase):
@data( @data(
['--site-id=1'], ['--site-id=1'],
['--site-id=1', '--site-domain=fake.server'], ['--site-id=1', '--site-name=fake.server'],
['--site-id=1', '--site-domain=fake.server', '--partner-code=fake_partner'], ['--site-id=1', '--site-name=fake.server', '--partner-name=fake_partner'],
['--site-id=1', '--site-domain=fake.server', '--partner-code=fake_partner', ['--site-id=1', '--site-domain=fake.server', '--partner-name=fake_partner',
'--lms-url-root=http://fake.server'], '--theme-scss-path=site/sass/css/'],
['--site-id=1', '--site-domain=fake.server', '--partner-code=fake_partner', ['--site-id=1', '--site-domain=fake.server', '--partner-name=fake_partner',
'--lms-url-root=http://fake.server', '--client-id=fake'], '--theme-scss-path=site/sass/css/', '--payment-processors=cybersource'],
['--site-id=1', '--site-domain=fake.server', '--partner-name=fake_partner',
'--theme-scss-path=site/sass/css/', '--payment-processors=cybersource',
'--segment-key=abc']
) )
def test_missing_arguments(self, arguments): def test_missing_arguments(self, command_args):
""" Verify CommandError is raised when required arguments are missing """ """ Verify CommandError is raised when required arguments are missing """
with self.assertRaises(CommandError): with self.assertRaises(CommandError):
call_command(self.command, *arguments) call_command(self.command_name, *command_args)
...@@ -8,13 +8,18 @@ from django.test import override_settings ...@@ -8,13 +8,18 @@ from django.test import override_settings
from ecommerce.core.models import BusinessClient, User, SiteConfiguration, validate_configuration from ecommerce.core.models import BusinessClient, User, SiteConfiguration, validate_configuration
from ecommerce.core.tests import toggle_switch from ecommerce.core.tests import toggle_switch
from ecommerce.extensions.payment.tests.processors import DummyProcessor, AnotherDummyProcessor from ecommerce.extensions.payment.tests.processors import DummyProcessor, AnotherDummyProcessor
from ecommerce.tests.factories import SiteConfigurationFactory
from ecommerce.tests.testcases import TestCase from ecommerce.tests.testcases import TestCase
def _make_site_config(payment_processors_str, site_id=1): def _make_site_config(payment_processors_str, site_id=1):
site = Site.objects.get(id=site_id) site = Site.objects.get(id=site_id)
return SiteConfiguration(site=site, payment_processors=payment_processors_str) return SiteConfiguration(
site=site,
payment_processors=payment_processors_str,
from_email='sender@example.com'
)
class UserTests(TestCase): class UserTests(TestCase):
...@@ -182,6 +187,18 @@ class SiteConfigurationTests(TestCase): ...@@ -182,6 +187,18 @@ class SiteConfigurationTests(TestCase):
result = site_config.get_payment_processors() result = site_config.get_payment_processors()
self.assertEqual(result, expected_result) self.assertEqual(result, expected_result)
def test_get_from_email(self):
"""
Validate SiteConfiguration.get_from_email() along with whether, or not,
the base from email address is actually changed when a site-specific value is specified.
"""
site_config = SiteConfigurationFactory(from_email='', partner__name='TestX')
self.assertEqual(site_config.get_from_email(), settings.OSCAR_FROM_EMAIL)
expected_from_email = "expected@email.com"
site_config = SiteConfigurationFactory(from_email=expected_from_email, partner__name='TestX')
self.assertEqual(site_config.get_from_email(), expected_from_email)
class HelperMethodTests(TestCase): class HelperMethodTests(TestCase):
""" Tests helper methods in models.py """ """ Tests helper methods in models.py """
......
...@@ -7,14 +7,16 @@ import logging ...@@ -7,14 +7,16 @@ import logging
from django.db import transaction from django.db import transaction
from ecommerce_worker.fulfillment.v1.tasks import fulfill_order from ecommerce_worker.fulfillment.v1.tasks import fulfill_order
from oscar.apps.checkout.mixins import OrderPlacementMixin from oscar.apps.checkout.mixins import OrderPlacementMixin
from oscar.core.loading import get_class from oscar.core.loading import get_class, get_model
import waffle import waffle
from ecommerce.extensions.analytics.utils import audit_log from ecommerce.extensions.analytics.utils import audit_log
from ecommerce.extensions.api import data as data_api from ecommerce.extensions.api import data as data_api
from ecommerce.extensions.api.constants import APIConstants as AC from ecommerce.extensions.api.constants import APIConstants as AC
from ecommerce.extensions.checkout.exceptions import BasketNotFreeError from ecommerce.extensions.checkout.exceptions import BasketNotFreeError
from ecommerce.extensions.customer.utils import Dispatcher
CommunicationEventType = get_model('customer', 'CommunicationEventType')
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
post_checkout = get_class('checkout.signals', 'post_checkout') post_checkout = get_class('checkout.signals', 'post_checkout')
...@@ -156,3 +158,25 @@ class EdxOrderPlacementMixin(OrderPlacementMixin): ...@@ -156,3 +158,25 @@ class EdxOrderPlacementMixin(OrderPlacementMixin):
) )
return order return order
def send_confirmation_message(self, order, code, site=None, **kwargs):
ctx = self.get_message_context(order)
try:
event_type = CommunicationEventType.objects.get(code=code)
except CommunicationEventType.DoesNotExist:
# No event-type in database, attempt to find templates for this
# type and render them immediately to get the messages. Since we
# have not CommunicationEventType to link to, we can't create a
# CommunicationEvent instance.
messages = CommunicationEventType.objects.get_and_render(code, ctx)
event_type = None
else:
messages = event_type.get_messages(ctx)
if messages and messages['body']:
logger.info("Order #%s - sending %s messages", order.number, code)
dispatcher = Dispatcher(logger)
dispatcher.dispatch_order_messages(order, messages, event_type, site, **kwargs)
else:
logger.warning("Order #%s - no %s communication event type",
order.number, code)
...@@ -3,6 +3,7 @@ import logging ...@@ -3,6 +3,7 @@ import logging
from django.conf import settings from django.conf import settings
from django.dispatch import receiver from django.dispatch import receiver
from oscar.core.loading import get_class from oscar.core.loading import get_class
from threadlocals import threadlocals
import waffle import waffle
from ecommerce.core.url_utils import get_lms_url from ecommerce.core.url_utils import get_lms_url
...@@ -86,7 +87,8 @@ def send_course_purchase_email(sender, order=None, **kwargs): # pylint: disable ...@@ -86,7 +87,8 @@ def send_course_purchase_email(sender, order=None, **kwargs): # pylint: disable
), ),
'credit_hours': product.attr.credit_hours, 'credit_hours': product.attr.credit_hours,
'credit_provider': provider_data['display_name'], 'credit_provider': provider_data['display_name'],
} },
threadlocals.get_current_request().site
) )
else: else:
......
...@@ -4,7 +4,10 @@ Tests for the ecommerce.extensions.checkout.mixins module. ...@@ -4,7 +4,10 @@ Tests for the ecommerce.extensions.checkout.mixins module.
from decimal import Decimal from decimal import Decimal
from mock import Mock, patch from mock import Mock, patch
from django.core import mail
from django.test import RequestFactory
from oscar.core.loading import get_model from oscar.core.loading import get_model
from oscar.test import factories
from oscar.test.newfactories import BasketFactory, ProductFactory, UserFactory from oscar.test.newfactories import BasketFactory, ProductFactory, UserFactory
from testfixtures import LogCapture from testfixtures import LogCapture
from waffle.models import Sample from waffle.models import Sample
...@@ -14,6 +17,7 @@ from ecommerce.extensions.checkout.exceptions import BasketNotFreeError ...@@ -14,6 +17,7 @@ from ecommerce.extensions.checkout.exceptions import BasketNotFreeError
from ecommerce.extensions.checkout.mixins import EdxOrderPlacementMixin from ecommerce.extensions.checkout.mixins import EdxOrderPlacementMixin
from ecommerce.extensions.fulfillment.status import ORDER from ecommerce.extensions.fulfillment.status import ORDER
from ecommerce.extensions.refund.tests.mixins import RefundTestMixin from ecommerce.extensions.refund.tests.mixins import RefundTestMixin
from ecommerce.tests.factories import SiteConfigurationFactory
from ecommerce.tests.mixins import BusinessIntelligenceMixin from ecommerce.tests.mixins import BusinessIntelligenceMixin
from ecommerce.tests.testcases import TestCase from ecommerce.tests.testcases import TestCase
...@@ -201,3 +205,41 @@ class EdxOrderPlacementMixinTests(BusinessIntelligenceMixin, RefundTestMixin, Te ...@@ -201,3 +205,41 @@ class EdxOrderPlacementMixinTests(BusinessIntelligenceMixin, RefundTestMixin, Te
with self.assertRaises(BasketNotFreeError): with self.assertRaises(BasketNotFreeError):
EdxOrderPlacementMixin().place_free_order(basket) EdxOrderPlacementMixin().place_free_order(basket)
def test_send_confirmation_message(self, __):
"""
Verify the send confirmation message override functions as expected
"""
request = RequestFactory()
user = self.create_user()
user.email = 'test_user@example.com'
request.user = user
site_from_email = 'from@example.com'
site_configuration = SiteConfigurationFactory(partner__name='Tester', from_email=site_from_email)
request.site = site_configuration.site
order = factories.create_order()
order.user = user
mixin = EdxOrderPlacementMixin()
mixin.request = request
# Happy path
mixin.send_confirmation_message(order, 'ORDER_PLACED', request.site)
self.assertEqual(mail.outbox[0].from_email, site_from_email)
mail.outbox = []
# Invalid code path (graceful exit)
mixin.send_confirmation_message(order, 'INVALID_CODE', request.site)
self.assertEqual(len(mail.outbox), 0)
# Invalid messages container path (graceful exit)
with patch('ecommerce.extensions.checkout.mixins.CommunicationEventType.objects.get') as mock_get:
mock_event_type = Mock()
mock_event_type.get_messages.return_value = {}
mock_get.return_value = mock_event_type
mixin.send_confirmation_message(order, 'ORDER_PLACED', request.site)
self.assertEqual(len(mail.outbox), 0)
mock_event_type.get_messages.return_value = {'body': None}
mock_get.return_value = mock_event_type
mixin.send_confirmation_message(order, 'ORDER_PLACED', request.site)
self.assertEqual(len(mail.outbox), 0)
from django.conf import settings from django.conf import settings
from django.core import mail from django.core import mail
from django.test import RequestFactory
import httpretty import httpretty
import mock
from oscar.test import factories from oscar.test import factories
from oscar.test.newfactories import BasketFactory, UserFactory from oscar.test.newfactories import BasketFactory
from threadlocals.threadlocals import get_current_request from threadlocals.threadlocals import get_current_request
from ecommerce.core.tests import toggle_switch from ecommerce.core.tests import toggle_switch
...@@ -10,10 +12,20 @@ from ecommerce.core.url_utils import get_lms_url ...@@ -10,10 +12,20 @@ from ecommerce.core.url_utils import get_lms_url
from ecommerce.courses.models import Course from ecommerce.courses.models import Course
from ecommerce.extensions.catalogue.tests.mixins import CourseCatalogTestMixin from ecommerce.extensions.catalogue.tests.mixins import CourseCatalogTestMixin
from ecommerce.extensions.checkout.signals import send_course_purchase_email from ecommerce.extensions.checkout.signals import send_course_purchase_email
from ecommerce.tests.factories import SiteConfigurationFactory
from ecommerce.tests.testcases import TestCase from ecommerce.tests.testcases import TestCase
class SignalTests(CourseCatalogTestMixin, TestCase): class SignalTests(CourseCatalogTestMixin, TestCase):
def setUp(self):
super(SignalTests, self).setUp()
self.request = RequestFactory()
self.user = self.create_user()
self.request.user = self.user
self.site_configuration = SiteConfigurationFactory(partner__name='Tester', from_email='from@example.com')
self.request.site = self.site_configuration.site
@httpretty.activate @httpretty.activate
def test_post_checkout_callback(self): def test_post_checkout_callback(self):
""" """
...@@ -26,15 +38,17 @@ class SignalTests(CourseCatalogTestMixin, TestCase): ...@@ -26,15 +38,17 @@ class SignalTests(CourseCatalogTestMixin, TestCase):
content_type="application/json" content_type="application/json"
) )
toggle_switch('ENABLE_NOTIFICATIONS', True) toggle_switch('ENABLE_NOTIFICATIONS', True)
user = UserFactory()
course = Course.objects.create(id='edX/DemoX/Demo_Course', name='Demo Course') course = Course.objects.create(id='edX/DemoX/Demo_Course', name='Demo Course')
seat = course.create_or_update_seat('credit', False, 50, self.partner, 'ASU', None, 2) seat = course.create_or_update_seat('credit', False, 50, self.partner, 'ASU', None, 2)
basket = BasketFactory() basket = BasketFactory()
basket.add_product(seat, 1) basket.add_product(seat, 1)
order = factories.create_order(number=1, basket=basket, user=user) order = factories.create_order(number=1, basket=basket, user=self.user)
send_course_purchase_email(None, order=order) with mock.patch('threadlocals.threadlocals.get_current_request') as mock_gcr:
mock_gcr.return_value = self.request
send_course_purchase_email(None, order=order)
self.assertEqual(len(mail.outbox), 1) self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].from_email, self.site_configuration.from_email)
self.assertEqual(mail.outbox[0].subject, 'Order Receipt') self.assertEqual(mail.outbox[0].subject, 'Order Receipt')
self.assertEqual( self.assertEqual(
mail.outbox[0].body, mail.outbox[0].body,
...@@ -54,7 +68,7 @@ class SignalTests(CourseCatalogTestMixin, TestCase): ...@@ -54,7 +68,7 @@ class SignalTests(CourseCatalogTestMixin, TestCase):
'\n\nYou received this message because you purchased credit hours for {course_title}, ' '\n\nYou received this message because you purchased credit hours for {course_title}, '
'an {platform_name} course.\n'.format( 'an {platform_name} course.\n'.format(
course_title=order.lines.first().product.title, course_title=order.lines.first().product.title,
full_name=user.get_full_name(), full_name=self.user.get_full_name(),
credit_hours=2, credit_hours=2,
credit_provider='Hogwarts', credit_provider='Hogwarts',
platform_name=get_current_request().site.name, platform_name=get_current_request().site.name,
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import oscar.models.fields.autoslugfield
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
('catalogue', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='CommunicationEventType',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('code', oscar.models.fields.autoslugfield.AutoSlugField(populate_from='name', unique=True, verbose_name='Code', editable=False, separator='_', max_length=128, help_text='Code used for looking up this event programmatically', blank=True)),
('name', models.CharField(verbose_name='Name', max_length=255, help_text='This is just used for organisational purposes')),
('category', models.CharField(default='Order related', max_length=255, verbose_name='Category', choices=[('Order related', 'Order related'), ('User related', 'User related')])),
('email_subject_template', models.CharField(verbose_name='Email Subject Template', max_length=255, blank=True, null=True)),
('email_body_template', models.TextField(blank=True, verbose_name='Email Body Template', null=True)),
('email_body_html_template', models.TextField(verbose_name='Email Body HTML Template', blank=True, help_text='HTML template', null=True)),
('sms_template', models.CharField(verbose_name='SMS Template', max_length=170, help_text='SMS template', blank=True, null=True)),
('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date Created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date Updated')),
],
options={
'verbose_name_plural': 'Communication event types',
'verbose_name': 'Communication event type',
'abstract': False,
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Email',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('subject', models.TextField(max_length=255, verbose_name='Subject')),
('body_text', models.TextField(verbose_name='Body Text')),
('body_html', models.TextField(verbose_name='Body HTML', blank=True)),
('date_sent', models.DateTimeField(auto_now_add=True, verbose_name='Date Sent')),
('user', models.ForeignKey(verbose_name='User', related_name='emails', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name_plural': 'Emails',
'verbose_name': 'Email',
'abstract': False,
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Notification',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('subject', models.CharField(max_length=255)),
('body', models.TextField()),
('category', models.CharField(max_length=255, blank=True)),
('location', models.CharField(default='Inbox', max_length=32, choices=[('Inbox', 'Inbox'), ('Archive', 'Archive')])),
('date_sent', models.DateTimeField(auto_now_add=True)),
('date_read', models.DateTimeField(blank=True, null=True)),
('recipient', models.ForeignKey(related_name='notifications', to=settings.AUTH_USER_MODEL)),
('sender', models.ForeignKey(to=settings.AUTH_USER_MODEL, null=True)),
],
options={
'ordering': ('-date_sent',),
'verbose_name_plural': 'Notifications',
'verbose_name': 'Notification',
'abstract': False,
},
bases=(models.Model,),
),
migrations.CreateModel(
name='ProductAlert',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('email', models.EmailField(max_length=75, db_index=True, verbose_name='Email', blank=True)),
('key', models.CharField(max_length=128, db_index=True, verbose_name='Key', blank=True)),
('status', models.CharField(default='Active', max_length=20, verbose_name='Status', choices=[('Unconfirmed', 'Not yet confirmed'), ('Active', 'Active'), ('Cancelled', 'Cancelled'), ('Closed', 'Closed')])),
('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')),
('date_confirmed', models.DateTimeField(blank=True, verbose_name='Date confirmed', null=True)),
('date_cancelled', models.DateTimeField(blank=True, verbose_name='Date cancelled', null=True)),
('date_closed', models.DateTimeField(blank=True, verbose_name='Date closed', null=True)),
('product', models.ForeignKey(to='catalogue.Product')),
('user', models.ForeignKey(null=True, verbose_name='User', related_name='alerts', to=settings.AUTH_USER_MODEL, blank=True)),
],
options={
'verbose_name_plural': 'Product alerts',
'verbose_name': 'Product alert',
'abstract': False,
},
bases=(models.Model,),
),
]
# noinspection PyUnresolvedReferences
from oscar.apps.customer.models import * # pragma: no cover noqa pylint: disable=wildcard-import,unused-wildcard-import
from django.conf import settings
from django.core import mail
from django.test import RequestFactory
from oscar.core.loading import get_model
from oscar.test import factories
from ecommerce.extensions.customer.utils import Dispatcher
from ecommerce.tests.factories import SiteConfigurationFactory
from ecommerce.tests.testcases import TestCase
CommunicationEventType = get_model('customer', 'CommunicationEventType')
class CustomerUtilsTests(TestCase):
def setUp(self):
super(CustomerUtilsTests, self).setUp()
self.dispatcher = Dispatcher()
self.request = RequestFactory()
self.user = self.create_user()
self.user.email = 'test_user@example.com'
self.request.user = self.user
self.site_configuration = SiteConfigurationFactory(partner__name='Tester', from_email='from@example.com')
self.request.site = self.site_configuration.site
self.order = factories.create_order()
self.order.user = self.user
def test_dispatch_direct_messages(self):
recipient = 'test_dispatch_direct_messages@example.com'
messages = {
'subject': 'The message subject.',
'body': 'The message body.',
'html': '<p>The message html body.</p>'
}
self.dispatcher.dispatch_direct_messages(recipient, messages, self.request.site)
self.assertEqual(mail.outbox[0].from_email, self.site_configuration.from_email)
mail.outbox = []
# Test graceful failure paths
messages = {
'subject': None,
'body': 'The message body.',
'html': '<p>The message html body.</p>'
}
self.dispatcher.dispatch_direct_messages(recipient, messages, self.request.site)
self.assertEqual(len(mail.outbox), 0)
messages = {
'subject': "The subject",
'body': None,
'html': '<p>The message html body.</p>'
}
self.dispatcher.dispatch_direct_messages(recipient, messages, self.request.site)
self.assertEqual(len(mail.outbox), 0)
def test_dispatch_order_messages(self):
messages = {
'subject': 'The message subject.',
'body': 'The message body.',
'html': '<p>The message html body.</p>',
'sms': None # Text messaging is currently NotImplemented
}
event_type = CommunicationEventType.objects.create(
email_body_template="Body Template",
email_body_html_template="<p>Body HTML Template</p>"
)
self.dispatcher.dispatch_order_messages(self.order, messages, event_type, self.request.site)
self.assertEqual(mail.outbox[0].from_email, self.site_configuration.from_email)
def test_dispatch_order_messages_null_eventtype(self):
messages = {
'subject': 'The message subject.',
'body': 'The message body.',
'html': '<p>The message html body.</p>',
'sms': None # Text messaging is currently NotImplemented
}
self.dispatcher.dispatch_order_messages(self.order, messages, None, self.request.site)
self.assertEqual(mail.outbox[0].from_email, self.site_configuration.from_email)
def test_dispatch_order_messages_sms_notimplemented(self):
messages = {
'subject': 'The message subject.',
'body': 'The message body.',
'html': '<p>The message html body.</p>',
'sms': 'This should trigger a NotImplementedException'
}
event_type = CommunicationEventType.objects.all().first()
with self.assertRaises(NotImplementedError):
self.dispatcher.dispatch_order_messages(self.order, messages, event_type, self.request.site)
def test_dispatch_order_messages_empty_user_email_workflow(self):
"""
If the user does not have an email address then the send operation is gracefully exited,
so there should be no exception raised by this test and coverage should be reflected.
"""
self.order.user.email = ''
messages = {
'subject': 'The message subject.',
'body': 'The message body.',
'html': '<p>The message html body.</p>',
'sms': None
}
event_type = CommunicationEventType.objects.all().first()
self.dispatcher.dispatch_order_messages(self.order, messages, event_type, self.request.site)
def test_send_email_messages_no_site(self):
"""
Ensure the send email workflow executes correctly when a site is not specified
"""
recipient = 'test_dispatch_direct_messages@example.com'
messages = {
'subject': 'The message subject.',
'body': 'The message body.',
'html': None,
'sms': None,
}
self.dispatcher.send_email_messages(recipient, messages)
self.assertEqual(mail.outbox[0].from_email, settings.OSCAR_FROM_EMAIL)
from django.conf import settings
from django.core.mail import EmailMessage, EmailMultiAlternatives
from oscar.apps.customer.utils import * # pylint: disable=wildcard-import, unused-wildcard-import
# pylint: disable=abstract-method, function-redefined
class Dispatcher(Dispatcher):
def dispatch_direct_messages(self, recipient, messages, site=None): # pylint: disable=arguments-differ
"""
Dispatch one-off messages to explicitly specified recipient(s).
"""
if messages['subject'] and messages['body']:
self.send_email_messages(recipient, messages, site)
def dispatch_order_messages(self, order, messages, event_type=None, site=None, **kwargs): # pylint: disable=arguments-differ
"""
Dispatch order-related messages to the customer
"""
# Note: We do not support anonymous orders
self.dispatch_user_messages(order.user, messages, site)
# Create order communications event for audit
if event_type is not None:
# pylint: disable=protected-access
CommunicationEvent._default_manager.create(order=order, event_type=event_type)
def dispatch_user_messages(self, user, messages, site=None): # pylint: disable=arguments-differ
"""
Send messages to a site user
"""
if messages['subject'] and (messages['body'] or messages['html']):
self.send_user_email_messages(user, messages, site)
if messages['sms']:
self.send_text_message(user, messages['sms'])
def send_user_email_messages(self, user, messages, site=None): # pylint: disable=arguments-differ
"""
Sends message to the registered user / customer and collects data in
database
"""
if not user.email:
msg = "Unable to send email messages: No email address for '{username}'.".format(username=user.username)
self.logger.warning(msg)
return
email = self.send_email_messages(user.email, messages, site)
# Is user is signed in, record the event for audit
if email and user.is_authenticated():
# pylint: disable=protected-access
Email._default_manager.create(user=user,
subject=email.subject,
body_text=email.body,
body_html=messages['html'])
def send_email_messages(self, recipient, messages, site=None): # pylint:disable=arguments-differ
"""
Plain email sending to the specified recipient
"""
from_email = settings.OSCAR_FROM_EMAIL
if site:
from_email = site.siteconfiguration.get_from_email()
# Determine whether we are sending a HTML version too
if messages['html']:
email = EmailMultiAlternatives(messages['subject'],
messages['body'],
from_email=from_email,
to=[recipient])
email.attach_alternative(messages['html'], "text/html")
else:
email = EmailMessage(messages['subject'], # pylint: disable=redefined-variable-type
messages['body'],
from_email=from_email,
to=[recipient])
self.logger.info("Sending email to %s" % recipient)
email.send()
return email
...@@ -2,7 +2,6 @@ import logging ...@@ -2,7 +2,6 @@ import logging
from oscar.core.loading import get_model, get_class from oscar.core.loading import get_model, get_class
from premailer import transform from premailer import transform
from threadlocals.threadlocals import get_current_request
from ecommerce.extensions.analytics.utils import parse_tracking_context from ecommerce.extensions.analytics.utils import parse_tracking_context
...@@ -12,7 +11,7 @@ CommunicationEventType = get_model('customer', 'CommunicationEventType') ...@@ -12,7 +11,7 @@ CommunicationEventType = get_model('customer', 'CommunicationEventType')
Dispatcher = get_class('customer.utils', 'Dispatcher') Dispatcher = get_class('customer.utils', 'Dispatcher')
def send_notification(user, commtype_code, context): def send_notification(user, commtype_code, context, site):
"""Send different notification mail to the user based on the triggering event. """Send different notification mail to the user based on the triggering event.
Args: Args:
...@@ -29,7 +28,7 @@ def send_notification(user, commtype_code, context): ...@@ -29,7 +28,7 @@ def send_notification(user, commtype_code, context):
full_name = user.get_full_name() full_name = user.get_full_name()
context.update({ context.update({
'full_name': full_name, 'full_name': full_name,
'platform_name': get_current_request().site.name, 'platform_name': site.name,
'tracking_pixel': tracking_pixel, 'tracking_pixel': tracking_pixel,
}) })
...@@ -47,4 +46,4 @@ def send_notification(user, commtype_code, context): ...@@ -47,4 +46,4 @@ def send_notification(user, commtype_code, context):
if messages and (messages['body'] or messages['html']): if messages and (messages['body'] or messages['html']):
messages['html'] = transform(messages['html']) messages['html'] = transform(messages['html'])
Dispatcher().dispatch_user_messages(user, messages) Dispatcher().dispatch_user_messages(user, messages, site)
...@@ -27,6 +27,7 @@ OSCAR_APPS = [ ...@@ -27,6 +27,7 @@ OSCAR_APPS = [
'ecommerce.extensions.basket', 'ecommerce.extensions.basket',
'ecommerce.extensions.catalogue', 'ecommerce.extensions.catalogue',
'ecommerce.extensions.checkout', 'ecommerce.extensions.checkout',
'ecommerce.extensions.customer',
'ecommerce.extensions.dashboard', 'ecommerce.extensions.dashboard',
'ecommerce.extensions.dashboard.orders', 'ecommerce.extensions.dashboard.orders',
'ecommerce.extensions.dashboard.users', 'ecommerce.extensions.dashboard.users',
...@@ -261,3 +262,5 @@ ENROLLMENT_FULFILLMENT_TIMEOUT = 7 ...@@ -261,3 +262,5 @@ ENROLLMENT_FULFILLMENT_TIMEOUT = 7
VOUCHER_CODE_LENGTH = 16 VOUCHER_CODE_LENGTH = 16
THUMBNAIL_DEBUG = False THUMBNAIL_DEBUG = False
OSCAR_FROM_EMAIL = 'testing@example.com'
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment