Bladeren bron

Add a new Communication app that manages all of Oscar's communications.

master
samitnuk 7 jaren geleden
bovenliggende
commit
2af6753c67
100 gewijzigde bestanden met toevoegingen van 1846 en 903 verwijderingen
  1. 2
    1
      docs/source/internals/getting_started.rst
  2. 22
    0
      docs/source/ref/apps/communication.rst
  3. 13
    5
      docs/source/ref/apps/customer.rst
  4. 16
    0
      docs/source/ref/settings.rst
  5. 27
    0
      docs/source/ref/templatetags.rst
  6. 90
    4
      docs/source/releases/v2.1.rst
  7. 10
    8
      sandbox/apps/gateway/views.py
  8. 2
    1
      sandbox/settings.py
  9. 1
    0
      src/oscar/__init__.py
  10. 7
    41
      src/oscar/apps/checkout/mixins.py
  11. 1
    0
      src/oscar/apps/communication/__init__.py
  12. 210
    0
      src/oscar/apps/communication/abstract_models.py
  13. 2
    2
      src/oscar/apps/communication/admin.py
  14. 72
    0
      src/oscar/apps/communication/app.py
  15. 9
    0
      src/oscar/apps/communication/apps.py
  16. 11
    0
      src/oscar/apps/communication/config.py
  17. 0
    0
      src/oscar/apps/communication/managers.py
  18. 88
    0
      src/oscar/apps/communication/migrations/0001_initial.py
  19. 30
    0
      src/oscar/apps/communication/migrations/0002_reset_table_names.py
  20. 23
    0
      src/oscar/apps/communication/migrations/0003_remove_notification_category_make_code_uppercase.py
  21. 0
    0
      src/oscar/apps/communication/migrations/__init__.py
  22. 26
    0
      src/oscar/apps/communication/models.py
  23. 0
    0
      src/oscar/apps/communication/notifications/__init__.py
  24. 1
    1
      src/oscar/apps/communication/notifications/context_processors.py
  25. 4
    4
      src/oscar/apps/communication/notifications/views.py
  26. 173
    0
      src/oscar/apps/communication/utils.py
  27. 0
    216
      src/oscar/apps/customer/abstract_models.py
  28. 2
    2
      src/oscar/apps/customer/alerts/receivers.py
  29. 140
    111
      src/oscar/apps/customer/alerts/utils.py
  30. 2
    2
      src/oscar/apps/customer/alerts/views.py
  31. 4
    4
      src/oscar/apps/customer/apps.py
  32. 11
    23
      src/oscar/apps/customer/forms.py
  33. 40
    0
      src/oscar/apps/customer/migrations/0006_auto_20190430_1736.py
  34. 4
    11
      src/oscar/apps/customer/mixins.py
  35. 0
    22
      src/oscar/apps/customer/models.py
  36. 19
    11
      src/oscar/apps/customer/notifications/services.py
  37. 27
    113
      src/oscar/apps/customer/utils.py
  38. 23
    27
      src/oscar/apps/customer/views.py
  39. 1
    1
      src/oscar/apps/dashboard/communications/forms.py
  40. 2
    2
      src/oscar/apps/dashboard/communications/views.py
  41. 1
    1
      src/oscar/apps/order/abstract_models.py
  42. 20
    0
      src/oscar/apps/order/migrations/0008_auto_20190301_1035.py
  43. 46
    1
      src/oscar/apps/order/utils.py
  44. 20
    0
      src/oscar/core/loading.py
  45. 4
    0
      src/oscar/defaults.py
  46. 2
    3
      src/oscar/management/commands/oscar_generate_email_content.py
  47. 2
    2
      src/oscar/management/commands/oscar_send_alerts.py
  48. 0
    0
      src/oscar/templates/oscar/communication/email/email_detail.html
  49. 0
    0
      src/oscar/templates/oscar/communication/email/email_list.html
  50. 0
    0
      src/oscar/templates/oscar/communication/emails/base.html
  51. 0
    0
      src/oscar/templates/oscar/communication/emails/base.txt
  52. 3
    2
      src/oscar/templates/oscar/communication/emails/commtype_email_changed_body.html
  53. 3
    2
      src/oscar/templates/oscar/communication/emails/commtype_email_changed_body.txt
  54. 0
    0
      src/oscar/templates/oscar/communication/emails/commtype_email_changed_subject.txt
  55. 1
    1
      src/oscar/templates/oscar/communication/emails/commtype_order_placed_body.html
  56. 0
    0
      src/oscar/templates/oscar/communication/emails/commtype_order_placed_body.txt
  57. 0
    0
      src/oscar/templates/oscar/communication/emails/commtype_order_placed_subject.txt
  58. 3
    2
      src/oscar/templates/oscar/communication/emails/commtype_password_changed_body.html
  59. 3
    2
      src/oscar/templates/oscar/communication/emails/commtype_password_changed_body.txt
  60. 0
    0
      src/oscar/templates/oscar/communication/emails/commtype_password_changed_subject.txt
  61. 3
    2
      src/oscar/templates/oscar/communication/emails/commtype_password_reset_body.html
  62. 3
    2
      src/oscar/templates/oscar/communication/emails/commtype_password_reset_body.txt
  63. 0
    0
      src/oscar/templates/oscar/communication/emails/commtype_password_reset_subject.txt
  64. 6
    4
      src/oscar/templates/oscar/communication/emails/commtype_product_alert_body.html
  65. 4
    3
      src/oscar/templates/oscar/communication/emails/commtype_product_alert_body.txt
  66. 23
    0
      src/oscar/templates/oscar/communication/emails/commtype_product_alert_confirmation_body.html
  67. 17
    0
      src/oscar/templates/oscar/communication/emails/commtype_product_alert_confirmation_body.txt
  68. 0
    0
      src/oscar/templates/oscar/communication/emails/commtype_product_alert_confirmation_subject.txt
  69. 0
    0
      src/oscar/templates/oscar/communication/emails/commtype_product_alert_subject.txt
  70. 0
    0
      src/oscar/templates/oscar/communication/emails/commtype_registration_body.html
  71. 0
    0
      src/oscar/templates/oscar/communication/emails/commtype_registration_body.txt
  72. 0
    0
      src/oscar/templates/oscar/communication/emails/commtype_registration_sms.txt
  73. 0
    0
      src/oscar/templates/oscar/communication/emails/commtype_registration_subject.txt
  74. 0
    0
      src/oscar/templates/oscar/communication/notifications/detail.html
  75. 0
    0
      src/oscar/templates/oscar/communication/notifications/list.html
  76. 0
    13
      src/oscar/templates/oscar/customer/emails/commtype_product_alert_confirmation_body.txt
  77. 13
    0
      src/oscar/templatetags/url_tags.py
  78. 25
    1
      src/oscar/test/utils.py
  79. 4
    0
      src/oscar/utils/deprecation.py
  80. 40
    0
      tests/_site/apps/customer/migrations/0006_auto_20190430_1736.py
  81. 0
    22
      tests/functional/checkout/test_customer_checkout.py
  82. 50
    59
      tests/functional/customer/test_alert.py
  83. 114
    0
      tests/functional/customer/test_emails.py
  84. 5
    3
      tests/functional/customer/test_notification.py
  85. 9
    7
      tests/functional/dashboard/test_communication.py
  86. 2
    1
      tests/functional/dashboard/test_user.py
  87. 0
    0
      tests/functional/order/__init__.py
  88. 61
    0
      tests/functional/order/test_emails.py
  89. 0
    0
      tests/integration/communication/__init__.py
  90. 65
    0
      tests/integration/communication/test_communicationeventtype.py
  91. 67
    0
      tests/integration/communication/test_dispatcher.py
  92. 13
    11
      tests/integration/communication/test_notification.py
  93. 2
    3
      tests/integration/customer/test_alert.py
  94. 1
    1
      tests/integration/customer/test_custom_user_model.py
  95. 0
    34
      tests/integration/customer/test_customer.py
  96. 0
    99
      tests/integration/customer/test_dispatcher.py
  97. 0
    10
      tests/integration/customer/test_models.py
  98. 61
    0
      tests/integration/order/test_order_dispatcher.py
  99. 35
    0
      tests/integration/templatetags/test_url_tags.py
  100. 0
    0
      tests/settings.py

+ 2
- 1
docs/source/internals/getting_started.rst Bestand weergeven

@@ -64,7 +64,7 @@ Now add Oscar's context processors to the template settings, listed below:
64 64
 
65 65
     'oscar.apps.search.context_processors.search_form',
66 66
     'oscar.apps.checkout.context_processors.checkout',
67
-    'oscar.apps.customer.notifications.context_processors.notifications',
67
+    'oscar.apps.communication.notifications.context_processors.notifications',
68 68
     'oscar.core.context_processors.metadata',
69 69
 
70 70
 Next, modify ``INSTALLED_APPS`` to be a list, and add ``django.contrib.sites``,
@@ -91,6 +91,7 @@ depends on. Also set ``SITE_ID``:
91 91
         'oscar.apps.shipping.apps.ShippingConfig',
92 92
         'oscar.apps.catalogue.apps.CatalogueConfig',
93 93
         'oscar.apps.catalogue.reviews.apps.CatalogueReviewsConfig',
94
+        'oscar.apps.communication.apps.CommunicationConfig',
94 95
         'oscar.apps.partner.apps.PartnerConfig',
95 96
         'oscar.apps.basket.apps.BasketConfig',
96 97
         'oscar.apps.payment.apps.PaymentConfig',

+ 22
- 0
docs/source/ref/apps/communication.rst Bestand weergeven

@@ -0,0 +1,22 @@
1
+=============
2
+Communication
3
+=============
4
+
5
+The Communication app is used to manage emails and notifications to users.
6
+If ``OSCAR_SAVE_SENT_EMAILS_TO_DB`` is ``True``, then all sent emails
7
+are saved to the database as instances of ``oscar.apps.communication.models.Email``
8
+
9
+The ``Dispatcher`` class from ``oscar.apps.communication.utils`` is used to send
10
+emails and notifications.
11
+
12
+Abstract models
13
+---------------
14
+
15
+.. automodule:: oscar.apps.communication.abstract_models
16
+    :members:
17
+
18
+Utils
19
+-----
20
+
21
+.. automodule:: oscar.apps.communication.utils
22
+    :members:

+ 13
- 5
docs/source/ref/apps/customer.rst Bestand weergeven

@@ -2,10 +2,9 @@
2 2
 Customer
3 3
 ========
4 4
 
5
-The customer app bundles communication with customers. This includes models
6
-to record product alerts and sent emails. It also contains the views that
7
-allow a customer to manage their data (profile information, shipping addresses,
8
-etc.)
5
+The customer app includes models to record product alerts and sent emails.
6
+It also contains the views that allow a customer to manage their data
7
+(profile information, shipping addresses, etc.)
9 8
 
10 9
 Abstract models
11 10
 ---------------
@@ -40,4 +39,13 @@ The context for the alert email body contains a ``hurry`` variable that is set
40 39
 to ``True`` if the number of active alerts for a product is greater than the
41 40
 quantity of the product available in stock.
42 41
 
43
-Alerts are sent using the Communication Event framework.
42
+CustomerDispatcher
43
+------------------
44
+
45
+``oscar.apps.customer.utils.CustomerDispatcher`` is used to send customer emails
46
+(e.g., registration, password reset, order confirmations).
47
+
48
+AlertsDispatcher
49
+----------------
50
+
51
+``oscar.apps.customer.alerts.utils.AlertsDispatcher`` is used to send product alerts.

+ 16
- 0
docs/source/ref/settings.rst Bestand weergeven

@@ -292,6 +292,14 @@ A URL which is passed into the templates for communication events.  It is not
292 292
 used in Oscar's default templates but could be used to include static assets
293 293
 (e.g. images) in a HTML email template.
294 294
 
295
+``OSCAR_SAVE_SENT_EMAILS_TO_DB``
296
+--------------------------------
297
+
298
+Default: ``True``
299
+
300
+Indicates if sent emails will be saved to database as instances of
301
+``oscar.apps.communication.models.Email``.
302
+
295 303
 Offer settings
296 304
 ==============
297 305
 
@@ -530,3 +538,11 @@ Default: ``False``
530 538
 A flag to control whether Oscar's CSV writer should prepend a byte order mark
531 539
 (BOM) to CSV files that are encoded in UTF-8. Useful for compatibility with some
532 540
 CSV readers, Microsoft Excel in particular.
541
+
542
+
543
+``OSCAR_URL_SCHEMA``
544
+--------------------
545
+
546
+Default: ``http``
547
+
548
+The schema that will be used to build absolute url in ``absolute_url`` template tag.

+ 27
- 0
docs/source/ref/templatetags.rst Bestand weergeven

@@ -193,3 +193,30 @@ This renders something like:
193 193
 .. code-block:: html
194 194
 
195 195
     Time since creation: 2 days
196
+
197
+
198
+URL tags
199
+-----------
200
+
201
+Load these tags using ``{% load url_tags %}``.
202
+
203
+``absolute_url``
204
+~~~~~~~~~~~~~~~~~~
205
+
206
+Returns an absolute URL for the provided domain and path.
207
+
208
+.. code-block:: html+django
209
+
210
+    <a href="{% absolute_url site.domain reset_url %}" class="btn-primary">Reset password</a>
211
+
212
+This tag can be used with ``blocktrans`` as follows:
213
+
214
+.. code-block:: html+django
215
+
216
+    {% absolute_url site.domain reset_url as absolute_reset_url %}
217
+    {% blocktrans with url=absolute_reset_url %}
218
+        You can reset your password here - {{ url }}
219
+    {% endblocktrans %}
220
+
221
+The schema for absolute url can be set through ``OSCAR_URL_SCHEMA``
222
+setting (``http`` by default).

+ 90
- 4
docs/source/releases/v2.1.rst Bestand weergeven

@@ -1,6 +1,6 @@
1
-=======================
1
+========================================
2 2
 Oscar 2.1 release notes (in development)
3
-=======================
3
+========================================
4 4
 
5 5
 :release: tbd
6 6
 
@@ -19,7 +19,7 @@ Compatibility
19 19
 .. _new_in_2.1:
20 20
 
21 21
 What's new in Oscar 2.1?
22
-~~~~~~~~~~~~~~~~~~~~~~~~
22
+~~~~~~~~~~~~~~~~~~~~~~~~~~
23 23
 
24 24
 - The database performance of ``offer.Range.all_products()`` was substantially
25 25
   improved. The internals of that method have changed and specifically
@@ -31,6 +31,68 @@ What's new in Oscar 2.1?
31 31
   Django doesn't generate migrations if a project modifies the ``OSCAR_IMAGE_FOLDER``
32 32
   to specify a custom directory structure for uploaded images.
33 33
 
34
+Communications app
35
+------------------
36
+
37
+A new ``communication`` app was introduced to provide a single point of entry
38
+for all communications sent by Oscar. This is a significant change with implications
39
+as follows:
40
+
41
+- Projects will need to add
42
+  ``oscar.apps.communication.apps.CommunicationConfig`` to ``INSTALLED_APPS``.
43
+
44
+  The ``CommunicationEventType``, ``Email`` and ``Notification`` models have
45
+  moved from the ``customer`` app to the ``communication`` app. In order to
46
+  preserve existing data, the table names for these models are unchanged.
47
+
48
+  This is a change that requires database migration.
49
+
50
+- The ``Dispatcher`` class moved from ``customer.utils`` to
51
+  ``communication.utils``. ``Dispatcher`` is now responsible for sending
52
+  all notifications, and not just emails.
53
+
54
+- A ``CustomerDispatcher`` utility class that wraps the core ``Dispatcher``
55
+  has been introduced to the ``customer`` app for sending communications to
56
+  customers.
57
+
58
+- An ``AlertsDispatcher`` utility class that wraps the core ``Dispatcher``
59
+  has been introduced to the ``customer.alerts`` module for sending product
60
+  alerts.
61
+
62
+- An ``OrderDispatcher``  utility class that wraps the core ``Dispatcher``
63
+  has been introduced to the ``order`` app for sending order related
64
+  communications.
65
+
66
+- A new setting, ``OSCAR_SAVE_SENT_EMAILS_TO_DB`` controls whether emails
67
+  sent through the ``Dispatcher`` are saved to the database. This defaults
68
+  to ``True``.
69
+
70
+- The ability to send multipart emails with attachments was added to the new
71
+  dispatcher.
72
+
73
+- All communication email templates (``commtype_*``) have moved from
74
+  moved from ``customer/emails`` to ``communication/emails``.
75
+
76
+- Templates in ``customer/email/`` and ``customer/notification/`` have moved
77
+  to ``communication/email/`` and ``communication/notification/``.
78
+
79
+- An ``absolute_url`` template tag was introduced to facilitate generating
80
+  absolute URLs in templates for a given domain and path. The schema for
81
+  generated URLs is configured via the ``OSCAR_URL_SCHEMA`` setting, which defaults
82
+  to ``http``.
83
+
84
+Backwards incompatible changes in Oscar 2.1
85
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
86
+
87
+- The ``category`` field has been removed from the
88
+  ``communication.Notification`` model. This change requires database migration.
89
+
90
+- The ``checkout.mixins.OrderPlacementMixin.send_confirmation_message``
91
+  method has been replaced with a new ``send_order_placed_email`` method.
92
+
93
+- ``customer.notifications.context_processors.notifications`` has moved to
94
+  ``communication.notifications.context_processors.notifications``.
95
+
34 96
 Bug fixes
35 97
 ~~~~~~~~~
36 98
 
@@ -64,4 +126,28 @@ Dependency changes
64 126
 .. _deprecated_features_in_2.1:
65 127
 
66 128
 Deprecated features
67
-~~~~~~~~~~~~~~~~~~
129
+~~~~~~~~~~~~~~~~~~~
130
+
131
+- ``customer.alerts.utils.send_alerts`` is deprecated.
132
+  Use ``AlertsDispatcher.send_alerts`` instead.
133
+
134
+- ``customer.alerts.utils.send_alert_confirmation`` is deprecated.
135
+  Use ``AlertsDispatcher.send_product_alert_confirmation_email_for_user``
136
+  instead.
137
+
138
+- ``customer.alerts.utils.send_product_alerts`` is deprecated.
139
+  Use ``AlertsDispatcher.send_product_alert_email_for_user`` instead.
140
+
141
+- ``customer.notifications.services.notify_user`` is deprecated.
142
+  Use Dispatcher.notify_user``.
143
+
144
+- ``customer.notifications.services.notify_users`` is deprecated.
145
+  Use ``Dispatcher.notify_users`` instead.
146
+
147
+- ``customer.forms.PasswordResetForm.get_reset_url`` has been removed.
148
+
149
+- ``customer.views.ProfileUpdateView.form_valid`` was modified
150
+  to use a new ``send_email_changed_email`` method.
151
+
152
+- ``customer.views.ChangePasswordView.form_valid`` was modified
153
+  to use a new ``send_password_changed_email`` method.

+ 10
- 8
sandbox/apps/gateway/views.py Bestand weergeven

@@ -3,13 +3,15 @@ import logging
3 3
 from django import http
4 4
 from django.contrib import messages
5 5
 from django.contrib.auth.models import User
6
-from django.core.mail import send_mail
7 6
 from django.template.loader import get_template
8 7
 from django.urls import reverse
9 8
 from django.views import generic
10 9
 
11 10
 from apps.gateway import forms
12 11
 from oscar.apps.customer.forms import generate_username
12
+from oscar.core.loading import get_class
13
+
14
+Dispatcher = get_class('communication.utils', 'Dispatcher')
13 15
 
14 16
 logger = logging.getLogger('gateway')
15 17
 
@@ -41,10 +43,10 @@ class GatewayView(generic.FormView):
41 43
         return user
42 44
 
43 45
     def send_confirmation_email(self, real_email, user, password):
44
-        msg = get_template('gateway/email.txt').render({
45
-            'email': user.email,
46
-            'password': password
47
-        })
48
-        send_mail('Dashboard access to Oscar sandbox',
49
-                  msg, 'blackhole@latest.oscarcommerce.com',
50
-                  [real_email])
46
+        msgs = {
47
+            'subject': 'Dashboard access to Oscar sandbox',
48
+            'body': get_template('gateway/email.txt').render({
49
+                'email': user.email, 'password': password})
50
+        }
51
+        Dispatcher().send_email_messages(
52
+            real_email, msgs, from_email='blackhole@latest.oscarcommerce.com')

+ 2
- 1
sandbox/settings.py Bestand weergeven

@@ -131,7 +131,7 @@ TEMPLATES = [
131 131
 
132 132
                 # Oscar specific
133 133
                 'oscar.apps.search.context_processors.search_form',
134
-                'oscar.apps.customer.notifications.context_processors.notifications',
134
+                'oscar.apps.communication.notifications.context_processors.notifications',
135 135
                 'oscar.apps.checkout.context_processors.checkout',
136 136
                 'oscar.core.context_processors.metadata',
137 137
             ],
@@ -264,6 +264,7 @@ INSTALLED_APPS = [
264 264
     'oscar.apps.shipping.apps.ShippingConfig',
265 265
     'oscar.apps.catalogue.apps.CatalogueConfig',
266 266
     'oscar.apps.catalogue.reviews.apps.CatalogueReviewsConfig',
267
+    'oscar.apps.communication.apps.CommunicationConfig',
267 268
     'oscar.apps.partner.apps.PartnerConfig',
268 269
     'oscar.apps.basket.apps.BasketConfig',
269 270
     'oscar.apps.payment.apps.PaymentConfig',

+ 1
- 0
src/oscar/__init__.py Bestand weergeven

@@ -36,6 +36,7 @@ INSTALLED_APPS = [
36 36
     'oscar.apps.shipping.apps.ShippingConfig',
37 37
     'oscar.apps.catalogue.apps.CatalogueConfig',
38 38
     'oscar.apps.catalogue.reviews.apps.CatalogueReviewsConfig',
39
+    'oscar.apps.communication.apps.CommunicationConfig',
39 40
     'oscar.apps.partner.apps.PartnerConfig',
40 41
     'oscar.apps.basket.apps.BasketConfig',
41 42
     'oscar.apps.payment.apps.PaymentConfig',

+ 7
- 41
src/oscar/apps/checkout/mixins.py Bestand weergeven

@@ -1,7 +1,6 @@
1 1
 import logging
2 2
 
3 3
 from django.contrib.sites.models import Site
4
-from django.contrib.sites.shortcuts import get_current_site
5 4
 from django.core.exceptions import ObjectDoesNotExist
6 5
 from django.http import HttpResponseRedirect
7 6
 from django.urls import NoReverseMatch, reverse
@@ -10,7 +9,7 @@ from oscar.apps.checkout.signals import post_checkout
10 9
 from oscar.core.loading import get_class, get_model
11 10
 
12 11
 OrderCreator = get_class('order.utils', 'OrderCreator')
13
-Dispatcher = get_class('customer.utils', 'Dispatcher')
12
+OrderDispatcher = get_class('order.utils', 'OrderDispatcher')
14 13
 CheckoutSessionMixin = get_class('checkout.session', 'CheckoutSessionMixin')
15 14
 BillingAddress = get_model('order', 'BillingAddress')
16 15
 ShippingAddress = get_model('order', 'ShippingAddress')
@@ -20,7 +19,6 @@ PaymentEvent = get_model('order', 'PaymentEvent')
20 19
 PaymentEventQuantity = get_model('order', 'PaymentEventQuantity')
21 20
 UserAddress = get_model('address', 'UserAddress')
22 21
 Basket = get_model('basket', 'Basket')
23
-CommunicationEventType = get_model('customer', 'CommunicationEventType')
24 22
 
25 23
 # Standard logger for checkout events
26 24
 logger = logging.getLogger('oscar.checkout')
@@ -42,9 +40,6 @@ class OrderPlacementMixin(CheckoutSessionMixin):
42 40
     # handle_payment method.
43 41
     _payment_events = None
44 42
 
45
-    # Default code for the email to send after successful checkout
46
-    communication_type_code = 'ORDER_PLACED'
47
-
48 43
     view_signal = post_checkout
49 44
 
50 45
     # Payment handling methods
@@ -248,7 +243,7 @@ class OrderPlacementMixin(CheckoutSessionMixin):
248 243
         order is submitted.
249 244
         """
250 245
         # Send confirmation message (normally an email)
251
-        self.send_confirmation_message(order, self.communication_type_code)
246
+        self.send_order_placed_email(order)
252 247
 
253 248
         # Flush all session data
254 249
         self.checkout_session.flush()
@@ -268,44 +263,15 @@ class OrderPlacementMixin(CheckoutSessionMixin):
268 263
     def get_success_url(self):
269 264
         return reverse('checkout:thank-you')
270 265
 
271
-    def send_confirmation_message(self, order, code, **kwargs):
272
-        try:
273
-            ctx = self.get_message_context(order, code)
274
-        except TypeError:
275
-            # It seems like the get_message_context method was overridden and
276
-            # it does not support the code argument yet
277
-            logger.warning(
278
-                'The signature of the get_message_context method has changed, '
279
-                'please update it in your codebase'
280
-            )
281
-            ctx = self.get_message_context(order)
282
-
283
-        try:
284
-            event_type = CommunicationEventType.objects.get(code=code)
285
-        except CommunicationEventType.DoesNotExist:
286
-            # No event-type in database, attempt to find templates for this
287
-            # type and render them immediately to get the messages.  Since we
288
-            # have not CommunicationEventType to link to, we can't create a
289
-            # CommunicationEvent instance.
290
-            messages = CommunicationEventType.objects.get_and_render(code, ctx)
291
-            event_type = None
292
-        else:
293
-            messages = event_type.get_messages(ctx)
294
-
295
-        if messages and messages['body']:
296
-            logger.info("Order #%s - sending %s messages", order.number, code)
297
-            dispatcher = Dispatcher(logger)
298
-            dispatcher.dispatch_order_messages(order, messages,
299
-                                               event_type, **kwargs)
300
-        else:
301
-            logger.warning("Order #%s - no %s communication event type",
302
-                           order.number, code)
266
+    def send_order_placed_email(self, order):
267
+        extra_context = self.get_message_context(order)
268
+        dispatcher = OrderDispatcher(logger=logger)
269
+        dispatcher.send_order_placed_email_for_user(order, extra_context)
303 270
 
304
-    def get_message_context(self, order, code=None):
271
+    def get_message_context(self, order):
305 272
         ctx = {
306 273
             'user': self.request.user,
307 274
             'order': order,
308
-            'site': get_current_site(self.request),
309 275
             'lines': order.lines.all()
310 276
         }
311 277
 

+ 1
- 0
src/oscar/apps/communication/__init__.py Bestand weergeven

@@ -0,0 +1 @@
1
+default_app_config = 'oscar.apps.communication.apps.CommunicationConfig'

+ 210
- 0
src/oscar/apps/communication/abstract_models.py Bestand weergeven

@@ -0,0 +1,210 @@
1
+from django.conf import settings
2
+from django.core.validators import RegexValidator
3
+from django.db import models
4
+from django.template import engines
5
+from django.template.exceptions import TemplateDoesNotExist
6
+from django.template.loader import get_template
7
+from django.utils.translation import gettext_lazy as _
8
+
9
+from oscar.apps.communication.managers import CommunicationTypeManager
10
+from oscar.core.compat import AUTH_USER_MODEL
11
+from oscar.models.fields import AutoSlugField
12
+
13
+
14
+class AbstractEmail(models.Model):
15
+    """
16
+    This is a record of an email sent to a customer.
17
+    """
18
+    user = models.ForeignKey(
19
+        AUTH_USER_MODEL,
20
+        on_delete=models.CASCADE,
21
+        related_name='emails',
22
+        verbose_name=_("User"),
23
+        null=True)
24
+    email = models.EmailField(_('Email Address'), null=True, blank=True)
25
+    subject = models.TextField(_('Subject'), max_length=255)
26
+    body_text = models.TextField(_("Body Text"))
27
+    body_html = models.TextField(_("Body HTML"), blank=True)
28
+    date_sent = models.DateTimeField(_("Date Sent"), auto_now_add=True)
29
+
30
+    class Meta:
31
+        abstract = True
32
+        app_label = 'communication'
33
+        verbose_name = _('Email')
34
+        verbose_name_plural = _('Emails')
35
+
36
+    def __str__(self):
37
+        if self.user:
38
+            return _("Email to %(user)s with subject '%(subject)s'") % {
39
+                'user': self.user.get_username(), 'subject': self.subject}
40
+        else:
41
+            return _("Email to %(email)s with subject '%(subject)s'") % {
42
+                'email': self.email, 'subject': self.subject}
43
+
44
+
45
+class AbstractCommunicationEventType(models.Model):
46
+    """
47
+    A 'type' of communication.  Like an order confirmation email.
48
+    """
49
+
50
+    #: Code used for looking up this event programmatically.
51
+    # e.g. PASSWORD_RESET. AutoSlugField uppercases the code for us because
52
+    # it's a useful convention that's been enforced in previous Oscar versions
53
+    code = AutoSlugField(
54
+        _('Code'), max_length=128, unique=True, populate_from='name',
55
+        separator='_', uppercase=True, editable=True,
56
+        validators=[
57
+            RegexValidator(
58
+                regex=r'^[A-Z_][0-9A-Z_]*$',
59
+                message=_(
60
+                    "Code can only contain the uppercase letters (A-Z), "
61
+                    "digits, and underscores, and can't start with a digit."))],
62
+        help_text=_("Code used for looking up this event programmatically"))
63
+
64
+    #: Name is the friendly description of an event for use in the admin
65
+    name = models.CharField(_('Name'), max_length=255)
66
+
67
+    # We allow communication types to be categorised
68
+    # For backwards-compatibility, the choice values are quite verbose
69
+    ORDER_RELATED = 'Order related'
70
+    USER_RELATED = 'User related'
71
+    CATEGORY_CHOICES = (
72
+        (ORDER_RELATED, _('Order related')),
73
+        (USER_RELATED, _('User related'))
74
+    )
75
+
76
+    category = models.CharField(
77
+        _('Category'), max_length=255, default=ORDER_RELATED,
78
+        choices=CATEGORY_CHOICES)
79
+
80
+    # Template content for emails
81
+    # NOTE: There's an intentional distinction between None and ''. None
82
+    # instructs Oscar to look for a file-based template, '' is just an empty
83
+    # template.
84
+    email_subject_template = models.CharField(
85
+        _('Email Subject Template'), max_length=255, blank=True, null=True)
86
+    email_body_template = models.TextField(
87
+        _('Email Body Template'), blank=True, null=True)
88
+    email_body_html_template = models.TextField(
89
+        _('Email Body HTML Template'), blank=True, null=True,
90
+        help_text=_("HTML template"))
91
+
92
+    # Template content for SMS messages
93
+    sms_template = models.CharField(_('SMS Template'), max_length=170,
94
+                                    blank=True, null=True,
95
+                                    help_text=_("SMS template"))
96
+
97
+    date_created = models.DateTimeField(_("Date Created"), auto_now_add=True)
98
+    date_updated = models.DateTimeField(_("Date Updated"), auto_now=True)
99
+
100
+    objects = CommunicationTypeManager()
101
+
102
+    # File templates
103
+    email_subject_template_file = 'oscar/communication/emails/commtype_%s_subject.txt'
104
+    email_body_template_file = 'oscar/communication/emails/commtype_%s_body.txt'
105
+    email_body_html_template_file = 'oscar/communication/emails/commtype_%s_body.html'
106
+    sms_template_file = 'oscar/communication/sms/commtype_%s_body.txt'
107
+
108
+    class Meta:
109
+        abstract = True
110
+        app_label = 'communication'
111
+        verbose_name = _("Communication event type")
112
+        verbose_name_plural = _("Communication event types")
113
+
114
+    def get_messages(self, ctx=None):
115
+        """
116
+        Return a dict of templates with the context merged in
117
+
118
+        We look first at the field templates but fail over to
119
+        a set of file templates that follow a conventional path.
120
+        """
121
+        code = self.code.lower()
122
+
123
+        # Build a dict of message name to Template instances
124
+        templates = {'subject': 'email_subject_template',
125
+                     'body': 'email_body_template',
126
+                     'html': 'email_body_html_template',
127
+                     'sms': 'sms_template'}
128
+        for name, attr_name in templates.items():
129
+            field = getattr(self, attr_name, None)
130
+            if field is not None:
131
+                # Template content is in a model field
132
+                templates[name] = engines['django'].from_string(field)
133
+            else:
134
+                # Model field is empty - look for a file template
135
+                template_name = getattr(self, "%s_file" % attr_name) % code
136
+                try:
137
+                    templates[name] = get_template(template_name)
138
+                except TemplateDoesNotExist:
139
+                    templates[name] = None
140
+
141
+        # Pass base URL for serving images within HTML emails
142
+        if ctx is None:
143
+            ctx = {}
144
+        ctx['static_base_url'] = getattr(
145
+            settings, 'OSCAR_STATIC_BASE_URL', None)
146
+
147
+        messages = {}
148
+        for name, template in templates.items():
149
+            messages[name] = template.render(ctx) if template else ''
150
+
151
+        # Ensure the email subject doesn't contain any newlines
152
+        messages['subject'] = messages['subject'].replace("\n", "")
153
+        messages['subject'] = messages['subject'].replace("\r", "")
154
+
155
+        return messages
156
+
157
+    def __str__(self):
158
+        return self.name
159
+
160
+    def is_order_related(self):
161
+        return self.category == self.ORDER_RELATED
162
+
163
+    def is_user_related(self):
164
+        return self.category == self.USER_RELATED
165
+
166
+
167
+class AbstractNotification(models.Model):
168
+    recipient = models.ForeignKey(
169
+        AUTH_USER_MODEL,
170
+        on_delete=models.CASCADE,
171
+        related_name='notifications')
172
+
173
+    # Not all notifications will have a sender.
174
+    sender = models.ForeignKey(
175
+        AUTH_USER_MODEL,
176
+        on_delete=models.CASCADE,
177
+        null=True)
178
+
179
+    # HTML is allowed in this field as it can contain links
180
+    subject = models.CharField(max_length=255)
181
+    body = models.TextField()
182
+
183
+    INBOX, ARCHIVE = 'Inbox', 'Archive'
184
+    choices = (
185
+        (INBOX, _('Inbox')),
186
+        (ARCHIVE, _('Archive')))
187
+    location = models.CharField(max_length=32, choices=choices,
188
+                                default=INBOX)
189
+
190
+    date_sent = models.DateTimeField(auto_now_add=True)
191
+    date_read = models.DateTimeField(blank=True, null=True)
192
+
193
+    class Meta:
194
+        abstract = True
195
+        app_label = 'communication'
196
+        ordering = ('-date_sent',)
197
+        verbose_name = _('Notification')
198
+        verbose_name_plural = _('Notifications')
199
+
200
+    def __str__(self):
201
+        return self.subject
202
+
203
+    def archive(self):
204
+        self.location = self.ARCHIVE
205
+        self.save()
206
+    archive.alters_data = True
207
+
208
+    @property
209
+    def is_read(self):
210
+        return self.date_read is not None

src/oscar/apps/customer/admin.py → src/oscar/apps/communication/admin.py Bestand weergeven

@@ -2,8 +2,8 @@ from django.contrib import admin
2 2
 
3 3
 from oscar.core.loading import get_model
4 4
 
5
-CommunicationEventType = get_model('customer', 'CommunicationEventType')
6
-Email = get_model('customer', 'Email')
5
+CommunicationEventType = get_model('communication', 'CommunicationEventType')
6
+Email = get_model('communication', 'Email')
7 7
 
8 8
 
9 9
 admin.site.register(Email)

+ 72
- 0
src/oscar/apps/communication/app.py Bestand weergeven

@@ -0,0 +1,72 @@
1
+from django.conf.urls import url
2
+from django.contrib.auth.decorators import login_required
3
+from django.views import generic
4
+
5
+from oscar.core.application import Application
6
+from oscar.core.loading import get_class
7
+
8
+
9
+class CommunicationApplication(Application):
10
+    name = 'communication'
11
+
12
+    alert_list_view = get_class(
13
+        'communication.alerts.views', 'ProductAlertListView')
14
+    alert_create_view = get_class(
15
+        'communication.alerts.views', 'ProductAlertCreateView')
16
+    alert_confirm_view = get_class(
17
+        'communication.alerts.views', 'ProductAlertConfirmView')
18
+    alert_cancel_view = get_class(
19
+        'communication.alerts.views', 'ProductAlertCancelView')
20
+
21
+    notification_inbox_view = get_class(
22
+        'communication.notifications.views', 'InboxView')
23
+    notification_archive_view = get_class(
24
+        'communication.notifications.views', 'ArchiveView')
25
+    notification_update_view = get_class(
26
+        'communication.notifications.views', 'UpdateView')
27
+    notification_detail_view = get_class(
28
+        'communication.notifications.views', 'DetailView')
29
+
30
+    def get_urls(self):
31
+        urls = [
32
+            # Alerts
33
+            # Alerts can be setup by anonymous users: some views do not
34
+            # require login
35
+            url(r'^alerts/$',
36
+                login_required(self.alert_list_view.as_view()),
37
+                name='alerts-list'),
38
+            url(r'^alerts/create/(?P<pk>\d+)/$',
39
+                self.alert_create_view.as_view(),
40
+                name='alert-create'),
41
+            url(r'^alerts/confirm/(?P<key>[a-z0-9]+)/$',
42
+                self.alert_confirm_view.as_view(),
43
+                name='alerts-confirm'),
44
+            url(r'^alerts/cancel/key/(?P<key>[a-z0-9]+)/$',
45
+                self.alert_cancel_view.as_view(),
46
+                name='alerts-cancel-by-key'),
47
+            url(r'^alerts/cancel/(?P<pk>[a-z0-9]+)/$',
48
+                login_required(self.alert_cancel_view.as_view()),
49
+                name='alerts-cancel-by-pk'),
50
+
51
+            # Notifications
52
+            # Redirect to notification inbox
53
+            url(r'^notifications/$', generic.RedirectView.as_view(
54
+                url='/accounts/notifications/inbox/', permanent=False)),
55
+            url(r'^notifications/inbox/$',
56
+                login_required(self.notification_inbox_view.as_view()),
57
+                name='notifications-inbox'),
58
+            url(r'^notifications/archive/$',
59
+                login_required(self.notification_archive_view.as_view()),
60
+                name='notifications-archive'),
61
+            url(r'^notifications/update/$',
62
+                login_required(self.notification_update_view.as_view()),
63
+                name='notifications-update'),
64
+            url(r'^notifications/(?P<pk>\d+)/$',
65
+                login_required(self.notification_detail_view.as_view()),
66
+                name='notifications-detail'),
67
+        ]
68
+
69
+        return self.post_process_urls(urls)
70
+
71
+
72
+application = CommunicationApplication()

+ 9
- 0
src/oscar/apps/communication/apps.py Bestand weergeven

@@ -0,0 +1,9 @@
1
+from django.utils.translation import gettext_lazy as _
2
+
3
+from oscar.core.application import OscarConfig
4
+
5
+
6
+class CommunicationConfig(OscarConfig):
7
+    label = 'communication'
8
+    name = 'oscar.apps.communication'
9
+    verbose_name = _('Communication')

+ 11
- 0
src/oscar/apps/communication/config.py Bestand weergeven

@@ -0,0 +1,11 @@
1
+from django.apps import AppConfig
2
+from django.utils.translation import gettext_lazy as _
3
+
4
+
5
+class CommunicationConfig(AppConfig):
6
+    label = 'communication'
7
+    name = 'oscar.apps.communication'
8
+    verbose_name = _('Communication')
9
+
10
+    def ready(self):
11
+        from .alerts import receivers  # noqa

src/oscar/apps/customer/managers.py → src/oscar/apps/communication/managers.py Bestand weergeven


+ 88
- 0
src/oscar/apps/communication/migrations/0001_initial.py Bestand weergeven

@@ -0,0 +1,88 @@
1
+# -*- coding: utf-8 -*-
2
+# Generated by Django 1.11.10 on 2018-02-18 10:32
3
+from __future__ import unicode_literals
4
+
5
+from django.conf import settings
6
+from django.db import migrations, models
7
+import django.db.models.deletion
8
+import oscar.models.fields.autoslugfield
9
+
10
+
11
+class Migration(migrations.Migration):
12
+
13
+    initial = True
14
+
15
+    dependencies = [
16
+        ('catalogue', '0013_auto_20170821_1548'),
17
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
18
+        ('customer', '0005_auto_20181115_1953'),
19
+    ]
20
+
21
+    state_operations = [
22
+        migrations.CreateModel(
23
+            name='CommunicationEventType',
24
+            fields=[
25
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
26
+                ('code', oscar.models.fields.autoslugfield.AutoSlugField(blank=True, editable=False, help_text='Code used for looking up this event programmatically', max_length=128, populate_from='name', separator='_', unique=True, validators=[django.core.validators.RegexValidator(message="Code can only contain the letters a-z, A-Z, digits, and underscores, and can't start with a digit.", regex='^[a-zA-Z_][0-9a-zA-Z_]*$')], verbose_name='Code')),
27
+                ('name', models.CharField(max_length=255, verbose_name='Name')),
28
+                ('category', models.CharField(choices=[('Order related', 'Order related'), ('User related', 'User related')], default='Order related', max_length=255, verbose_name='Category')),
29
+                ('email_subject_template', models.CharField(blank=True, max_length=255, null=True, verbose_name='Email Subject Template')),
30
+                ('email_body_template', models.TextField(blank=True, null=True, verbose_name='Email Body Template')),
31
+                ('email_body_html_template', models.TextField(blank=True, help_text='HTML template', null=True, verbose_name='Email Body HTML Template')),
32
+                ('sms_template', models.CharField(blank=True, help_text='SMS template', max_length=170, null=True, verbose_name='SMS Template')),
33
+                ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date Created')),
34
+                ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date Updated')),
35
+            ],
36
+            options={
37
+                'verbose_name': 'Communication event type',
38
+                'verbose_name_plural': 'Communication event types',
39
+                'db_table': 'customer_communicationeventtype',
40
+                'abstract': False,
41
+            },
42
+        ),
43
+        migrations.CreateModel(
44
+            name='Email',
45
+            fields=[
46
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
47
+                ('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='Email Address')),
48
+                ('subject', models.TextField(max_length=255, verbose_name='Subject')),
49
+                ('body_text', models.TextField(verbose_name='Body Text')),
50
+                ('body_html', models.TextField(blank=True, verbose_name='Body HTML')),
51
+                ('date_sent', models.DateTimeField(auto_now_add=True, verbose_name='Date Sent')),
52
+                ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='emails', to=settings.AUTH_USER_MODEL, verbose_name='User')),
53
+            ],
54
+            options={
55
+                'verbose_name': 'Email',
56
+                'verbose_name_plural': 'Emails',
57
+                'db_table': 'customer_email',
58
+                'abstract': False,
59
+            },
60
+        ),
61
+        migrations.CreateModel(
62
+            name='Notification',
63
+            fields=[
64
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
65
+                ('subject', models.CharField(max_length=255)),
66
+                ('body', models.TextField()),
67
+                ('category', models.CharField(blank=True, max_length=255)),
68
+                ('location', models.CharField(choices=[('Inbox', 'Inbox'), ('Archive', 'Archive')], default='Inbox', max_length=32)),
69
+                ('date_sent', models.DateTimeField(auto_now_add=True)),
70
+                ('date_read', models.DateTimeField(blank=True, null=True)),
71
+                ('recipient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL)),
72
+                ('sender', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
73
+            ],
74
+            options={
75
+                'verbose_name': 'Notification',
76
+                'verbose_name_plural': 'Notifications',
77
+                'db_table': 'customer_notification',
78
+                'ordering': ('-date_sent',),
79
+                'abstract': False,
80
+            },
81
+        ),
82
+    ]
83
+
84
+    operations = [
85
+        migrations.SeparateDatabaseAndState(
86
+            state_operations=state_operations
87
+        ),
88
+    ]

+ 30
- 0
src/oscar/apps/communication/migrations/0002_reset_table_names.py Bestand weergeven

@@ -0,0 +1,30 @@
1
+# Generated by Django 2.0.13 on 2019-04-30 16:35
2
+
3
+from django.db import migrations
4
+
5
+
6
+class Migration(migrations.Migration):
7
+
8
+    atomic = False
9
+
10
+    dependencies = [
11
+        ('communication', '0001_initial'),
12
+        ('order', '0008_auto_20190301_1035'),
13
+    ]
14
+
15
+    operations = [
16
+        # Alter the names of the tables migrated from the customer app
17
+        # to the Django defaults.
18
+        migrations.AlterModelTable(
19
+            name='communicationeventtype',
20
+            table=None,
21
+        ),
22
+        migrations.AlterModelTable(
23
+            name='email',
24
+            table=None,
25
+        ),
26
+        migrations.AlterModelTable(
27
+            name='notification',
28
+            table=None,
29
+        ),
30
+    ]

+ 23
- 0
src/oscar/apps/communication/migrations/0003_remove_notification_category_make_code_uppercase.py Bestand weergeven

@@ -0,0 +1,23 @@
1
+# Generated by Django 2.0.13 on 2019-05-26 08:03
2
+import django.core.validators
3
+from django.db import migrations
4
+import oscar.models.fields.autoslugfield
5
+
6
+
7
+class Migration(migrations.Migration):
8
+
9
+    dependencies = [
10
+        ('communication', '0002_reset_table_names'),
11
+    ]
12
+
13
+    operations = [
14
+        migrations.RemoveField(
15
+            model_name='notification',
16
+            name='category',
17
+        ),
18
+        migrations.AlterField(
19
+            model_name='communicationeventtype',
20
+            name='code',
21
+            field=oscar.models.fields.autoslugfield.AutoSlugField(blank=True, editable=False, help_text='Code used for looking up this event programmatically', max_length=128, populate_from='name', separator='_', unique=True, validators=[django.core.validators.RegexValidator(message="Code can only contain the uppercase letters (A-Z), digits, and underscores, and can't start with a digit.", regex='^[A-Z_][0-9A-Z_]*$')], verbose_name='Code'),
22
+        ),
23
+    ]

src/oscar/apps/customer/notifications/__init__.py → src/oscar/apps/communication/migrations/__init__.py Bestand weergeven


+ 26
- 0
src/oscar/apps/communication/models.py Bestand weergeven

@@ -0,0 +1,26 @@
1
+from oscar.core.loading import is_model_registered
2
+
3
+from .abstract_models import *  # noqa
4
+
5
+__all__ = []
6
+
7
+
8
+if not is_model_registered('communication', 'Email'):
9
+    class Email(AbstractEmail):
10
+        pass
11
+
12
+    __all__.append('Email')
13
+
14
+
15
+if not is_model_registered('communication', 'CommunicationEventType'):
16
+    class CommunicationEventType(AbstractCommunicationEventType):
17
+        pass
18
+
19
+    __all__.append('CommunicationEventType')
20
+
21
+
22
+if not is_model_registered('communication', 'Notification'):
23
+    class Notification(AbstractNotification):
24
+        pass
25
+
26
+    __all__.append('Notification')

src/oscar/templates/oscar/customer/emails/commtype_registration_sms.txt → src/oscar/apps/communication/notifications/__init__.py Bestand weergeven


src/oscar/apps/customer/notifications/context_processors.py → src/oscar/apps/communication/notifications/context_processors.py Bestand weergeven

@@ -1,6 +1,6 @@
1 1
 from oscar.core.loading import get_model
2 2
 
3
-Notification = get_model('customer', 'Notification')
3
+Notification = get_model('communication', 'Notification')
4 4
 
5 5
 
6 6
 def notifications(request):

src/oscar/apps/customer/notifications/views.py → src/oscar/apps/communication/notifications/views.py Bestand weergeven

@@ -11,12 +11,12 @@ from oscar.core.utils import redirect_to_referrer
11 11
 from oscar.views.generic import BulkEditMixin
12 12
 
13 13
 PageTitleMixin = get_class('customer.mixins', 'PageTitleMixin')
14
-Notification = get_model('customer', 'Notification')
14
+Notification = get_model('communication', 'Notification')
15 15
 
16 16
 
17 17
 class NotificationListView(PageTitleMixin, generic.ListView):
18 18
     model = Notification
19
-    template_name = 'oscar/customer/notifications/list.html'
19
+    template_name = 'oscar/communication/notifications/list.html'
20 20
     context_object_name = 'notifications'
21 21
     paginate_by = settings.OSCAR_NOTIFICATIONS_PER_PAGE
22 22
     page_title = _("Notifications")
@@ -48,7 +48,7 @@ class ArchiveView(NotificationListView):
48 48
 
49 49
 class DetailView(PageTitleMixin, generic.DetailView):
50 50
     model = Notification
51
-    template_name = 'oscar/customer/notifications/detail.html'
51
+    template_name = 'oscar/communication/notifications/detail.html'
52 52
     context_object_name = 'notification'
53 53
     active_tab = 'notifications'
54 54
 
@@ -81,7 +81,7 @@ class UpdateView(BulkEditMixin, generic.RedirectView):
81 81
 
82 82
     def get_success_response(self):
83 83
         return redirect_to_referrer(
84
-            self.request, 'customer:notifications-inbox')
84
+            self.request, 'communication:notifications-inbox')
85 85
 
86 86
     def archive(self, request, notifications):
87 87
         for notification in notifications:

+ 173
- 0
src/oscar/apps/communication/utils.py Bestand weergeven

@@ -0,0 +1,173 @@
1
+import logging
2
+
3
+from django.conf import settings
4
+from django.contrib.sites.models import Site
5
+from django.core.mail import EmailMessage, EmailMultiAlternatives
6
+
7
+from oscar.core.loading import get_model
8
+
9
+CommunicationEventType = get_model('communication', 'CommunicationEventType')
10
+Email = get_model('communication', 'Email')
11
+Notification = get_model('communication', 'Notification')
12
+
13
+
14
+class Dispatcher(object):
15
+
16
+    def __init__(self, logger=None, mail_connection=None):
17
+        if not logger:
18
+            logger = logging.getLogger(__name__)
19
+        self.logger = logger
20
+        # Supply a mail_connection if you want the dispatcher to use that
21
+        # instead of opening a new one.
22
+        self.mail_connection = mail_connection
23
+
24
+    # Public API methods
25
+
26
+    def dispatch_direct_messages(self, recipient_email, messages, attachments=None):
27
+        """
28
+        Dispatch one-off messages to explicitly specified recipient email.
29
+        """
30
+        if messages['subject'] and (messages['body'] or messages['html']):
31
+            return self.send_email_messages(recipient_email, messages, attachments=attachments)
32
+
33
+    def dispatch_anonymous_messages(self, email, messages, attachments=None):
34
+        dispatched_messages = {}
35
+        if email:
36
+            dispatched_messages['email'] = self.send_email_messages(email, messages, attachments=attachments), None
37
+        return dispatched_messages
38
+
39
+    def dispatch_user_messages(self, user, messages, attachments=None):
40
+        """
41
+        Send messages to a site user
42
+        """
43
+        dispatched_messages = {}
44
+        if messages['subject'] and (messages['body'] or messages['html']):
45
+            dispatched_messages['email'] = self.send_user_email_messages(user, messages, attachments)
46
+        if messages['sms']:
47
+            dispatched_messages['sms'] = self.send_text_message(user, messages['sms'])
48
+        return dispatched_messages
49
+
50
+    def notify_user(self, user, subject, **kwargs):
51
+        """
52
+        Send a simple notification to a user
53
+        """
54
+        Notification.objects.create(recipient=user, subject=subject, **kwargs)
55
+
56
+    def notify_users(self, users, subject, **kwargs):
57
+        """
58
+        Send a simple notification to an iterable of users
59
+        """
60
+        for user in users:
61
+            self.notify_user(user, subject, **kwargs)
62
+
63
+    # Internal
64
+
65
+    def create_email(self, user, messages, email):
66
+        """
67
+        Create ``Email`` instance in database for logging purposes.
68
+        """
69
+        if email and user.is_authenticated:
70
+            return Email.objects.create(
71
+                user=user,
72
+                email=user.email,
73
+                subject=email.subject,
74
+                body_text=email.body,
75
+                body_html=messages['html'],
76
+            )
77
+
78
+    def send_user_email_messages(self, user, messages, attachments=None):
79
+        """
80
+        Send message to the registered user / customer and collect data in database.
81
+        """
82
+        if not user.email:
83
+            self.logger.warning("Unable to send email messages as user #%d has"
84
+                                " no email address", user.id)
85
+            return None
86
+
87
+        email = self.send_email_messages(user.email, messages, attachments=attachments)
88
+
89
+        if settings.OSCAR_SAVE_SENT_EMAILS_TO_DB:
90
+            self.create_email(user, messages, email)
91
+
92
+        return email
93
+
94
+    def send_email_messages(self, recipient_email, messages, from_email=None, attachments=None):
95
+        """
96
+        Send email to recipient, HTML attachment optional.
97
+        """
98
+        from_email = from_email or settings.OSCAR_FROM_EMAIL
99
+
100
+        content_attachments, file_attachments = self.prepare_attachments(attachments)
101
+
102
+        # Determine whether we are sending a HTML version too
103
+        if messages['html']:
104
+            email = EmailMultiAlternatives(
105
+                messages['subject'],
106
+                messages['body'],
107
+                from_email=from_email,
108
+                to=[recipient_email],
109
+                attachments=content_attachments,
110
+            )
111
+            email.attach_alternative(messages['html'], "text/html")
112
+        else:
113
+            email = EmailMessage(
114
+                messages['subject'],
115
+                messages['body'],
116
+                from_email=from_email,
117
+                to=[recipient_email],
118
+                attachments=content_attachments,
119
+            )
120
+        for attachment in file_attachments:
121
+            email.attach_file(attachment)
122
+
123
+        self.logger.info("Sending email to %s" % recipient_email)
124
+
125
+        if self.mail_connection:
126
+            self.mail_connection.send_messages([email])
127
+        else:
128
+            email.send()
129
+
130
+        return email
131
+
132
+    def send_text_message(self, user, event_type):
133
+        raise NotImplementedError
134
+
135
+    def prepare_attachments(self, attachments):
136
+        """
137
+        Two types of attachments can be attached to emails:
138
+
139
+            * "Content" attachment is one of:
140
+                * instance of ``MIMEBase`` (from ``email.mime.base``);
141
+                * list ``[filename, content, mimetype]``;
142
+
143
+            * "File" attachment is a path to file from an instance of
144
+              ``FileField`` based fields.
145
+
146
+        "Content" and "file" attachments attached to emails differently.
147
+        """
148
+        content_attachments = []
149
+        file_attachments = []
150
+        if attachments is not None:
151
+            for attachment in attachments:
152
+                if isinstance(attachment, str):
153
+                    file_attachments.append(attachment)
154
+                else:
155
+                    content_attachments.append(attachment)
156
+
157
+        return content_attachments, file_attachments
158
+
159
+    def get_base_context(self):
160
+        """
161
+        Return context that is common to all emails
162
+        """
163
+        return {'site': Site.objects.get_current()}
164
+
165
+    def get_messages(self, event_code, extra_context=None):
166
+        """
167
+        Return rendered messages
168
+        """
169
+        context = self.get_base_context()
170
+        if extra_context is not None:
171
+            context.update(extra_context)
172
+        msgs = CommunicationEventType.objects.get_and_render(event_code, context)
173
+        return msgs

+ 0
- 216
src/oscar/apps/customer/abstract_models.py Bestand weergeven

@@ -1,20 +1,12 @@
1
-from django.conf import settings
2 1
 from django.contrib.auth import models as auth_models
3 2
 from django.core.mail import send_mail
4
-from django.core.validators import RegexValidator
5 3
 from django.db import models
6
-from django.template import TemplateDoesNotExist, engines
7
-from django.template.loader import get_template
8 4
 from django.urls import reverse
9 5
 from django.utils import timezone
10 6
 from django.utils.crypto import get_random_string
11 7
 from django.utils.translation import gettext_lazy as _
12 8
 
13 9
 from oscar.core.compat import AUTH_USER_MODEL
14
-from oscar.core.loading import get_class
15
-from oscar.models.fields import AutoSlugField
16
-
17
-CommunicationTypeManager = get_class('customer.managers', 'CommunicationTypeManager')
18 10
 
19 11
 
20 12
 class UserManager(auth_models.BaseUserManager):
@@ -121,213 +113,6 @@ class AbstractUser(auth_models.AbstractBaseUser,
121 113
         self._migrate_alerts_to_user()
122 114
 
123 115
 
124
-class AbstractEmail(models.Model):
125
-    """
126
-    This is a record of all emails sent to a customer.
127
-    Normally, we only record order-related emails.
128
-    """
129
-    user = models.ForeignKey(
130
-        AUTH_USER_MODEL,
131
-        on_delete=models.CASCADE,
132
-        related_name='emails',
133
-        verbose_name=_("User"),
134
-        null=True)
135
-    email = models.EmailField(_('Email Address'), null=True, blank=True)
136
-    subject = models.TextField(_('Subject'), max_length=255)
137
-    body_text = models.TextField(_("Body Text"))
138
-    body_html = models.TextField(_("Body HTML"), blank=True)
139
-    date_sent = models.DateTimeField(_("Date Sent"), auto_now_add=True)
140
-
141
-    class Meta:
142
-        abstract = True
143
-        app_label = 'customer'
144
-        verbose_name = _('Email')
145
-        verbose_name_plural = _('Emails')
146
-
147
-    def __str__(self):
148
-        if self.user:
149
-            return _("Email to %(user)s with subject '%(subject)s'") % {
150
-                'user': self.user.get_username(), 'subject': self.subject}
151
-        else:
152
-            return _("Anonymous email to %(email)s with subject '%(subject)s'") % {
153
-                'email': self.email, 'subject': self.subject}
154
-
155
-
156
-class AbstractCommunicationEventType(models.Model):
157
-    """
158
-    A 'type' of communication.  Like an order confirmation email.
159
-    """
160
-
161
-    #: Code used for looking up this event programmatically.
162
-    # e.g. PASSWORD_RESET. AutoSlugField uppercases the code for us because
163
-    # it's a useful convention that's been enforced in previous Oscar versions
164
-    code = AutoSlugField(
165
-        _('Code'), max_length=128, unique=True, populate_from='name',
166
-        separator="_", uppercase=True, editable=True,
167
-        validators=[
168
-            RegexValidator(
169
-                regex=r'^[a-zA-Z_][0-9a-zA-Z_]*$',
170
-                message=_(
171
-                    "Code can only contain the letters a-z, A-Z, digits, "
172
-                    "and underscores, and can't start with a digit."))],
173
-        help_text=_("Code used for looking up this event programmatically"))
174
-
175
-    #: Name is the friendly description of an event for use in the admin
176
-    name = models.CharField(
177
-        _('Name'), max_length=255,
178
-        help_text=_("This is just used for organisational purposes"))
179
-
180
-    # We allow communication types to be categorised
181
-    # For backwards-compatibility, the choice values are quite verbose
182
-    ORDER_RELATED = 'Order related'
183
-    USER_RELATED = 'User related'
184
-    CATEGORY_CHOICES = (
185
-        (ORDER_RELATED, _('Order related')),
186
-        (USER_RELATED, _('User related'))
187
-    )
188
-
189
-    category = models.CharField(
190
-        _('Category'), max_length=255, default=ORDER_RELATED,
191
-        choices=CATEGORY_CHOICES)
192
-
193
-    # Template content for emails
194
-    # NOTE: There's an intentional distinction between None and ''. None
195
-    # instructs Oscar to look for a file-based template, '' is just an empty
196
-    # template.
197
-    email_subject_template = models.CharField(
198
-        _('Email Subject Template'), max_length=255, blank=True, null=True)
199
-    email_body_template = models.TextField(
200
-        _('Email Body Template'), blank=True, null=True)
201
-    email_body_html_template = models.TextField(
202
-        _('Email Body HTML Template'), blank=True, null=True,
203
-        help_text=_("HTML template"))
204
-
205
-    # Template content for SMS messages
206
-    sms_template = models.CharField(_('SMS Template'), max_length=170,
207
-                                    blank=True, null=True,
208
-                                    help_text=_("SMS template"))
209
-
210
-    date_created = models.DateTimeField(_("Date Created"), auto_now_add=True)
211
-    date_updated = models.DateTimeField(_("Date Updated"), auto_now=True)
212
-
213
-    objects = CommunicationTypeManager()
214
-
215
-    # File templates
216
-    email_subject_template_file = 'oscar/customer/emails/commtype_%s_subject.txt'
217
-    email_body_template_file = 'oscar/customer/emails/commtype_%s_body.txt'
218
-    email_body_html_template_file = 'oscar/customer/emails/commtype_%s_body.html'
219
-    sms_template_file = 'oscar/customer/sms/commtype_%s_body.txt'
220
-
221
-    class Meta:
222
-        abstract = True
223
-        app_label = 'customer'
224
-        verbose_name = _("Communication event type")
225
-        verbose_name_plural = _("Communication event types")
226
-
227
-    def get_messages(self, ctx=None):
228
-        """
229
-        Return a dict of templates with the context merged in
230
-
231
-        We look first at the field templates but fail over to
232
-        a set of file templates that follow a conventional path.
233
-        """
234
-        code = self.code.lower()
235
-
236
-        # Build a dict of message name to Template instances
237
-        templates = {'subject': 'email_subject_template',
238
-                     'body': 'email_body_template',
239
-                     'html': 'email_body_html_template',
240
-                     'sms': 'sms_template'}
241
-        for name, attr_name in templates.items():
242
-            field = getattr(self, attr_name, None)
243
-            if field is not None:
244
-                # Template content is in a model field
245
-                templates[name] = engines['django'].from_string(field)
246
-            else:
247
-                # Model field is empty - look for a file template
248
-                template_name = getattr(self, "%s_file" % attr_name) % code
249
-                try:
250
-                    templates[name] = get_template(template_name)
251
-                except TemplateDoesNotExist:
252
-                    templates[name] = None
253
-
254
-        # Pass base URL for serving images within HTML emails
255
-        if ctx is None:
256
-            ctx = {}
257
-        ctx['static_base_url'] = getattr(
258
-            settings, 'OSCAR_STATIC_BASE_URL', None)
259
-
260
-        messages = {}
261
-        for name, template in templates.items():
262
-            messages[name] = template.render(ctx) if template else ''
263
-
264
-        # Ensure the email subject doesn't contain any newlines
265
-        messages['subject'] = messages['subject'].replace("\n", "")
266
-        messages['subject'] = messages['subject'].replace("\r", "")
267
-
268
-        return messages
269
-
270
-    def __str__(self):
271
-        return self.name
272
-
273
-    def is_order_related(self):
274
-        return self.category == self.ORDER_RELATED
275
-
276
-    def is_user_related(self):
277
-        return self.category == self.USER_RELATED
278
-
279
-
280
-class AbstractNotification(models.Model):
281
-    recipient = models.ForeignKey(
282
-        AUTH_USER_MODEL,
283
-        db_index=True,
284
-        on_delete=models.CASCADE,
285
-        related_name='notifications')
286
-
287
-    # Not all notifications will have a sender.
288
-    sender = models.ForeignKey(
289
-        AUTH_USER_MODEL,
290
-        on_delete=models.CASCADE,
291
-        null=True)
292
-
293
-    # HTML is allowed in this field as it can contain links
294
-    subject = models.CharField(max_length=255)
295
-    body = models.TextField()
296
-
297
-    # Some projects may want to categorise their notifications.  You may want
298
-    # to use this field to show a different icons next to the notification.
299
-    category = models.CharField(max_length=255, blank=True)
300
-
301
-    INBOX, ARCHIVE = 'Inbox', 'Archive'
302
-    choices = (
303
-        (INBOX, _('Inbox')),
304
-        (ARCHIVE, _('Archive')))
305
-    location = models.CharField(max_length=32, choices=choices,
306
-                                default=INBOX)
307
-
308
-    date_sent = models.DateTimeField(auto_now_add=True, db_index=True)
309
-    date_read = models.DateTimeField(blank=True, null=True)
310
-
311
-    class Meta:
312
-        abstract = True
313
-        app_label = 'customer'
314
-        ordering = ('-date_sent',)
315
-        verbose_name = _('Notification')
316
-        verbose_name_plural = _('Notifications')
317
-
318
-    def __str__(self):
319
-        return self.subject
320
-
321
-    def archive(self):
322
-        self.location = self.ARCHIVE
323
-        self.save()
324
-    archive.alters_data = True
325
-
326
-    @property
327
-    def is_read(self):
328
-        return self.date_read is not None
329
-
330
-
331 116
 class AbstractProductAlert(models.Model):
332 117
     """
333 118
     An alert for when a product comes back in stock
@@ -342,7 +127,6 @@ class AbstractProductAlert(models.Model):
342 127
     user = models.ForeignKey(
343 128
         AUTH_USER_MODEL,
344 129
         blank=True,
345
-        db_index=True,
346 130
         null=True,
347 131
         on_delete=models.CASCADE,
348 132
         related_name="alerts",

+ 2
- 2
src/oscar/apps/customer/alerts/receivers.py Bestand weergeven

@@ -7,8 +7,8 @@ from oscar.core.loading import get_model
7 7
 def send_product_alerts(sender, instance, created, **kwargs):
8 8
     if kwargs.get('raw', False):
9 9
         return
10
-    from oscar.apps.customer.alerts import utils
11
-    utils.send_product_alerts(instance.product)
10
+    from oscar.apps.customer.alerts.utils import AlertsDispatcher
11
+    AlertsDispatcher().send_product_alert_email_for_user(instance.product)
12 12
 
13 13
 
14 14
 if settings.OSCAR_EAGER_ALERTS:

+ 140
- 111
src/oscar/apps/customer/alerts/utils.py Bestand weergeven

@@ -1,134 +1,163 @@
1 1
 import logging
2
+import warnings
2 3
 
3
-from django.contrib.sites.models import Site
4
-from django.core import mail
5 4
 from django.db.models import Max
6 5
 from django.template import loader
7 6
 
8
-from oscar.apps.customer.notifications import services
9 7
 from oscar.core.loading import get_class, get_model
8
+from oscar.utils.deprecation import RemovedInOscar22Warning
10 9
 
11
-CommunicationEventType = get_model('customer', 'CommunicationEventType')
12 10
 ProductAlert = get_model('customer', 'ProductAlert')
13 11
 Product = get_model('catalogue', 'Product')
14
-Dispatcher = get_class('customer.utils', 'Dispatcher')
12
+Dispatcher = get_class('communication.utils', 'Dispatcher')
15 13
 Selector = get_class('partner.strategy', 'Selector')
16 14
 
17
-logger = logging.getLogger('oscar.alerts')
15
+alerts_logger = logging.getLogger('oscar.alerts')
18 16
 
19 17
 
20
-def send_alerts():
18
+class AlertsDispatcher:
21 19
     """
22
-    Send out product alerts
20
+    Dispatcher to send concrete product alerts related emails
21
+    and notifications.
23 22
     """
24
-    products = Product.objects.filter(
25
-        productalert__status=ProductAlert.ACTIVE
26
-    ).distinct()
27
-    logger.info("Found %d products with active alerts", products.count())
28
-    for product in products:
29
-        send_product_alerts(product)
30 23
 
24
+    # Event codes
25
+    PRODUCT_ALERT_EVENT_CODE = 'PRODUCT_ALERT'
26
+    PRODUCT_ALERT_CONFIRMATION_EVENT_CODE = 'PRODUCT_ALERT_CONFIRMATION'
27
+
28
+    def __init__(self, logger=None, mail_connection=None):
29
+        self.dispatcher = Dispatcher(
30
+            logger=logger or alerts_logger,
31
+            mail_connection=mail_connection,
32
+        )
33
+
34
+    def send_alerts(self):
35
+        """
36
+        Check all products with active product alerts for
37
+        availability and send out email alerts when a product is
38
+        available to buy.
39
+        """
40
+        products = Product.browsable.filter(productalert__status=ProductAlert.ACTIVE).distinct()
41
+        self.dispatcher.logger.info("Found %d products with active alerts", products.count())
42
+        for product in products:
43
+            self.send_product_alert_email_for_user(product)
44
+
45
+    def send_product_alert_email_for_user(self, product):  # noqa: C901 too complex
46
+        """
47
+        Check for notifications for this product and send email to users
48
+        if the product is back in stock. Add a little 'hurry' note if the
49
+        amount of in-stock items is less then the number of notifications.
50
+        """
51
+        stockrecords = product.stockrecords.all()
52
+        num_stockrecords = len(stockrecords)
53
+        if not num_stockrecords:
54
+            return
55
+
56
+        self.dispatcher.logger.info("Sending alerts for '%s'", product)
57
+        alerts = ProductAlert.objects.filter(
58
+            product_id__in=(product.id, product.parent_id),
59
+            status=ProductAlert.ACTIVE,
60
+        )
61
+
62
+        # Determine 'hurry mode'
63
+        if num_stockrecords == 1:
64
+            num_in_stock = stockrecords[0].num_in_stock
65
+        else:
66
+            result = stockrecords.aggregate(max_in_stock=Max('num_in_stock'))
67
+            num_in_stock = result['max_in_stock']
68
+
69
+        # 'hurry_mode' is false if 'num_in_stock' is None
70
+        hurry_mode = num_in_stock is not None and alerts.count() > num_in_stock
71
+
72
+        messages_to_send = []
73
+        user_messages_to_send = []
74
+        num_notifications = 0
75
+        selector = Selector()
76
+        for alert in alerts:
77
+            # Check if the product is available to this user
78
+            strategy = selector.strategy(user=alert.user)
79
+            data = strategy.fetch_for_product(product)
80
+            if not data.availability.is_available_to_buy:
81
+                continue
82
+
83
+            extra_context = {
84
+                'alert': alert,
85
+                'hurry': hurry_mode,
86
+            }
87
+            if alert.user:
88
+                # Send a site notification
89
+                num_notifications += 1
90
+                self.notify_user_about_product_alert(alert.user, extra_context)
91
+
92
+            messages = self.dispatcher.get_messages(self.PRODUCT_ALERT_EVENT_CODE, extra_context)
93
+
94
+            if messages and messages['body']:
95
+                if alert.user:
96
+                    user_messages_to_send.append((alert.user, messages))
97
+                else:
98
+                    messages_to_send.append((alert.get_email_address(), messages))
99
+            alert.close()
100
+
101
+        if messages_to_send or user_messages_to_send:
102
+            for message in messages_to_send:
103
+                self.dispatcher.dispatch_direct_messages(*message)
104
+            for message in user_messages_to_send:
105
+                self.dispatcher.dispatch_user_messages(*message)
106
+
107
+        self.dispatcher.logger.info(
108
+            "Sent %d notifications and %d messages",
109
+            num_notifications, len(messages_to_send) + len(user_messages_to_send)
110
+        )
111
+
112
+    def send_product_alert_confirmation_email_for_user(self, alert, extra_context=None):
113
+        """
114
+        Send an alert confirmation email.
115
+        """
116
+        if extra_context is None:
117
+            extra_context = {'alert': alert}
118
+        messages = self.dispatcher.get_messages(self.PRODUCT_ALERT_CONFIRMATION_EVENT_CODE, extra_context)
119
+        self.dispatcher.dispatch_direct_messages(alert.email, messages)
120
+
121
+    def notify_user_about_product_alert(self, user, context):
122
+        subj_tpl = loader.get_template('oscar/customer/alerts/message_subject.html')
123
+        message_tpl = loader.get_template('oscar/customer/alerts/message.html')
124
+        self.dispatcher.notify_user(
125
+            user,
126
+            subj_tpl.render(context).strip(),
127
+            body=message_tpl.render(context).strip()
128
+        )
31 129
 
32
-def send_alert_confirmation(alert):
33
-    """
34
-    Send an alert confirmation email.
35
-    """
36
-    ctx = {
37
-        'alert': alert,
38
-        'site': Site.objects.get_current(),
39
-    }
40 130
 
41
-    code = 'PRODUCT_ALERT_CONFIRMATION'
42
-    messages = CommunicationEventType.objects.get_and_render(code, ctx)
131
+def send_alerts():
132
+    warnings.warn(
133
+        'Use of `send_alerts` is deprecated. Please use `send_alerts` '
134
+        'method of `AlertsDispatcher`.',
135
+        RemovedInOscar22Warning,
136
+        stacklevel=2,
137
+    )
43 138
 
44
-    if messages and messages['body']:
45
-        Dispatcher().dispatch_direct_messages(alert.email, messages)
139
+    AlertsDispatcher().send_alerts()
46 140
 
47 141
 
48
-def send_product_alerts(product):   # noqa C901 too complex
49
-    """
50
-    Check for notifications for this product and send email to users
51
-    if the product is back in stock. Add a little 'hurry' note if the
52
-    amount of in-stock items is less then the number of notifications.
53
-    """
54
-    stockrecords = product.stockrecords.all()
55
-    num_stockrecords = len(stockrecords)
56
-    if not num_stockrecords:
57
-        return
58
-
59
-    logger.info("Sending alerts for '%s'", product)
60
-    alerts = ProductAlert.objects.filter(
61
-        product_id__in=(product.id, product.parent_id),
62
-        status=ProductAlert.ACTIVE,
142
+def send_alert_confirmation(alert):
143
+    warnings.warn(
144
+        'Use of `send_alert_confirmation` is deprecated. Please use '
145
+        '`send_product_alert_confirmation_email_for_user` method of '
146
+        '`AlertsDispatcher`.',
147
+        RemovedInOscar22Warning,
148
+        stacklevel=2,
63 149
     )
64 150
 
65
-    # Determine 'hurry mode'
66
-    if num_stockrecords == 1:
67
-        num_in_stock = stockrecords[0].num_in_stock
68
-    else:
69
-        result = stockrecords.aggregate(max_in_stock=Max('num_in_stock'))
70
-        num_in_stock = result['max_in_stock']
71
-
72
-    # hurry_mode is false if num_in_stock is None
73
-    hurry_mode = num_in_stock is not None and alerts.count() > num_in_stock
74
-
75
-    code = 'PRODUCT_ALERT'
76
-    try:
77
-        event_type = CommunicationEventType.objects.get(code=code)
78
-    except CommunicationEventType.DoesNotExist:
79
-        event_type = CommunicationEventType.objects.model(code=code)
80
-
81
-    messages_to_send = []
82
-    user_messages_to_send = []
83
-    num_notifications = 0
84
-    selector = Selector()
85
-    for alert in alerts:
86
-        # Check if the product is available to this user
87
-        strategy = selector.strategy(user=alert.user)
88
-        data = strategy.fetch_for_product(product)
89
-        if not data.availability.is_available_to_buy:
90
-            continue
91
-
92
-        ctx = {
93
-            'alert': alert,
94
-            'site': Site.objects.get_current(),
95
-            'hurry': hurry_mode,
96
-        }
97
-        if alert.user:
98
-            # Send a site notification
99
-            num_notifications += 1
100
-            subj_tpl = loader.get_template('oscar/customer/alerts/message_subject.html')
101
-            message_tpl = loader.get_template('oscar/customer/alerts/message.html')
102
-            services.notify_user(
103
-                alert.user,
104
-                subj_tpl.render(ctx).strip(),
105
-                body=message_tpl.render(ctx).strip()
106
-            )
107
-
108
-        # Build message and add to list
109
-        messages = event_type.get_messages(ctx)
110
-
111
-        if messages and messages['body']:
112
-            if alert.user:
113
-                user_messages_to_send.append(
114
-                    (alert.user, messages)
115
-                )
116
-            else:
117
-                messages_to_send.append(
118
-                    (alert.get_email_address(), messages)
119
-                )
120
-        alert.close()
121
-
122
-    # Send all messages using one SMTP connection to avoid opening lots of them
123
-    if messages_to_send or user_messages_to_send:
124
-        connection = mail.get_connection()
125
-        connection.open()
126
-        disp = Dispatcher(mail_connection=connection)
127
-        for message in messages_to_send:
128
-            disp.dispatch_direct_messages(*message)
129
-        for message in user_messages_to_send:
130
-            disp.dispatch_user_messages(*message)
131
-        connection.close()
132
-
133
-    logger.info("Sent %d notifications and %d messages", num_notifications,
134
-                len(messages_to_send) + len(user_messages_to_send))
151
+    AlertsDispatcher().send_product_alert_confirmation_email_for_user(alert)
152
+
153
+
154
+def send_product_alerts(product):
155
+    warnings.warn(
156
+        'Use of `send_product_alerts` is deprecated. '
157
+        'Please use `send_product_alert_email_for_user` '
158
+        'method of `AlertsDispatcher`.',
159
+        RemovedInOscar22Warning,
160
+        stacklevel=2,
161
+    )
162
+
163
+    AlertsDispatcher().send_product_alert_email_for_user(product)

+ 2
- 2
src/oscar/apps/customer/alerts/views.py Bestand weergeven

@@ -5,7 +5,7 @@ from django.shortcuts import get_object_or_404
5 5
 from django.utils.translation import gettext_lazy as _
6 6
 from django.views import generic
7 7
 
8
-from oscar.apps.customer.alerts import utils
8
+from oscar.apps.customer.alerts.utils import AlertsDispatcher
9 9
 from oscar.core.loading import get_class, get_model
10 10
 
11 11
 Product = get_model('catalogue', 'Product')
@@ -60,7 +60,7 @@ class ProductAlertCreateView(generic.CreateView):
60 60
     def form_valid(self, form):
61 61
         response = super().form_valid(form)
62 62
         if self.object.is_anonymous:
63
-            utils.send_alert_confirmation(self.object)
63
+            AlertsDispatcher().send_product_alert_confirmation_email_for_user(self.object)
64 64
         return response
65 65
 
66 66
     def get_success_url(self):

+ 4
- 4
src/oscar/apps/customer/apps.py Bestand weergeven

@@ -42,13 +42,13 @@ class CustomerConfig(OscarConfig):
42 42
         self.profile_delete_view = get_class('customer.views', 'ProfileDeleteView')
43 43
         self.change_password_view = get_class('customer.views', 'ChangePasswordView')
44 44
 
45
-        self.notification_inbox_view = get_class('customer.notifications.views',
45
+        self.notification_inbox_view = get_class('communication.notifications.views',
46 46
                                                  'InboxView')
47
-        self.notification_archive_view = get_class('customer.notifications.views',
47
+        self.notification_archive_view = get_class('communication.notifications.views',
48 48
                                                    'ArchiveView')
49
-        self.notification_update_view = get_class('customer.notifications.views',
49
+        self.notification_update_view = get_class('communication.notifications.views',
50 50
                                                   'UpdateView')
51
-        self.notification_detail_view = get_class('customer.notifications.views',
51
+        self.notification_detail_view = get_class('communication.notifications.views',
52 52
                                                   'DetailView')
53 53
 
54 54
         self.alert_list_view = get_class('customer.alerts.views',

+ 11
- 23
src/oscar/apps/customer/forms.py Bestand weergeven

@@ -17,8 +17,7 @@ from oscar.core.compat import existing_user_fields, get_user_model
17 17
 from oscar.core.loading import get_class, get_model, get_profile_class
18 18
 from oscar.forms import widgets
19 19
 
20
-Dispatcher = get_class('customer.utils', 'Dispatcher')
21
-CommunicationEventType = get_model('customer', 'communicationeventtype')
20
+CustomerDispatcher = get_class('customer.utils', 'CustomerDispatcher')
22 21
 ProductAlert = get_model('customer', 'ProductAlert')
23 22
 User = get_user_model()
24 23
 
@@ -38,10 +37,8 @@ class PasswordResetForm(auth_forms.PasswordResetForm):
38 37
     """
39 38
     This form takes the same structure as its parent from :py:mod:`django.contrib.auth`
40 39
     """
41
-    communication_type_code = "PASSWORD_RESET"
42 40
 
43
-    def save(self, domain_override=None, use_https=False, request=None,
44
-             **kwargs):
41
+    def save(self, domain_override=None, request=None, **kwargs):
45 42
         """
46 43
         Generates a one-use only link for resetting password and sends to the
47 44
         user.
@@ -53,24 +50,15 @@ class PasswordResetForm(auth_forms.PasswordResetForm):
53 50
         active_users = User._default_manager.filter(
54 51
             email__iexact=email, is_active=True)
55 52
         for user in active_users:
56
-            reset_url = self.get_reset_url(site, request, user, use_https)
57
-            ctx = {
58
-                'user': user,
59
-                'site': site,
60
-                'reset_url': reset_url}
61
-            messages = CommunicationEventType.objects.get_and_render(
62
-                code=self.communication_type_code, context=ctx)
63
-            Dispatcher().dispatch_user_messages(user, messages)
64
-
65
-    def get_reset_url(self, site, request, user, use_https):
66
-        # the request argument isn't used currently, but implementors might
67
-        # need it to determine the correct subdomain
68
-        reset_url = "%s://%s%s" % (
69
-            'https' if use_https else 'http',
70
-            site.domain,
71
-            get_password_reset_url(user))
72
-
73
-        return reset_url
53
+            self.send_password_reset_email(site, user)
54
+
55
+    def send_password_reset_email(self, site, user):
56
+        extra_context = {
57
+            'user': user,
58
+            'site': site,
59
+            'reset_url': get_password_reset_url(user),
60
+        }
61
+        CustomerDispatcher().send_password_reset_email_for_user(user, extra_context)
74 62
 
75 63
 
76 64
 class EmailAuthenticationForm(AuthenticationForm):

+ 40
- 0
src/oscar/apps/customer/migrations/0006_auto_20190430_1736.py Bestand weergeven

@@ -0,0 +1,40 @@
1
+# Generated by Django 2.0.13 on 2019-04-30 16:36
2
+
3
+from django.db import migrations
4
+
5
+
6
+class Migration(migrations.Migration):
7
+
8
+    dependencies = [
9
+        ('customer', '0005_auto_20181115_1953'),
10
+    ]
11
+
12
+    state_operations = [
13
+        migrations.DeleteModel(
14
+            name='CommunicationEventType',
15
+        ),
16
+        migrations.RemoveField(
17
+            model_name='email',
18
+            name='user',
19
+        ),
20
+        migrations.RemoveField(
21
+            model_name='notification',
22
+            name='recipient',
23
+        ),
24
+        migrations.RemoveField(
25
+            model_name='notification',
26
+            name='sender',
27
+        ),
28
+        migrations.DeleteModel(
29
+            name='Email',
30
+        ),
31
+        migrations.DeleteModel(
32
+            name='Notification',
33
+        ),
34
+    ]
35
+
36
+    operations = [
37
+        migrations.SeparateDatabaseAndState(
38
+            state_operations=state_operations
39
+        ),
40
+    ]

+ 4
- 11
src/oscar/apps/customer/mixins.py Bestand weergeven

@@ -3,15 +3,14 @@ import logging
3 3
 from django.conf import settings
4 4
 from django.contrib.auth import authenticate
5 5
 from django.contrib.auth import login as auth_login
6
-from django.contrib.sites.shortcuts import get_current_site
7 6
 
8 7
 from oscar.apps.customer.signals import user_registered
9 8
 from oscar.core.compat import get_user_model
10 9
 from oscar.core.loading import get_class, get_model
11 10
 
12 11
 User = get_user_model()
13
-CommunicationEventType = get_model('customer', 'CommunicationEventType')
14
-Dispatcher = get_class('customer.utils', 'Dispatcher')
12
+CommunicationEventType = get_model('communication', 'CommunicationEventType')
13
+CustomerDispatcher = get_class('customer.utils', 'CustomerDispatcher')
15 14
 
16 15
 logger = logging.getLogger('oscar.customer')
17 16
 
@@ -38,7 +37,6 @@ class PageTitleMixin(object):
38 37
 
39 38
 
40 39
 class RegisterUserMixin(object):
41
-    communication_type_code = 'REGISTRATION'
42 40
 
43 41
     def register_user(self, form):
44 42
         """
@@ -84,10 +82,5 @@ class RegisterUserMixin(object):
84 82
         return user
85 83
 
86 84
     def send_registration_email(self, user):
87
-        code = self.communication_type_code
88
-        ctx = {'user': user,
89
-               'site': get_current_site(self.request)}
90
-        messages = CommunicationEventType.objects.get_and_render(
91
-            code, ctx)
92
-        if messages and messages['body']:
93
-            Dispatcher().dispatch_user_messages(user, messages)
85
+        extra_context = {'user': user}
86
+        CustomerDispatcher().send_registration_email_for_user(user, extra_context)

+ 0
- 22
src/oscar/apps/customer/models.py Bestand weergeven

@@ -4,28 +4,6 @@ from oscar.core.loading import is_model_registered
4 4
 __all__ = []
5 5
 
6 6
 
7
-if not is_model_registered('customer', 'Email'):
8
-    class Email(abstract_models.AbstractEmail):
9
-        pass
10
-
11
-    __all__.append('Email')
12
-
13
-
14
-if not is_model_registered('customer', 'CommunicationEventType'):
15
-    class CommunicationEventType(
16
-            abstract_models.AbstractCommunicationEventType):
17
-        pass
18
-
19
-    __all__.append('CommunicationEventType')
20
-
21
-
22
-if not is_model_registered('customer', 'Notification'):
23
-    class Notification(abstract_models.AbstractNotification):
24
-        pass
25
-
26
-    __all__.append('Notification')
27
-
28
-
29 7
 if not is_model_registered('customer', 'ProductAlert'):
30 8
     class ProductAlert(abstract_models.AbstractProductAlert):
31 9
         pass

+ 19
- 11
src/oscar/apps/customer/notifications/services.py Bestand weergeven

@@ -1,18 +1,26 @@
1
-from oscar.core.loading import get_model
1
+import warnings
2 2
 
3
-Notification = get_model('customer', 'Notification')
3
+from oscar.core.loading import get_class
4
+from oscar.utils.deprecation import RemovedInOscar22Warning
5
+
6
+Dispatcher = get_class('communication.utils', 'Dispatcher')
4 7
 
5 8
 
6 9
 def notify_user(user, subject, **kwargs):
7
-    """
8
-    Send a simple notification to a user
9
-    """
10
-    Notification.objects.create(recipient=user, subject=subject, **kwargs)
10
+    warnings.warn(
11
+        'Use of `notify_user` is deprecated. Please use `notify_user` '
12
+        'method of `communications.utils.Dispatcher`.',
13
+        RemovedInOscar22Warning,
14
+        stacklevel=2,
15
+    )
16
+    Dispatcher().notify_user(user, subject, **kwargs)
11 17
 
12 18
 
13 19
 def notify_users(users, subject, **kwargs):
14
-    """
15
-    Send a simple notification to an iterable of users
16
-    """
17
-    for user in users:
18
-        notify_user(user, subject, **kwargs)
20
+    warnings.warn(
21
+        'Use of `notify_users` is deprecated. Please use `notify_users` '
22
+        'method of `communications.utils.Dispatcher`.',
23
+        RemovedInOscar22Warning,
24
+        stacklevel=2,
25
+    )
26
+    Dispatcher().notify_users(users, subject, **kwargs)

+ 27
- 113
src/oscar/apps/customer/utils.py Bestand weergeven

@@ -1,130 +1,44 @@
1
-import logging
2
-
3
-from django.conf import settings
4 1
 from django.contrib.auth.tokens import default_token_generator
5
-from django.core.mail import EmailMessage, EmailMultiAlternatives
6 2
 from django.urls import reverse
7 3
 from django.utils.encoding import force_bytes
8 4
 
9 5
 from oscar.core.compat import urlsafe_base64_encode
10
-from oscar.core.loading import get_model
11
-
12
-CommunicationEvent = get_model('order', 'CommunicationEvent')
13
-Email = get_model('customer', 'Email')
14
-
15
-
16
-class Dispatcher(object):
17
-    def __init__(self, logger=None, mail_connection=None):
18
-        if not logger:
19
-            logger = logging.getLogger(__name__)
20
-        self.logger = logger
21
-        # Supply a mail_connection if you want the dispatcher to use that
22
-        # instead of opening a new one.
23
-        self.mail_connection = mail_connection
24
-
25
-    # Public API methods
26
-
27
-    def dispatch_direct_messages(self, recipient, messages):
28
-        """
29
-        Dispatch one-off messages to explicitly specified recipient.
30
-        """
31
-        if messages['subject'] and (messages['body'] or messages['html']):
32
-            return self.send_email_messages(recipient, messages)
33
-
34
-    def dispatch_order_messages(self, order, messages, event_type=None, **kwargs):
35
-        """
36
-        Dispatch order-related messages to the customer.
37
-        """
38
-        if order.is_anonymous:
39
-            email = kwargs.get('email_address', order.guest_email)
40
-            dispatched_messages = self.dispatch_anonymous_messages(email, messages)
41
-        else:
42
-            dispatched_messages = self.dispatch_user_messages(order.user, messages)
6
+from oscar.core.loading import get_class
43 7
 
44
-        self.create_communication_event(order, event_type, dispatched_messages)
8
+Dispatcher = get_class('communication.utils', 'Dispatcher')
9
+Selector = get_class('partner.strategy', 'Selector')
45 10
 
46
-    def dispatch_anonymous_messages(self, email, messages):
47
-        dispatched_messages = {}
48
-        if email:
49
-            dispatched_messages['email'] = self.send_email_messages(email, messages), None
50
-        return dispatched_messages
51 11
 
52
-    def dispatch_user_messages(self, user, messages):
53
-        """
54
-        Send messages to a site user
55
-        """
56
-        dispatched_messages = {}
57
-        if messages['subject'] and (messages['body'] or messages['html']):
58
-            dispatched_messages['email'] = self.send_user_email_messages(user, messages)
59
-        if messages['sms']:
60
-            dispatched_messages['sms'] = self.send_text_message(user, messages['sms'])
61
-        return dispatched_messages
62
-
63
-    # Internal
64
-
65
-    def create_communication_event(self, order, event_type, dispatched_messages):
66
-        """
67
-        Create order communications event for audit
68
-        """
69
-        if dispatched_messages and event_type is not None:
70
-            CommunicationEvent._default_manager.create(order=order, event_type=event_type)
71
-
72
-    def create_customer_email(self, user, messages, email):
73
-        """
74
-        Create Email instance in database for logging purposes.
75
-        """
76
-        # Is user is signed in, record the event for audit
77
-        if email and user.is_authenticated:
78
-            return Email._default_manager.create(user=user,
79
-                                                 email=user.email,
80
-                                                 subject=email.subject,
81
-                                                 body_text=email.body,
82
-                                                 body_html=messages['html'])
83
-
84
-    def send_user_email_messages(self, user, messages):
85
-        """
86
-        Send message to the registered user / customer and collect data in database.
87
-        """
88
-        if not user.email:
89
-            self.logger.warning("Unable to send email messages as user #%d has"
90
-                                " no email address", user.id)
91
-            return None, None
12
+class CustomerDispatcher:
13
+    """
14
+    Dispatcher to send concrete customer related emails.
15
+    """
92 16
 
93
-        email = self.send_email_messages(user.email, messages)
94
-        return email, self.create_customer_email(user, messages, email)
17
+    # Event codes
18
+    REGISTRATION_EVENT_CODE = 'REGISTRATION'
19
+    PASSWORD_RESET_EVENT_CODE = 'PASSWORD_RESET'
20
+    PASSWORD_CHANGED_EVENT_CODE = 'PASSWORD_CHANGED'
21
+    EMAIL_CHANGED_EVENT_CODE = 'EMAIL_CHANGED'
95 22
 
96
-    def send_email_messages(self, recipient, messages):
97
-        """
98
-        Send email to recipient, HTML attachment optional.
99
-        """
100
-        if hasattr(settings, 'OSCAR_FROM_EMAIL'):
101
-            from_email = settings.OSCAR_FROM_EMAIL
102
-        else:
103
-            from_email = None
23
+    def __init__(self, logger=None, mail_connection=None):
24
+        self.dispatcher = Dispatcher(logger=logger, mail_connection=mail_connection)
104 25
 
105
-        # Determine whether we are sending a HTML version too
106
-        if messages['html']:
107
-            email = EmailMultiAlternatives(messages['subject'],
108
-                                           messages['body'],
109
-                                           from_email=from_email,
110
-                                           to=[recipient])
111
-            email.attach_alternative(messages['html'], "text/html")
112
-        else:
113
-            email = EmailMessage(messages['subject'],
114
-                                 messages['body'],
115
-                                 from_email=from_email,
116
-                                 to=[recipient])
117
-        self.logger.info("Sending email to %s" % recipient)
26
+    def send_registration_email_for_user(self, user, extra_context):
27
+        messages = self.dispatcher.get_messages(self.REGISTRATION_EVENT_CODE, extra_context)
28
+        self.dispatcher.dispatch_user_messages(user, messages)
118 29
 
119
-        if self.mail_connection:
120
-            self.mail_connection.send_messages([email])
121
-        else:
122
-            email.send()
30
+    def send_password_reset_email_for_user(self, user, extra_context):
31
+        messages = self.dispatcher.get_messages(self.PASSWORD_RESET_EVENT_CODE, extra_context)
32
+        self.dispatcher.dispatch_user_messages(user, messages)
123 33
 
124
-        return email
34
+    def send_password_changed_email_for_user(self, user, extra_context):
35
+        messages = self.dispatcher.get_messages(self.PASSWORD_CHANGED_EVENT_CODE, extra_context)
36
+        self.dispatcher.dispatch_user_messages(user, messages)
125 37
 
126
-    def send_text_message(self, user, event_type):
127
-        raise NotImplementedError
38
+    def send_email_changed_email_for_user(self, user, extra_context):
39
+        messages = self.dispatcher.get_messages(
40
+            self.EMAIL_CHANGED_EVENT_CODE, extra_context)
41
+        self.dispatcher.dispatch_user_messages(user, messages)
128 42
 
129 43
 
130 44
 def get_password_reset_url(user, token_generator=default_token_generator):

+ 23
- 27
src/oscar/apps/customer/views.py Bestand weergeven

@@ -5,7 +5,6 @@ from django.contrib.auth import login as auth_login
5 5
 from django.contrib.auth import logout as auth_logout
6 6
 from django.contrib.auth import update_session_auth_hash
7 7
 from django.contrib.auth.forms import PasswordChangeForm
8
-from django.contrib.sites.shortcuts import get_current_site
9 8
 from django.core.exceptions import ObjectDoesNotExist
10 9
 from django.shortcuts import get_object_or_404, redirect
11 10
 from django.urls import reverse, reverse_lazy
@@ -23,7 +22,7 @@ from . import signals
23 22
 
24 23
 PageTitleMixin, RegisterUserMixin = get_classes(
25 24
     'customer.mixins', ['PageTitleMixin', 'RegisterUserMixin'])
26
-Dispatcher = get_class('customer.utils', 'Dispatcher')
25
+CustomerDispatcher = get_class('customer.utils', 'CustomerDispatcher')
27 26
 EmailAuthenticationForm, EmailUserCreationForm, OrderSearchForm = get_classes(
28 27
     'customer.forms', ['EmailAuthenticationForm', 'EmailUserCreationForm',
29 28
                        'OrderSearchForm'])
@@ -31,11 +30,8 @@ ProfileForm, ConfirmPasswordForm = get_classes(
31 30
     'customer.forms', ['ProfileForm', 'ConfirmPasswordForm'])
32 31
 UserAddressForm = get_class('address.forms', 'UserAddressForm')
33 32
 Order = get_model('order', 'Order')
34
-Line = get_model('basket', 'Line')
35
-Basket = get_model('basket', 'Basket')
36 33
 UserAddress = get_model('address', 'UserAddress')
37
-Email = get_model('customer', 'Email')
38
-CommunicationEventType = get_model('customer', 'CommunicationEventType')
34
+Email = get_model('communication', 'Email')
39 35
 
40 36
 User = get_user_model()
41 37
 
@@ -305,7 +301,6 @@ class ProfileView(PageTitleMixin, generic.TemplateView):
305 301
 class ProfileUpdateView(PageTitleMixin, generic.FormView):
306 302
     form_class = ProfileForm
307 303
     template_name = 'oscar/customer/profile/profile_form.html'
308
-    communication_type_code = 'EMAIL_CHANGED'
309 304
     page_title = _('Edit Profile')
310 305
     active_tab = 'profile'
311 306
     success_url = reverse_lazy('customer:profile-view')
@@ -334,19 +329,20 @@ class ProfileUpdateView(PageTitleMixin, generic.FormView):
334 329
             # Email address has changed - send a confirmation email to the old
335 330
             # address including a password reset link in case this is a
336 331
             # suspicious change.
337
-            ctx = {
338
-                'user': self.request.user,
339
-                'site': get_current_site(self.request),
340
-                'reset_url': get_password_reset_url(old_user),
341
-                'new_email': new_email,
342
-            }
343
-            msgs = CommunicationEventType.objects.get_and_render(
344
-                code=self.communication_type_code, context=ctx)
345
-            Dispatcher().dispatch_user_messages(old_user, msgs)
332
+            self.send_email_changed_email(old_user, new_email)
346 333
 
347 334
         messages.success(self.request, _("Profile updated"))
348 335
         return redirect(self.get_success_url())
349 336
 
337
+    def send_email_changed_email(self, old_user, new_email):
338
+        user = self.request.user
339
+        extra_context = {
340
+            'user': user,
341
+            'reset_url': get_password_reset_url(old_user),
342
+            'new_email': new_email,
343
+        }
344
+        CustomerDispatcher().send_email_changed_email_for_user(old_user, extra_context)
345
+
350 346
 
351 347
 class ProfileDeleteView(PageTitleMixin, generic.FormView):
352 348
     form_class = ConfirmPasswordForm
@@ -371,7 +367,6 @@ class ProfileDeleteView(PageTitleMixin, generic.FormView):
371 367
 class ChangePasswordView(PageTitleMixin, generic.FormView):
372 368
     form_class = PasswordChangeForm
373 369
     template_name = 'oscar/customer/profile/change_password_form.html'
374
-    communication_type_code = 'PASSWORD_CHANGED'
375 370
     page_title = _('Change Password')
376 371
     active_tab = 'profile'
377 372
     success_url = reverse_lazy('customer:profile-view')
@@ -386,17 +381,18 @@ class ChangePasswordView(PageTitleMixin, generic.FormView):
386 381
         update_session_auth_hash(self.request, self.request.user)
387 382
         messages.success(self.request, _("Password updated"))
388 383
 
389
-        ctx = {
390
-            'user': self.request.user,
391
-            'site': get_current_site(self.request),
392
-            'reset_url': get_password_reset_url(self.request.user),
393
-        }
394
-        msgs = CommunicationEventType.objects.get_and_render(
395
-            code=self.communication_type_code, context=ctx)
396
-        Dispatcher().dispatch_user_messages(self.request.user, msgs)
384
+        self.send_password_changed_email()
397 385
 
398 386
         return redirect(self.get_success_url())
399 387
 
388
+    def send_password_changed_email(self):
389
+        user = self.request.user
390
+        extra_context = {
391
+            'user': user,
392
+            'reset_url': get_password_reset_url(self.request.user),
393
+        }
394
+        CustomerDispatcher().send_password_changed_email_for_user(user, extra_context)
395
+
400 396
 
401 397
 # =============
402 398
 # Email history
@@ -404,7 +400,7 @@ class ChangePasswordView(PageTitleMixin, generic.FormView):
404 400
 
405 401
 class EmailHistoryView(PageTitleMixin, generic.ListView):
406 402
     context_object_name = "emails"
407
-    template_name = 'oscar/customer/email/email_list.html'
403
+    template_name = 'oscar/communication/email/email_list.html'
408 404
     paginate_by = settings.OSCAR_EMAILS_PER_PAGE
409 405
     page_title = _('Email History')
410 406
     active_tab = 'emails'
@@ -419,7 +415,7 @@ class EmailHistoryView(PageTitleMixin, generic.ListView):
419 415
 
420 416
 class EmailDetailView(PageTitleMixin, generic.DetailView):
421 417
     """Customer email"""
422
-    template_name = "oscar/customer/email/email_detail.html"
418
+    template_name = "oscar/communication/email/email_detail.html"
423 419
     context_object_name = 'email'
424 420
     active_tab = 'emails'
425 421
 

+ 1
- 1
src/oscar/apps/dashboard/communications/forms.py Bestand weergeven

@@ -5,7 +5,7 @@ from django.utils.translation import gettext_lazy as _
5 5
 from oscar.apps.customer.utils import normalise_email
6 6
 from oscar.core.loading import get_model
7 7
 
8
-CommunicationEventType = get_model('customer', 'CommunicationEventType')
8
+CommunicationEventType = get_model('communication', 'CommunicationEventType')
9 9
 Order = get_model('order', 'Order')
10 10
 
11 11
 

+ 2
- 2
src/oscar/apps/dashboard/communications/views.py Bestand weergeven

@@ -6,10 +6,10 @@ from django.views import generic
6 6
 
7 7
 from oscar.core.loading import get_class, get_model
8 8
 
9
-CommunicationEventType = get_model('customer', 'CommunicationEventType')
9
+CommunicationEventType = get_model('communication', 'CommunicationEventType')
10 10
 CommunicationEventTypeForm = get_class('dashboard.communications.forms',
11 11
                                        'CommunicationEventTypeForm')
12
-Dispatcher = get_class('customer.utils', 'Dispatcher')
12
+Dispatcher = get_class('communication.utils', 'Dispatcher')
13 13
 
14 14
 
15 15
 class ListView(generic.ListView):

+ 1
- 1
src/oscar/apps/order/abstract_models.py Bestand weergeven

@@ -472,7 +472,7 @@ class AbstractCommunicationEvent(models.Model):
472 472
         related_name="communication_events",
473 473
         verbose_name=_("Order"))
474 474
     event_type = models.ForeignKey(
475
-        'customer.CommunicationEventType',
475
+        'communication.CommunicationEventType',
476 476
         on_delete=models.CASCADE,
477 477
         verbose_name=_("Event Type"))
478 478
     date_created = models.DateTimeField(_("Date"), auto_now_add=True, db_index=True)

+ 20
- 0
src/oscar/apps/order/migrations/0008_auto_20190301_1035.py Bestand weergeven

@@ -0,0 +1,20 @@
1
+# Generated by Django 2.1.7 on 2019-03-01 10:35
2
+
3
+from django.db import migrations, models
4
+import django.db.models.deletion
5
+
6
+
7
+class Migration(migrations.Migration):
8
+
9
+    dependencies = [
10
+        ('order', '0007_auto_20181115_1953'),
11
+        ('communication', '0001_initial'),
12
+    ]
13
+
14
+    operations = [
15
+        migrations.AlterField(
16
+            model_name='communicationevent',
17
+            name='event_type',
18
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='communication.CommunicationEventType', verbose_name='Event Type'),
19
+        ),
20
+    ]

+ 46
- 1
src/oscar/apps/order/utils.py Bestand weergeven

@@ -6,13 +6,16 @@ from django.db import transaction
6 6
 from django.utils.translation import gettext_lazy as _
7 7
 
8 8
 from oscar.apps.order.signals import order_placed
9
-from oscar.core.loading import get_model
9
+from oscar.core.loading import get_class, get_model
10 10
 
11 11
 from . import exceptions
12 12
 
13 13
 Order = get_model('order', 'Order')
14 14
 Line = get_model('order', 'Line')
15 15
 OrderDiscount = get_model('order', 'OrderDiscount')
16
+CommunicationEvent = get_model('order', 'CommunicationEvent')
17
+CommunicationEventType = get_model('communication', 'CommunicationEventType')
18
+Dispatcher = get_class('communication.utils', 'Dispatcher')
16 19
 
17 20
 
18 21
 class OrderNumberGenerator(object):
@@ -260,3 +263,45 @@ class OrderCreator(object):
260 263
         Updates the models that care about this voucher.
261 264
         """
262 265
         voucher.record_usage(order, user)
266
+
267
+
268
+class OrderDispatcher:
269
+    """
270
+    Dispatcher to send concrete order related emails.
271
+    """
272
+
273
+    # Event codes
274
+    ORDER_PLACED_EVENT_CODE = 'ORDER_PLACED'
275
+
276
+    def __init__(self, logger=None, mail_connection=None):
277
+        self.dispatcher = Dispatcher(logger=logger, mail_connection=mail_connection)
278
+
279
+    def dispatch_order_messages(self, order, messages, event_code, attachments=None, **kwargs):
280
+        """
281
+        Dispatch order-related messages to the customer.
282
+        """
283
+        self.dispatcher.logger.info("Order #%s - sending %s messages", order.number, event_code)
284
+        if order.is_anonymous:
285
+            email = kwargs.get('email_address', order.guest_email)
286
+            dispatched_messages = self.dispatcher.dispatch_anonymous_messages(email, messages, attachments)
287
+        else:
288
+            dispatched_messages = self.dispatcher.dispatch_user_messages(order.user, messages, attachments)
289
+
290
+        try:
291
+            event_type = CommunicationEventType.objects.get(code=event_code)
292
+        except CommunicationEventType.DoesNotExist:
293
+            event_type = None
294
+
295
+        self.create_communication_event(order, event_type, dispatched_messages)
296
+
297
+    def create_communication_event(self, order, event_type, dispatched_messages):
298
+        """
299
+        Create order communications event for audit.
300
+        """
301
+        if dispatched_messages and event_type is not None:
302
+            CommunicationEvent._default_manager.create(order=order, event_type=event_type)
303
+
304
+    def send_order_placed_email_for_user(self, order, extra_context, attachments=None):
305
+        event_code = self.ORDER_PLACED_EVENT_CODE
306
+        messages = self.dispatcher.get_messages(event_code, extra_context)
307
+        self.dispatch_order_messages(order, messages, event_code, attachments=attachments)

+ 20
- 0
src/oscar/core/loading.py Bestand weergeven

@@ -1,5 +1,6 @@
1 1
 import sys
2 2
 import traceback
3
+import warnings
3 4
 from importlib import import_module
4 5
 
5 6
 from django.apps import apps
@@ -11,6 +12,16 @@ from django.utils.module_loading import import_string
11 12
 
12 13
 from oscar.core.exceptions import (
13 14
     AppNotFoundError, ClassNotFoundError, ModuleNotFoundError)
15
+from oscar.utils.deprecation import RemovedInOscar22Warning
16
+
17
+# To preserve backwards compatibility of loading classes which moved
18
+# from one Oscar module to another, we look into the dictionary below
19
+# for the moved items during loading.
20
+MOVED_MODELS = {
21
+    'customer': (
22
+        'communication', ('communicationeventtype', 'email', 'notification')
23
+    )
24
+}
14 25
 
15 26
 
16 27
 def get_class(module_label, classname, module_prefix='oscar.apps'):
@@ -224,6 +235,15 @@ def get_model(app_label, model_name):
224 235
     registry not being ready yet.
225 236
     Raises LookupError if model isn't found.
226 237
     """
238
+    oscar_moved_model = MOVED_MODELS.get(app_label, None)
239
+    if oscar_moved_model:
240
+        if model_name.lower() in oscar_moved_model[1]:
241
+            original_app_label = app_label
242
+            app_label = oscar_moved_model[0]
243
+            warnings.warn(
244
+                'Model %s has recently moved from %s to the application %s, '
245
+                'please update your imports.' % (model_name, original_app_label, app_label),
246
+                RemovedInOscar22Warning, stacklevel=2)
227 247
     try:
228 248
         return apps.get_model(app_label, model_name)
229 249
     except AppRegistryNotReady:

+ 4
- 0
src/oscar/defaults.py Bestand weergeven

@@ -245,3 +245,7 @@ OSCAR_SEARCH_FACETS = {
245 245
 OSCAR_PRODUCT_SEARCH_HANDLER = None
246 246
 
247 247
 OSCAR_THUMBNAILER = 'oscar.core.thumbnails.SorlThumbnail'
248
+
249
+OSCAR_URL_SCHEMA = 'http'
250
+
251
+OSCAR_SAVE_SENT_EMAILS_TO_DB = True

+ 2
- 3
src/oscar/management/commands/oscar_generate_email_content.py Bestand weergeven

@@ -1,10 +1,9 @@
1 1
 from django.core.management.base import BaseCommand, CommandError
2 2
 
3
-from oscar.core.loading import get_class, get_model
3
+from oscar.core.loading import get_model
4 4
 
5 5
 Order = get_model('order', 'Order')
6
-CommunicationEventType = get_model('customer', 'CommunicationEventType')
7
-Dispatcher = get_class('customer.utils', 'Dispatcher')
6
+CommunicationEventType = get_model('communication', 'CommunicationEventType')
8 7
 
9 8
 
10 9
 class Command(BaseCommand):

+ 2
- 2
src/oscar/management/commands/oscar_send_alerts.py Bestand weergeven

@@ -3,7 +3,7 @@ import logging
3 3
 from django.core.management.base import BaseCommand
4 4
 from django.utils.translation import gettext_lazy as _
5 5
 
6
-from oscar.apps.customer.alerts import utils
6
+from oscar.apps.customer.alerts.utils import AlertsDispatcher
7 7
 
8 8
 logger = logging.getLogger(__name__)
9 9
 
@@ -22,4 +22,4 @@ class Command(BaseCommand):
22 22
         availability and send out email alerts when a product is
23 23
         available to buy.
24 24
         """
25
-        utils.send_alerts()
25
+        AlertsDispatcher().send_alerts()

src/oscar/templates/oscar/customer/email/email_detail.html → src/oscar/templates/oscar/communication/email/email_detail.html Bestand weergeven


src/oscar/templates/oscar/customer/email/email_list.html → src/oscar/templates/oscar/communication/email/email_list.html Bestand weergeven


src/oscar/templates/oscar/customer/emails/base.html → src/oscar/templates/oscar/communication/emails/base.html Bestand weergeven


src/oscar/templates/oscar/customer/emails/base.txt → src/oscar/templates/oscar/communication/emails/base.txt Bestand weergeven


src/oscar/templates/oscar/customer/emails/commtype_email_changed_body.html → src/oscar/templates/oscar/communication/emails/commtype_email_changed_body.html Bestand weergeven

@@ -1,5 +1,6 @@
1
-{% extends "oscar/customer/emails/base.html" %}
1
+{% extends "oscar/communication/emails/base.html" %}
2 2
 {% load i18n %}
3
+{% load url_tags %}
3 4
 
4 5
 {% block tbody %}
5 6
 <tr>
@@ -16,7 +17,7 @@
16 17
 
17 18
 <tr>
18 19
     <td class="content-block">
19
-        <a href="http://{{ site.domain }}{{ reset_url }}" class="btn-primary">Reset password</a>
20
+        <a href="{% absolute_url site.domain reset_url %}" class="btn-primary">Reset password</a>
20 21
     </td>
21 22
 </tr>
22 23
 

src/oscar/templates/oscar/customer/emails/commtype_email_changed_body.txt → src/oscar/templates/oscar/communication/emails/commtype_email_changed_body.txt Bestand weergeven

@@ -1,10 +1,11 @@
1
-{% extends "oscar/customer/emails/base.txt" %}
1
+{% extends "oscar/communication/emails/base.txt" %}
2 2
 {% load i18n %}
3
+{% load url_tags %}
3 4
 
4 5
 {% block body %}{% autoescape off %}{% blocktrans with email=new_email %}You're receiving this email because your email address has been changed to {{ email }}.{% endblocktrans %}
5 6
 
6 7
 {% trans "If it wasn't you who changed it, please reset your password immediately and correct your email address:" %}
7
-http://{{ site.domain }}{{ reset_url }}
8
+{% absolute_url site.domain reset_url %}
8 9
 
9 10
 {% trans "If it was you who changed the email address, you can ignore this email." %}
10 11
 {% endautoescape %}{% endblock %}

src/oscar/templates/oscar/customer/emails/commtype_email_changed_subject.txt → src/oscar/templates/oscar/communication/emails/commtype_email_changed_subject.txt Bestand weergeven


src/oscar/templates/oscar/customer/emails/commtype_order_placed_body.html → src/oscar/templates/oscar/communication/emails/commtype_order_placed_body.html Bestand weergeven

@@ -1,4 +1,4 @@
1
-{% extends "oscar/customer/emails/base.html" %}
1
+{% extends "oscar/communication/emails/base.html" %}
2 2
 {% load currency_filters i18n %}
3 3
 
4 4
 {% block tbody %}

src/oscar/templates/oscar/customer/emails/commtype_order_placed_body.txt → src/oscar/templates/oscar/communication/emails/commtype_order_placed_body.txt Bestand weergeven


src/oscar/templates/oscar/customer/emails/commtype_order_placed_subject.txt → src/oscar/templates/oscar/communication/emails/commtype_order_placed_subject.txt Bestand weergeven


src/oscar/templates/oscar/customer/emails/commtype_password_changed_body.html → src/oscar/templates/oscar/communication/emails/commtype_password_changed_body.html Bestand weergeven

@@ -1,5 +1,6 @@
1
-{% extends "oscar/customer/emails/base.html" %}
1
+{% extends "oscar/communication/emails/base.html" %}
2 2
 {% load i18n %}
3
+{% load url_tags %}
3 4
 
4 5
 {% block tbody %}
5 6
     <tr>
@@ -16,7 +17,7 @@
16 17
 
17 18
     <tr>
18 19
         <td class="content-block">
19
-            <a href="http://{{ site.domain }}{{ reset_url }}" class="btn-primary">Reset password</a>
20
+            <a href="{% absolute_url site.domain reset_url %}" class="btn-primary">Reset password</a>
20 21
         </td>
21 22
     </tr>
22 23
 

src/oscar/templates/oscar/customer/emails/commtype_password_changed_body.txt → src/oscar/templates/oscar/communication/emails/commtype_password_changed_body.txt Bestand weergeven

@@ -1,12 +1,13 @@
1
-{% extends "oscar/customer/emails/base.txt" %}
1
+{% extends "oscar/communication/emails/base.txt" %}
2 2
 {% load i18n %}
3
+{% load url_tags %}
3 4
 
4 5
 {% block body %}{% autoescape off %}{% blocktrans with name=site.name %}
5 6
 You're receiving this email because your password has been changed at {{ name }}.
6 7
 {% endblocktrans %}
7 8
 
8 9
 {% trans "If it wasn't you who changed it, please reset your password immediately:" %}
9
-http://{{ site.domain }}{{ reset_url }}
10
+{% absolute_url site.domain reset_url %}
10 11
 
11 12
 {% trans "Otherwise, you can ignore this email." %}
12 13
 

src/oscar/templates/oscar/customer/emails/commtype_password_changed_subject.txt → src/oscar/templates/oscar/communication/emails/commtype_password_changed_subject.txt Bestand weergeven


src/oscar/templates/oscar/customer/emails/commtype_password_reset_body.html → src/oscar/templates/oscar/communication/emails/commtype_password_reset_body.html Bestand weergeven

@@ -1,5 +1,6 @@
1
-{% extends "oscar/customer/emails/base.html" %}
1
+{% extends "oscar/communication/emails/base.html" %}
2 2
 {% load i18n %}
3
+{% load url_tags %}
3 4
 
4 5
 {% block tbody %}
5 6
 <tr>
@@ -16,7 +17,7 @@
16 17
 
17 18
 <tr>
18 19
     <td class="content-block">
19
-        <a href="{{ reset_url }}" class="btn-primary">Reset password</a>
20
+        <a href="{% absolute_url site.domain reset_url %}" class="btn-primary">Reset password</a>
20 21
     </td>
21 22
 </tr>
22 23
 

src/oscar/templates/oscar/customer/emails/commtype_password_reset_body.txt → src/oscar/templates/oscar/communication/emails/commtype_password_reset_body.txt Bestand weergeven

@@ -1,8 +1,9 @@
1
-{% extends "oscar/customer/emails/base.txt" %}
1
+{% extends "oscar/communication/emails/base.txt" %}
2 2
 {% load i18n %}
3
+{% load url_tags %}
3 4
 
4 5
 {% block body %}{% autoescape off %}
5 6
 {% blocktrans with name=site.name %}You're receiving this e-mail because you requested a password reset for your user account at {{ name }}.{% endblocktrans %}
6 7
 
7 8
 {% trans "Please go to the following page and choose a new password:" %}
8
-{{ reset_url }}{% endautoescape %}{% endblock %}
9
+{% absolute_url site.domain reset_url %}{% endautoescape %}{% endblock %}

src/oscar/templates/oscar/customer/emails/commtype_password_reset_subject.txt → src/oscar/templates/oscar/communication/emails/commtype_password_reset_subject.txt Bestand weergeven


src/oscar/templates/oscar/customer/emails/commtype_product_alert_body.html → src/oscar/templates/oscar/communication/emails/commtype_product_alert_body.html Bestand weergeven

@@ -1,5 +1,6 @@
1
-{% extends "oscar/customer/emails/base.html" %}
1
+{% extends "oscar/communication/emails/base.html" %}
2 2
 {% load i18n %}
3
+{% load url_tags %}
3 4
 
4 5
 {% block body %}
5 6
     <p>
@@ -11,9 +12,10 @@
11 12
     </p>
12 13
 
13 14
     <p>
14
-        {% blocktrans with title=alert.product.get_title|safe path=alert.product.get_absolute_url %}
15
+        {% absolute_url site.domain alert.product.get_absolute_url as url %}
16
+        {% blocktrans with title=alert.product.get_title|safe url=url %}
15 17
             We are happy to inform you that our product '{{ title }}' is back in stock:
16
-            http://{{ site }}{{ path }}
18
+            {{ url }}
17 19
         {% endblocktrans %}
18 20
     </p>
19 21
 
@@ -26,7 +28,7 @@
26 28
     {% endif %}
27 29
 
28 30
     <p>
29
-        {% blocktrans with site_name=site.name %}
31
+        {% blocktrans %}
30 32
             With this email we have disabled your alert automatically and you will not
31 33
             receive any further email regarding this product.
32 34
         {% endblocktrans %}

src/oscar/templates/oscar/customer/emails/commtype_product_alert_body.txt → src/oscar/templates/oscar/communication/emails/commtype_product_alert_body.txt Bestand weergeven

@@ -1,7 +1,8 @@
1
-{% load i18n %}{% if alert.user and alert.user.get_short_name %}{% blocktrans with name=alert.user.get_short_name %}Dear {{ name }},{% endblocktrans %}{% else %}{% trans "Hello," %}{% endif %}
2
-{% blocktrans with title=alert.product.get_title|safe path=alert.product.get_absolute_url %}
1
+{% load i18n %}{% load url_tags %}{% if alert.user and alert.user.get_short_name %}{% blocktrans with name=alert.user.get_short_name %}Dear {{ name }},{% endblocktrans %}{% else %}{% trans "Hello," %}{% endif %}
2
+{% absolute_url site.domain alert.product.get_absolute_url as url %}
3
+{% blocktrans with title=alert.product.get_title|safe url=url %}
3 4
 We are happy to inform you that our product '{{ title }}' is back in stock:
4
-http://{{ site }}{{ path }}
5
+{{ url }}
5 6
 {% endblocktrans %}{% if hurry %}{% blocktrans %}
6 7
 Beware that the amount of items in stock is limited. Be quick or someone might get there first.
7 8
 {% endblocktrans %}{% endif %}{% blocktrans with site_name=site.name %}

+ 23
- 0
src/oscar/templates/oscar/communication/emails/commtype_product_alert_confirmation_body.html Bestand weergeven

@@ -0,0 +1,23 @@
1
+{% extends "oscar/communication/emails/base.html" %}
2
+{% load i18n %}
3
+{% load url_tags %}
4
+
5
+{% block body %}
6
+    {% absolute_url site.domain alert.get_confirm_url as confirm_url %}
7
+    {% absolute_url site.domain alert.get_cancel_url as cancel_url %}
8
+    {% blocktrans with title=alert.product.get_title|safe confirm_url=confirm_url cancel_url=cancel_url %}
9
+        Hello,
10
+        <p>
11
+            Somebody (hopefully you) has requested an email alert when '{{ title }}' is back in stock.
12
+            Please click the following link to confirm: {{ confirm_url }}
13
+        </p>
14
+        <p>
15
+            You can cancel this alert at any time by clicking the following link: {{ cancel_url }}
16
+        </p>
17
+
18
+        <p>
19
+            Thanks for your interest,
20
+            The {{ site }} Team
21
+        </p>
22
+    {% endblocktrans %}
23
+{% endblock %}

+ 17
- 0
src/oscar/templates/oscar/communication/emails/commtype_product_alert_confirmation_body.txt Bestand weergeven

@@ -0,0 +1,17 @@
1
+{% load i18n %}
2
+{% load url_tags %}
3
+{% absolute_url site.domain alert.get_confirm_url as confirm_url %}
4
+{% absolute_url site.domain alert.get_cancel_url as cancel_url %}
5
+{% blocktrans with title=alert.product.get_title|safe confirm_url=confirm_url cancel_url=cancel_url %}
6
+Hello,
7
+
8
+Somebody (hopefully you) has requested an email alert when
9
+'{{ title }}' is back in stock.  Please click the following link
10
+to confirm: {{ confirm_url }}
11
+
12
+You can cancel this alert at any time by clicking the following link:
13
+{{ cancel_url }}
14
+
15
+Thanks for your interest,
16
+The {{ site }} Team
17
+{% endblocktrans %}

src/oscar/templates/oscar/customer/emails/commtype_product_alert_confirmation_subject.txt → src/oscar/templates/oscar/communication/emails/commtype_product_alert_confirmation_subject.txt Bestand weergeven


src/oscar/templates/oscar/customer/emails/commtype_product_alert_subject.txt → src/oscar/templates/oscar/communication/emails/commtype_product_alert_subject.txt Bestand weergeven


src/oscar/templates/oscar/customer/emails/commtype_registration_body.html → src/oscar/templates/oscar/communication/emails/commtype_registration_body.html Bestand weergeven


src/oscar/templates/oscar/customer/emails/commtype_registration_body.txt → src/oscar/templates/oscar/communication/emails/commtype_registration_body.txt Bestand weergeven


+ 0
- 0
src/oscar/templates/oscar/communication/emails/commtype_registration_sms.txt Bestand weergeven


src/oscar/templates/oscar/customer/emails/commtype_registration_subject.txt → src/oscar/templates/oscar/communication/emails/commtype_registration_subject.txt Bestand weergeven


src/oscar/templates/oscar/customer/notifications/detail.html → src/oscar/templates/oscar/communication/notifications/detail.html Bestand weergeven


src/oscar/templates/oscar/customer/notifications/list.html → src/oscar/templates/oscar/communication/notifications/list.html Bestand weergeven


+ 0
- 13
src/oscar/templates/oscar/customer/emails/commtype_product_alert_confirmation_body.txt Bestand weergeven

@@ -1,13 +0,0 @@
1
-{% load i18n %}{% blocktrans with title=alert.product.get_title|safe domain=site.domain confirm_path=alert.get_confirm_url cancel_path=alert.get_cancel_url %}Hello,
2
-
3
-Somebody (hopefully you) has requested an email alert when
4
-'{{ title }}' is back in stock.  Please click the following link
5
-to confirm:
6
-http://{{ domain }}{{ confirm_path }}
7
-
8
-You can cancel this alert at any time by clicking the following link:
9
-http://{{ domain }}{{ cancel_path }}
10
-
11
-Thanks for your interest,
12
-The {{ site }} Team
13
-{% endblocktrans %}

+ 13
- 0
src/oscar/templatetags/url_tags.py Bestand weergeven

@@ -0,0 +1,13 @@
1
+from django import template
2
+from django.conf import settings
3
+
4
+register = template.Library()
5
+
6
+
7
+@register.simple_tag
8
+def absolute_url(domain, path):
9
+    return '{schema}://{domain}{path}'.format(
10
+        schema=settings.OSCAR_URL_SCHEMA,
11
+        domain=domain,
12
+        path=path
13
+    )

+ 25
- 1
src/oscar/test/utils.py Bestand weergeven

@@ -8,6 +8,7 @@ from django.conf import settings
8 8
 from django.contrib.auth.models import AnonymousUser
9 9
 from django.contrib.messages.storage.fallback import FallbackStorage
10 10
 from django.contrib.sessions.backends.db import SessionStore
11
+from django.core import mail
11 12
 from django.core.signing import Signer
12 13
 from django.db import connection
13 14
 from django.test import RequestFactory as BaseRequestFactory
@@ -15,7 +16,7 @@ from sorl.thumbnail.conf import settings as sorl_settings
15 16
 
16 17
 from oscar.core.loading import get_class, get_model
17 18
 from oscar.core.thumbnails import get_thumbnailer
18
-from oscar.test.factories import ProductImageFactory
19
+from oscar.test.factories import ProductImageFactory, UserFactory
19 20
 
20 21
 OSCAR_IMAGE_FOLDER_FORMATTED = 'images/products/{0}/{1:02d}/'.format(date.today().year, date.today().month)
21 22
 FULL_PATH_TO_IMAGES_FOLDER = os.path.join(settings.MEDIA_ROOT, OSCAR_IMAGE_FOLDER_FORMATTED)
@@ -97,6 +98,29 @@ class ThumbnailMixin:
97 98
             assert not os.path.isfile(path)
98 99
 
99 100
 
101
+class EmailsMixin:
102
+
103
+    def setUp(self):
104
+        super().setUp()
105
+        self.user = UserFactory()
106
+
107
+    def _test_send_plain_text_and_html(self, outboxed_email):
108
+        email = outboxed_email
109
+
110
+        assert '</p>' not in email.body  # Plain text body (because w/o </p> tags)
111
+
112
+        html_content = email.alternatives[0][0]
113
+        assert '</p>' in html_content
114
+
115
+        mimetype = email.alternatives[0][1]
116
+        assert mimetype == 'text/html'
117
+
118
+    def _test_common_part(self):
119
+        assert len(mail.outbox) == 1
120
+        assert mail.outbox[0].to == [self.user.email]
121
+        self._test_send_plain_text_and_html(mail.outbox[0])
122
+
123
+
100 124
 class RequestFactory(BaseRequestFactory):
101 125
     Basket = get_model('basket', 'basket')
102 126
     selector = get_class('partner.strategy', 'Selector')()

+ 4
- 0
src/oscar/utils/deprecation.py Bestand weergeven

@@ -1,2 +1,6 @@
1 1
 class RemovedInOscar21Warning(DeprecationWarning):
2 2
     pass
3
+
4
+
5
+class RemovedInOscar22Warning(DeprecationWarning):
6
+    pass

+ 40
- 0
tests/_site/apps/customer/migrations/0006_auto_20190430_1736.py Bestand weergeven

@@ -0,0 +1,40 @@
1
+# Generated by Django 2.0.13 on 2019-04-30 16:36
2
+
3
+from django.db import migrations
4
+
5
+
6
+class Migration(migrations.Migration):
7
+
8
+    dependencies = [
9
+        ('customer', '0005_auto_20181115_1953'),
10
+    ]
11
+
12
+    state_operations = [
13
+        migrations.DeleteModel(
14
+            name='CommunicationEventType',
15
+        ),
16
+        migrations.RemoveField(
17
+            model_name='email',
18
+            name='user',
19
+        ),
20
+        migrations.RemoveField(
21
+            model_name='notification',
22
+            name='recipient',
23
+        ),
24
+        migrations.RemoveField(
25
+            model_name='notification',
26
+            name='sender',
27
+        ),
28
+        migrations.DeleteModel(
29
+            name='Email',
30
+        ),
31
+        migrations.DeleteModel(
32
+            name='Notification',
33
+        ),
34
+    ]
35
+
36
+    operations = [
37
+        migrations.SeparateDatabaseAndState(
38
+            state_operations=state_operations
39
+        ),
40
+    ]

+ 0
- 22
tests/functional/checkout/test_customer_checkout.py Bestand weergeven

@@ -1,5 +1,4 @@
1 1
 from http import client as http_client
2
-from unittest.mock import patch
3 2
 
4 3
 from django.urls import reverse
5 4
 
@@ -247,24 +246,3 @@ class TestThankYouView(CheckoutMixin, WebTestCase):
247 246
         test_url = '%s?order_id=%s' % (reverse('checkout:thank-you'), order.pk)
248 247
         response = self.get(test_url, status='*', user=user)
249 248
         self.assertEqual(response.status_code, http_client.NOT_FOUND)
250
-
251
-
252
-class TestOrderPlacementMixin(CheckoutMixin, WebTestCase):
253
-
254
-    @patch('oscar.apps.checkout.mixins.logger')
255
-    def test_get_message_context_with_no_code(self, mock_logger):
256
-        original_get_message_context = OrderPlacementMixin.get_message_context
257
-
258
-        def get_message_context(self_, order):
259
-            get_message_context.called = True
260
-            return original_get_message_context(self_, order)
261
-        get_message_context.called = False
262
-
263
-        with patch.object(OrderPlacementMixin, 'get_message_context',
264
-                          get_message_context):
265
-            self.add_product_to_basket()
266
-            self.enter_shipping_address()
267
-            self.place_order()
268
-
269
-        self.assertTrue(mock_logger.warning.called)
270
-        self.assertTrue(get_message_context.called)

+ 50
- 59
tests/functional/customer/test_alert.py Bestand weergeven

@@ -6,15 +6,17 @@ from django.test import TestCase
6 6
 from django.urls import reverse
7 7
 from django_webtest import WebTest
8 8
 
9
-from oscar.apps.customer.alerts.utils import (
10
-    send_alert_confirmation, send_product_alerts)
11 9
 from oscar.apps.customer.forms import ProductAlertForm
12 10
 from oscar.apps.customer.models import ProductAlert
11
+from oscar.core.loading import get_class
13 12
 from oscar.test.factories import (
14 13
     ProductAlertFactory, UserFactory, create_product, create_stockrecord)
15 14
 
15
+CustomerDispatcher = get_class('customer.utils', 'CustomerDispatcher')
16
+AlertsDispatcher = get_class('customer.alerts.utils', 'AlertsDispatcher')
16 17
 
17
-class TestAUser(WebTest):
18
+
19
+class TestProductAlert(WebTest):
18 20
 
19 21
     def setUp(self):
20 22
         self.user = UserFactory()
@@ -26,10 +28,10 @@ class TestAUser(WebTest):
26 28
         form.submit()
27 29
 
28 30
         alerts = ProductAlert.objects.filter(user=self.user)
29
-        self.assertEqual(1, len(alerts))
31
+        assert len(alerts) == 1
30 32
         alert = alerts[0]
31
-        self.assertEqual(ProductAlert.ACTIVE, alert.status)
32
-        self.assertEqual(alert.product, self.product)
33
+        assert ProductAlert.ACTIVE == alert.status
34
+        assert alert.product == self.product
33 35
 
34 36
     def test_cannot_create_multiple_alerts_for_one_product(self):
35 37
         ProductAlertFactory(user=self.user, product=self.product,
@@ -37,10 +39,8 @@ class TestAUser(WebTest):
37 39
         # Alert form should not allow creation of additional alerts.
38 40
         form = ProductAlertForm(user=self.user, product=self.product, data={})
39 41
 
40
-        self.assertFalse(form.is_valid())
41
-        self.assertIn(
42
-            "You already have an active alert for this product",
43
-            form.errors['__all__'][0])
42
+        assert not form.is_valid()
43
+        assert "You already have an active alert for this product" in form.errors['__all__'][0]
44 44
 
45 45
 
46 46
 class TestAUserWithAnActiveStockAlert(WebTest):
@@ -56,34 +56,34 @@ class TestAUserWithAnActiveStockAlert(WebTest):
56 56
 
57 57
     def test_can_cancel_it(self):
58 58
         alerts = ProductAlert.objects.filter(user=self.user)
59
-        self.assertEqual(1, len(alerts))
59
+        assert len(alerts) == 1
60 60
         alert = alerts[0]
61
-        self.assertFalse(alert.is_cancelled)
61
+        assert not alert.is_cancelled
62 62
         self.app.get(
63 63
             reverse('customer:alerts-cancel-by-pk', kwargs={'pk': alert.pk}),
64 64
             user=self.user)
65 65
 
66 66
         alerts = ProductAlert.objects.filter(user=self.user)
67
-        self.assertEqual(1, len(alerts))
67
+        assert len(alerts) == 1
68 68
         alert = alerts[0]
69
-        self.assertTrue(alert.is_cancelled)
69
+        assert alert.is_cancelled
70 70
 
71 71
     def test_gets_notified_when_it_is_back_in_stock(self):
72 72
         self.stockrecord.num_in_stock = 10
73 73
         self.stockrecord.save()
74
-        self.assertEqual(1, self.user.notifications.all().count())
74
+        assert self.user.notifications.all().count() == 1
75 75
 
76 76
     def test_gets_emailed_when_it_is_back_in_stock(self):
77 77
         self.stockrecord.num_in_stock = 10
78 78
         self.stockrecord.save()
79
-        self.assertEqual(1, len(mail.outbox))
79
+        assert len(mail.outbox) == 1
80 80
 
81 81
     def test_does_not_get_emailed_when_it_is_saved_but_still_zero_stock(self):
82 82
         self.stockrecord.num_in_stock = 0
83 83
         self.stockrecord.save()
84
-        self.assertEqual(0, len(mail.outbox))
84
+        assert len(mail.outbox) == 0
85 85
 
86
-    @mock.patch('oscar.apps.customer.alerts.utils.services.notify_user')
86
+    @mock.patch('oscar.apps.communication.utils.Dispatcher.notify_user')
87 87
     def test_site_notification_sent(self, mock_notify):
88 88
         self.stockrecord.num_in_stock = 10
89 89
         self.stockrecord.save()
@@ -94,7 +94,7 @@ class TestAUserWithAnActiveStockAlert(WebTest):
94 94
                 self.product.get_absolute_url(), self.product.title)
95 95
         )
96 96
 
97
-    @mock.patch('oscar.apps.customer.alerts.utils.services.notify_user')
97
+    @mock.patch('oscar.apps.communication.utils.Dispatcher.notify_user')
98 98
     def test_product_title_truncated_in_alert_notification_subject(self, mock_notify):
99 99
         self.product.title = ('Aut nihil dignissimos perspiciatis. Beatae sed consequatur odit incidunt. '
100 100
                               'Quaerat labore perferendis quasi aut sunt maxime accusamus laborum. '
@@ -122,10 +122,10 @@ class TestAnAnonymousUser(WebTest):
122 122
         form.submit()
123 123
 
124 124
         alerts = ProductAlert.objects.filter(email='john@smith.com')
125
-        self.assertEqual(1, len(alerts))
125
+        assert len(alerts) == 1
126 126
         alert = alerts[0]
127
-        self.assertEqual(ProductAlert.UNCONFIRMED, alert.status)
128
-        self.assertEqual(alert.product, product)
127
+        assert ProductAlert.UNCONFIRMED == alert.status
128
+        assert alert.product == product
129 129
 
130 130
     def test_can_cancel_unconfirmed_stock_alert(self):
131 131
         alert = ProductAlertFactory(
@@ -133,7 +133,7 @@ class TestAnAnonymousUser(WebTest):
133 133
         self.app.get(
134 134
             reverse('customer:alerts-cancel-by-key', kwargs={'key': alert.key}))
135 135
         alert.refresh_from_db()
136
-        self.assertTrue(alert.is_cancelled)
136
+        assert alert.is_cancelled
137 137
 
138 138
     def test_cannot_create_multiple_alerts_for_one_product(self):
139 139
         product = create_product(num_in_stock=0)
@@ -146,10 +146,8 @@ class TestAnAnonymousUser(WebTest):
146 146
         form = ProductAlertForm(user=AnonymousUser(), product=product,
147 147
                                 data={'email': 'john@smith.com'})
148 148
 
149
-        self.assertFalse(form.is_valid())
150
-        self.assertIn(
151
-            "There is already an active stock alert for john@smith.com",
152
-            form.errors['__all__'][0])
149
+        assert not form.is_valid()
150
+        assert "There is already an active stock alert for john@smith.com" in form.errors['__all__'][0]
153 151
 
154 152
     def test_cannot_create_multiple_unconfirmed_alerts(self):
155 153
         # Create an unconfirmed alert
@@ -163,10 +161,9 @@ class TestAnAnonymousUser(WebTest):
163 161
             data={'email': 'john@smith.com'},
164 162
         )
165 163
 
166
-        self.assertFalse(form.is_valid())
167
-        self.assertIn(
168
-            "john@smith.com has been sent a confirmation email for another "
169
-            "product alert on this site.", form.errors['__all__'][0])
164
+        assert not form.is_valid()
165
+        message = "john@smith.com has been sent a confirmation email for another product alert on this site."
166
+        assert message in form.errors['__all__'][0]
170 167
 
171 168
 
172 169
 class TestHurryMode(TestCase):
@@ -174,18 +171,17 @@ class TestHurryMode(TestCase):
174 171
     def setUp(self):
175 172
         self.user = UserFactory()
176 173
         self.product = create_product()
174
+        self.dispatcher = AlertsDispatcher()
177 175
 
178 176
     def test_hurry_mode_not_set_when_stock_high(self):
179 177
         # One alert, 5 items in stock. No need to hurry.
180 178
         create_stockrecord(self.product, num_in_stock=5)
181 179
         ProductAlert.objects.create(user=self.user, product=self.product)
182 180
 
183
-        send_product_alerts(self.product)
181
+        self.dispatcher.send_product_alert_email_for_user(self.product)
184 182
 
185
-        self.assertEqual(1, len(mail.outbox))
186
-        self.assertNotIn(
187
-            'Beware that the amount of items in stock is limited',
188
-            mail.outbox[0].body)
183
+        assert len(mail.outbox) == 1
184
+        assert 'Beware that the amount of items in stock is limited' not in mail.outbox[0].body
189 185
 
190 186
     def test_hurry_mode_set_when_stock_low(self):
191 187
         # Two alerts, 1 item in stock. Hurry mode should be set.
@@ -193,12 +189,10 @@ class TestHurryMode(TestCase):
193 189
         ProductAlert.objects.create(user=self.user, product=self.product)
194 190
         ProductAlert.objects.create(user=UserFactory(), product=self.product)
195 191
 
196
-        send_product_alerts(self.product)
192
+        self.dispatcher.send_product_alert_email_for_user(self.product)
197 193
 
198
-        self.assertEqual(2, len(mail.outbox))
199
-        self.assertIn(
200
-            'Beware that the amount of items in stock is limited',
201
-            mail.outbox[0].body)
194
+        assert len(mail.outbox) == 2
195
+        assert 'Beware that the amount of items in stock is limited' in mail.outbox[0].body
202 196
 
203 197
     def test_hurry_mode_not_set_multiple_stockrecords(self):
204 198
         # Two stockrecords, 5 items in stock for one. No need to hurry.
@@ -206,11 +200,9 @@ class TestHurryMode(TestCase):
206 200
         create_stockrecord(self.product, num_in_stock=5)
207 201
         ProductAlert.objects.create(user=self.user, product=self.product)
208 202
 
209
-        send_product_alerts(self.product)
203
+        self.dispatcher.send_product_alert_email_for_user(self.product)
210 204
 
211
-        self.assertNotIn(
212
-            'Beware that the amount of items in stock is limited',
213
-            mail.outbox[0].body)
205
+        assert 'Beware that the amount of items in stock is limited' not in mail.outbox[0].body
214 206
 
215 207
     def test_hurry_mode_set_multiple_stockrecords(self):
216 208
         # Two stockrecords, low stock on both. Hurry mode should be set.
@@ -219,11 +211,9 @@ class TestHurryMode(TestCase):
219 211
         ProductAlert.objects.create(user=self.user, product=self.product)
220 212
         ProductAlert.objects.create(user=UserFactory(), product=self.product)
221 213
 
222
-        send_product_alerts(self.product)
214
+        self.dispatcher.send_product_alert_email_for_user(self.product)
223 215
 
224
-        self.assertIn(
225
-            'Beware that the amount of items in stock is limited',
226
-            mail.outbox[0].body)
216
+        assert 'Beware that the amount of items in stock is limited' in mail.outbox[0].body
227 217
 
228 218
 
229 219
 class TestAlertMessageSending(TestCase):
@@ -232,8 +222,9 @@ class TestAlertMessageSending(TestCase):
232 222
         self.user = UserFactory()
233 223
         self.product = create_product()
234 224
         create_stockrecord(self.product, num_in_stock=1)
225
+        self.dispatcher = AlertsDispatcher()
235 226
 
236
-    @mock.patch('oscar.apps.customer.utils.Dispatcher.dispatch_direct_messages')
227
+    @mock.patch('oscar.apps.communication.utils.Dispatcher.dispatch_direct_messages')
237 228
     def test_alert_confirmation_uses_dispatcher(self, mock_dispatch):
238 229
         alert = ProductAlert.objects.create(
239 230
             email='test@example.com',
@@ -241,18 +232,18 @@ class TestAlertMessageSending(TestCase):
241 232
             status=ProductAlert.UNCONFIRMED,
242 233
             product=self.product
243 234
         )
244
-        send_alert_confirmation(alert)
245
-        self.assertEqual(mock_dispatch.call_count, 1)
246
-        self.assertEqual(mock_dispatch.call_args[0][0], 'test@example.com')
235
+        AlertsDispatcher().send_product_alert_confirmation_email_for_user(alert)
236
+        assert mock_dispatch.call_count == 1
237
+        assert mock_dispatch.call_args[0][0] == 'test@example.com'
247 238
 
248
-    @mock.patch('oscar.apps.customer.utils.Dispatcher.dispatch_user_messages')
239
+    @mock.patch('oscar.apps.communication.utils.Dispatcher.dispatch_user_messages')
249 240
     def test_alert_uses_dispatcher(self, mock_dispatch):
250 241
         ProductAlert.objects.create(user=self.user, product=self.product)
251
-        send_product_alerts(self.product)
252
-        self.assertEqual(mock_dispatch.call_count, 1)
253
-        self.assertEqual(mock_dispatch.call_args[0][0], self.user)
242
+        self.dispatcher.send_product_alert_email_for_user(self.product)
243
+        assert mock_dispatch.call_count == 1
244
+        assert mock_dispatch.call_args[0][0] == self.user
254 245
 
255 246
     def test_alert_creates_email_obj(self):
256 247
         ProductAlert.objects.create(user=self.user, product=self.product)
257
-        send_product_alerts(self.product)
258
-        self.assertEqual(self.user.emails.count(), 1)
248
+        self.dispatcher.send_product_alert_email_for_user(self.product)
249
+        assert self.user.emails.count() == 1

+ 114
- 0
tests/functional/customer/test_emails.py Bestand weergeven

@@ -0,0 +1,114 @@
1
+from django.contrib.sites.models import Site
2
+from django.core import mail
3
+from django.test import TestCase
4
+
5
+from oscar.core.loading import get_class
6
+from oscar.test.factories import (
7
+    ProductAlertFactory, UserFactory, create_product)
8
+from oscar.test.utils import EmailsMixin
9
+
10
+CustomerDispatcher = get_class('customer.utils', 'CustomerDispatcher')
11
+AlertsDispatcher = get_class('customer.alerts.utils', 'AlertsDispatcher')
12
+
13
+
14
+class TestCustomerConcreteEmailsSending(EmailsMixin, TestCase):
15
+
16
+    def setUp(self):
17
+        super().setUp()
18
+        self.dispatcher = CustomerDispatcher()
19
+
20
+    def test_send_registration_email_for_user(self):
21
+        extra_context = {'user': self.user}
22
+        self.dispatcher.send_registration_email_for_user(self.user, extra_context)
23
+
24
+        self._test_common_part()
25
+        self.assertEqual('Thank you for registering.', mail.outbox[0].subject)
26
+        self.assertIn('Thank you for registering.', mail.outbox[0].body)
27
+
28
+    def test_send_password_reset_email_for_user(self):
29
+        extra_context = {
30
+            'user': self.user,
31
+            'reset_url': '/django-oscar/django-oscar',
32
+        }
33
+        self.dispatcher.send_password_reset_email_for_user(self.user, extra_context)
34
+
35
+        self._test_common_part()
36
+        expected_subject = 'Resetting your password at {}.'.format(Site.objects.get_current())
37
+        self.assertEqual(expected_subject, mail.outbox[0].subject)
38
+        self.assertIn('Please go to the following page and choose a new password:', mail.outbox[0].body)
39
+        self.assertIn('http://example.com/django-oscar/django-oscar', mail.outbox[0].body)
40
+
41
+    def test_send_password_changed_email_for_user(self):
42
+        extra_context = {
43
+            'user': self.user,
44
+            'reset_url': '/django-oscar/django-oscar',
45
+        }
46
+        self.dispatcher.send_password_changed_email_for_user(self.user, extra_context)
47
+
48
+        self._test_common_part()
49
+        expected_subject = 'Your password changed at {}.'.format(Site.objects.get_current())
50
+        self.assertEqual(expected_subject, mail.outbox[0].subject)
51
+        self.assertIn('your password has been changed', mail.outbox[0].body)
52
+        self.assertIn('http://example.com/django-oscar/django-oscar', mail.outbox[0].body)
53
+
54
+    def test_send_email_changed_email_for_user(self):
55
+        extra_context = {
56
+            'user': self.user,
57
+            'reset_url': '/django-oscar/django-oscar',
58
+            'new_email': 'some_new@mail.com',
59
+        }
60
+        self.dispatcher.send_email_changed_email_for_user(self.user, extra_context)
61
+
62
+        self._test_common_part()
63
+        expected_subject = 'Your email address has changed at {}.'.format(Site.objects.get_current())
64
+        self.assertEqual(expected_subject, mail.outbox[0].subject)
65
+        self.assertIn('your email address has been changed', mail.outbox[0].body)
66
+        self.assertIn('http://example.com/django-oscar/django-oscar', mail.outbox[0].body)
67
+        self.assertIn('some_new@mail.com', mail.outbox[0].body)
68
+
69
+
70
+class TestAlertsConcreteEmailsSending(EmailsMixin, TestCase):
71
+
72
+    def setUp(self):
73
+        super().setUp()
74
+        self.dispatcher = AlertsDispatcher()
75
+
76
+    def test_send_product_alert_email_for_user(self):
77
+        product = create_product(num_in_stock=5)
78
+        ProductAlertFactory(product=product, user=self.user)
79
+
80
+        self.dispatcher.send_product_alert_email_for_user(product)
81
+
82
+        self._test_common_part()
83
+        expected_subject = u'{} is back in stock'.format(product.title)
84
+        self.assertEqual(expected_subject, mail.outbox[0].subject)
85
+        self.assertIn('We are happy to inform you that our product', mail.outbox[0].body)
86
+        # No `hurry_mode`
87
+        self.assertNotIn('Beware that the amount of items in stock is limited.', mail.outbox[0].body)
88
+
89
+    def test_send_product_alert_email_for_user_with_hurry_mode(self):
90
+        another_user = UserFactory(email='another_user@mail.com')
91
+        product = create_product(num_in_stock=1)
92
+        ProductAlertFactory(product=product, user=self.user, email=self.user.email)
93
+        ProductAlertFactory(product=product, user=another_user, email=another_user.email)
94
+
95
+        self.dispatcher.send_product_alert_email_for_user(product)
96
+        self.assertEqual(len(mail.outbox), 2)  # Separate email for each user
97
+        expected_subject = u'{} is back in stock'.format(product.title)
98
+        self.assertEqual(expected_subject, mail.outbox[0].subject)
99
+        for outboxed_email in mail.outbox:
100
+            self.assertEqual(expected_subject, outboxed_email.subject)
101
+            self.assertIn('We are happy to inform you that our product', outboxed_email.body)
102
+            self.assertIn('Beware that the amount of items in stock is limited.', outboxed_email.body)
103
+
104
+    def test_send_product_alert_confirmation_email_for_user(self):
105
+        product = create_product(num_in_stock=5)
106
+        alert = ProductAlertFactory(product=product, user=self.user, email=self.user.email, key='key00042')
107
+
108
+        self.dispatcher.send_product_alert_confirmation_email_for_user(alert)
109
+
110
+        self._test_common_part()
111
+        self.assertEqual('Confirmation required for stock alert', mail.outbox[0].subject)
112
+        self.assertIn('Somebody (hopefully you) has requested an email alert', mail.outbox[0].body)
113
+        self.assertIn(alert.get_confirm_url(), mail.outbox[0].body)
114
+        self.assertIn(alert.get_cancel_url(), mail.outbox[0].body)

+ 5
- 3
tests/functional/customer/test_notification.py Bestand weergeven

@@ -2,17 +2,19 @@ from http import client as http_client
2 2
 
3 3
 from django.urls import reverse
4 4
 
5
-from oscar.apps.customer.models import Notification
6
-from oscar.apps.customer.notifications import services
5
+from oscar.core.loading import get_class, get_model
7 6
 from oscar.test.factories import UserFactory
8 7
 from oscar.test.testcases import WebTestCase
9 8
 
9
+Dispatcher = get_class('communication.utils', 'Dispatcher')
10
+Notification = get_model('communication', 'Notification')
11
+
10 12
 
11 13
 class TestAUserWithUnreadNotifications(WebTestCase):
12 14
 
13 15
     def setUp(self):
14 16
         self.user = UserFactory()
15
-        services.notify_user(self.user, "Test message")
17
+        Dispatcher().notify_user(self.user, "Test message")
16 18
 
17 19
     def test_can_see_them_in_page_header(self):
18 20
         homepage = self.app.get('/', user=self.user)

+ 9
- 7
tests/functional/dashboard/test_communication.py Bestand weergeven

@@ -1,10 +1,12 @@
1 1
 from django.core import mail
2 2
 from django.urls import reverse
3 3
 
4
-from oscar.apps.customer.models import CommunicationEventType
4
+from oscar.core.loading import get_model
5 5
 from oscar.test.factories import UserFactory
6 6
 from oscar.test.testcases import WebTestCase
7 7
 
8
+CommunicationEventType = get_model('communication', 'CommunicationEventType')
9
+
8 10
 
9 11
 class TestAnAdmin(WebTestCase):
10 12
 
@@ -15,19 +17,19 @@ class TestAnAdmin(WebTestCase):
15 17
             category=CommunicationEventType.USER_RELATED)
16 18
 
17 19
     def test_can_preview_an_email(self):
18
-        list_page = self.app.get(reverse('dashboard:comms-list'),
19
-                                 user=self.staff)
20
+        list_page = self.app.get(
21
+            reverse('dashboard:comms-list'), user=self.staff)
20 22
         update_page = list_page.click('Edit')
21 23
         form = update_page.form
22 24
         form['email_subject_template'] = 'Hello {{ user.username }}'
23 25
         form['email_body_template'] = 'Hello {{ user.username }}'
24 26
         form['email_body_html_template'] = 'Hello {{ user.username }}'
25 27
         preview = form.submit('show_preview')
26
-        self.assertTrue('Hello 1234' in preview.content.decode('utf8'))
28
+        assert 'Hello 1234' in preview.content.decode('utf8')
27 29
 
28 30
     def test_can_send_a_preview_email(self):
29
-        list_page = self.app.get(reverse('dashboard:comms-list'),
30
-                                 user=self.staff)
31
+        list_page = self.app.get(
32
+            reverse('dashboard:comms-list'), user=self.staff)
31 33
         update_page = list_page.click('Edit')
32 34
         form = update_page.form
33 35
         form['email_subject_template'] = 'Hello {{ user.username }}'
@@ -36,4 +38,4 @@ class TestAnAdmin(WebTestCase):
36 38
         form['preview_email'] = 'testing@example.com'
37 39
         form.submit('send_preview')
38 40
 
39
-        self.assertEqual(len(mail.outbox), 1)
41
+        assert len(mail.outbox) == 1

+ 2
- 1
tests/functional/dashboard/test_user.py Bestand weergeven

@@ -3,12 +3,13 @@ from django.urls import reverse, reverse_lazy
3 3
 from django.utils.translation import gettext_lazy as _
4 4
 from webtest import AppError
5 5
 
6
-from oscar.apps.customer.models import ProductAlert
7 6
 from oscar.core.compat import get_user_model
7
+from oscar.core.loading import get_model
8 8
 from oscar.test.factories import ProductAlertFactory, UserFactory
9 9
 from oscar.test.testcases import WebTestCase
10 10
 
11 11
 User = get_user_model()
12
+ProductAlert = get_model('customer', 'ProductAlert')
12 13
 
13 14
 
14 15
 class IndexViewTests(WebTestCase):

+ 0
- 0
tests/functional/order/__init__.py Bestand weergeven


+ 61
- 0
tests/functional/order/test_emails.py Bestand weergeven

@@ -0,0 +1,61 @@
1
+import os
2
+
3
+from django.core import mail
4
+from django.test import TestCase
5
+
6
+from oscar.core.loading import get_class
7
+from oscar.test.factories import ProductImageFactory, create_order
8
+from oscar.test.utils import EmailsMixin, remove_image_folders
9
+
10
+OrderDispatcher = get_class('order.utils', 'OrderDispatcher')
11
+
12
+
13
+class TestConcreteEmailsSending(EmailsMixin, TestCase):
14
+
15
+    def setUp(self):
16
+        super().setUp()
17
+        self.dispatcher = OrderDispatcher()
18
+
19
+    def test_send_order_placed_email_for_user(self):
20
+        order_number = 'SOME-NUM00042'
21
+        order = create_order(number=order_number, user=self.user)
22
+
23
+        extra_context = {
24
+            'order': order,
25
+            'lines': order.lines.all()
26
+        }
27
+        self.dispatcher.send_order_placed_email_for_user(order, extra_context)
28
+
29
+        self._test_common_part()
30
+        expected_subject = 'Confirmation of order {}'.format(order_number)
31
+        assert expected_subject == mail.outbox[0].subject
32
+        assert 'Your order contains:' in mail.outbox[0].body
33
+        product_title = order.lines.first().title
34
+        assert product_title in mail.outbox[0].body
35
+
36
+    def test_send_order_placed_email_with_attachments_for_user(self):
37
+        remove_image_folders()
38
+
39
+        order_number = 'SOME-NUM00042'
40
+        order = create_order(number=order_number, user=self.user)
41
+
42
+        extra_context = {
43
+            'order': order,
44
+            'lines': order.lines.all()
45
+        }
46
+        line = order.lines.first()
47
+        product_image = ProductImageFactory(product=line.product)
48
+        attachments = [
49
+            ['fake_file.html', b'file_content', 'text/html'],
50
+            ['fake_image.png', b'file_content', 'image/png'],
51
+            product_image.original.path,  # To test sending file from `FileField` based field
52
+        ]
53
+        self.dispatcher.send_order_placed_email_for_user(order, extra_context, attachments)
54
+
55
+        # All attachments were sent with email
56
+        assert len(mail.outbox[0].attachments) == 3
57
+        expected_attachments = ['fake_file.html', 'fake_image.png', 'test_image.jpg']
58
+        assert [attachment[0] for attachment in mail.outbox[0].attachments] == expected_attachments
59
+
60
+        # Remove test file
61
+        os.remove(product_image.original.path)

+ 0
- 0
tests/integration/communication/__init__.py Bestand weergeven


+ 65
- 0
tests/integration/communication/test_communicationeventtype.py Bestand weergeven

@@ -0,0 +1,65 @@
1
+import pytest
2
+from django.core.exceptions import ValidationError
3
+from django.test import TestCase
4
+
5
+from oscar.core.compat import get_user_model
6
+from oscar.core.loading import get_model
7
+
8
+User = get_user_model()
9
+
10
+CommunicationEventType = get_model('communication', 'CommunicationEventType')
11
+
12
+
13
+class CommunicationTypeTest(TestCase):
14
+    keys = ('body', 'html', 'sms', 'subject')
15
+    expected_error_message = (
16
+        'Code can only contain the uppercase letters (A-Z), '
17
+        'digits, and underscores, and can\'t start with a digit.'
18
+    )
19
+
20
+    def test_no_templates_returns_empty_string(self):
21
+        et = CommunicationEventType()
22
+        messages = et.get_messages()
23
+        for key in self.keys:
24
+            assert messages[key] == ''
25
+
26
+    def test_field_template_render(self):
27
+        et = CommunicationEventType(email_subject_template='Hello {{ name }}')
28
+        ctx = {'name': 'world'}
29
+        messages = et.get_messages(ctx)
30
+        assert 'Hello world' == messages['subject']
31
+
32
+    def test_new_line_in_subject_is_removed(self):
33
+        subjects = [
34
+            ('Subject with a newline\r\n', 'Subject with a newline'),
35
+            ('New line is in \n the middle', 'New line is in  the middle'),
36
+            ('\rStart with the new line', 'Start with the new line'),
37
+        ]
38
+
39
+        for original, modified in subjects:
40
+            et = CommunicationEventType(email_subject_template=original)
41
+            messages = et.get_messages()
42
+            assert modified == messages['subject']
43
+
44
+    def test_code_field_forbids_hyphens(self):
45
+        et = CommunicationEventType(code='A-B')
46
+
47
+        with pytest.raises(ValidationError) as exc_info:
48
+            et.full_clean()
49
+
50
+        assert self.expected_error_message in str(exc_info.value)
51
+
52
+    def test_code_field_forbids_lowercase_letters(self):
53
+        et = CommunicationEventType(name='Test name *** - 123')
54
+        et.save()
55
+
56
+        # Automatically created code is uppercased
57
+        expected_code = 'TEST_NAME_123'
58
+        assert et.code == expected_code
59
+
60
+        # Lowercased code is not valid
61
+        et.code = 'lower_case_code'
62
+        with pytest.raises(ValidationError) as exc_info:
63
+            et.full_clean()
64
+
65
+        assert self.expected_error_message in str(exc_info.value)

+ 67
- 0
tests/integration/communication/test_dispatcher.py Bestand weergeven

@@ -0,0 +1,67 @@
1
+from django.core import mail
2
+from django.test import TestCase
3
+
4
+from oscar.apps.customer.utils import get_password_reset_url
5
+from oscar.core.compat import get_user_model
6
+from oscar.core.loading import get_class, get_model
7
+from oscar.test.factories import SiteFactory
8
+
9
+User = get_user_model()
10
+
11
+CommunicationEventType = get_model('communication', 'CommunicationEventType')
12
+Email = get_model('communication', 'Email')
13
+Dispatcher = get_class('communication.utils', 'Dispatcher')
14
+CustomerDispatcher = get_class('customer.utils', 'CustomerDispatcher')
15
+
16
+
17
+class TestDispatcher(TestCase):
18
+
19
+    def _dispatch_user_messages(self, user, event_code, ctx, subject):
20
+        msgs = CommunicationEventType.objects.get_and_render(code=event_code, context=ctx)
21
+        Dispatcher().dispatch_user_messages(user, msgs)
22
+        assert len(mail.outbox) == 1
23
+        assert mail.outbox[0].subject == subject
24
+        assert Email.objects.count() == 1
25
+        email = Email.objects.last()
26
+        assert email.user.id == user.id
27
+        assert email.email == 'testuser@example.com'
28
+
29
+    def test_dispatch_email_changed_user_message(self):
30
+        user = User.objects.create_user('testuser', 'testuser@example.com', 'somesimplepassword')
31
+        event_code = CustomerDispatcher.EMAIL_CHANGED_EVENT_CODE
32
+        CommunicationEventType.objects.create(
33
+            code=event_code,
34
+            name='Email Changed',
35
+            category=CommunicationEventType.USER_RELATED,
36
+        )
37
+        ctx = {
38
+            'user': user,
39
+            'site': SiteFactory(name='Test Site'),
40
+            'reset_url': get_password_reset_url(user),
41
+            'new_email': 'newtestuser@example.com',
42
+        }
43
+        self._dispatch_user_messages(
44
+            user, event_code, ctx, 'Your email address has changed at Test Site.'
45
+        )
46
+
47
+    def test_dispatch_registration_email_message(self):
48
+        user = User.objects.create_user('testuser', 'testuser@example.com', 'somesimplepassword')
49
+        event_code = CustomerDispatcher.REGISTRATION_EVENT_CODE
50
+        CommunicationEventType.objects.create(
51
+            code=event_code,
52
+            name='Registration',
53
+            category=CommunicationEventType.USER_RELATED,
54
+        )
55
+        ctx = {'user': user,
56
+               'site': SiteFactory()}
57
+        self._dispatch_user_messages(user, event_code, ctx, 'Thank you for registering.')
58
+
59
+    def test_dispatcher_uses_email_connection(self):
60
+        connection = mail.get_connection()
61
+        disp = Dispatcher(mail_connection=connection)
62
+        disp.dispatch_direct_messages('test@example.com', {
63
+            'subject': 'Test',
64
+            'body': 'This is a test.',
65
+            'html': '',
66
+        })
67
+        assert len(mail.outbox) == 1

tests/integration/customer/test_notification.py → tests/integration/communication/test_notification.py Bestand weergeven

@@ -1,12 +1,14 @@
1 1
 from django.test import TestCase
2 2
 
3
-from oscar.apps.customer.models import Notification
4
-from oscar.apps.customer.notifications import services
3
+from oscar.apps.communication.models import Notification
5 4
 from oscar.core.compat import get_user_model
5
+from oscar.core.loading import get_class
6 6
 from oscar.test.factories import UserFactory
7 7
 
8 8
 User = get_user_model()
9 9
 
10
+Dispatcher = get_class('communication.utils', 'Dispatcher')
11
+
10 12
 
11 13
 class TestANewNotification(TestCase):
12 14
 
@@ -16,10 +18,10 @@ class TestANewNotification(TestCase):
16 18
             subject="Hello")
17 19
 
18 20
     def test_is_in_a_users_inbox(self):
19
-        self.assertEqual(Notification.INBOX, self.notification.location)
21
+        assert Notification.INBOX == self.notification.location
20 22
 
21 23
     def test_is_not_read(self):
22
-        self.assertFalse(self.notification.is_read)
24
+        assert not self.notification.is_read
23 25
 
24 26
 
25 27
 class TestANotification(TestCase):
@@ -31,7 +33,7 @@ class TestANotification(TestCase):
31 33
 
32 34
     def test_can_be_archived(self):
33 35
         self.notification.archive()
34
-        self.assertEqual(Notification.ARCHIVE, self.notification.location)
36
+        assert Notification.ARCHIVE == self.notification.location
35 37
 
36 38
 
37 39
 class NotificationServiceTestCase(TestCase):
@@ -41,18 +43,18 @@ class NotificationServiceTestCase(TestCase):
41 43
         subj = "Hello you!"
42 44
         body = "This is the notification body."
43 45
 
44
-        services.notify_user(user, subj, body=body)
46
+        Dispatcher().notify_user(user, subj, body=body)
45 47
         user_notification = Notification.objects.get(recipient=user)
46
-        self.assertEqual(user_notification.subject, subj)
47
-        self.assertEqual(user_notification.body, body)
48
+        assert user_notification.subject == subj
49
+        assert user_notification.body == body
48 50
 
49 51
     def test_notify_a_set_of_users(self):
50 52
         users = UserFactory.create_batch(3)
51 53
         subj = "Hello everyone!"
52 54
         body = "This is the notification body."
53 55
 
54
-        services.notify_users(User.objects.all(), subj, body=body)
56
+        Dispatcher().notify_users(User.objects.all(), subj, body=body)
55 57
         for user in users:
56 58
             user_notification = Notification.objects.get(recipient=user)
57
-            self.assertEqual(user_notification.subject, subj)
58
-            self.assertEqual(user_notification.body, body)
59
+            assert user_notification.subject == subj
60
+            assert user_notification.body == body

+ 2
- 3
tests/integration/customer/test_alert.py Bestand weergeven

@@ -12,8 +12,7 @@ class TestAnAlertForARegisteredUser(TestCase):
12 12
     def setUp(self):
13 13
         user = UserFactory()
14 14
         product = create_product()
15
-        self.alert = ProductAlert.objects.create(user=user,
16
-                                                 product=product)
15
+        self.alert = ProductAlert.objects.create(user=user, product=product)
17 16
 
18 17
     def test_defaults_to_active(self):
19
-        self.assertTrue(self.alert.is_active)
18
+        assert self.alert.is_active

+ 1
- 1
tests/integration/customer/test_custom_user_model.py Bestand weergeven

@@ -2,7 +2,7 @@ from django.test import TestCase
2 2
 
3 3
 from oscar.apps.customer.forms import ProfileForm
4 4
 from oscar.core.compat import existing_user_fields, get_user_model
5
-from oscar.test.factories.customer import ProductAlertFactory, UserFactory
5
+from oscar.test.factories import ProductAlertFactory, UserFactory
6 6
 
7 7
 
8 8
 class TestACustomUserModel(TestCase):

+ 0
- 34
tests/integration/customer/test_customer.py Bestand weergeven

@@ -1,34 +0,0 @@
1
-from django.test import TestCase
2
-
3
-from oscar.apps.customer.models import CommunicationEventType
4
-from oscar.core.compat import get_user_model
5
-
6
-User = get_user_model()
7
-
8
-
9
-class CommunicationTypeTest(TestCase):
10
-    keys = ('body', 'html', 'sms', 'subject')
11
-
12
-    def test_no_templates_returns_empty_string(self):
13
-        et = CommunicationEventType()
14
-        messages = et.get_messages()
15
-        for key in self.keys:
16
-            self.assertEqual('', messages[key])
17
-
18
-    def test_field_template_render(self):
19
-        et = CommunicationEventType(email_subject_template='Hello {{ name }}')
20
-        ctx = {'name': 'world'}
21
-        messages = et.get_messages(ctx)
22
-        self.assertEqual('Hello world', messages['subject'])
23
-
24
-    def test_new_line_in_subject_is_removed(self):
25
-        subjects = [
26
-            ('Subject with a newline\r\n', 'Subject with a newline'),
27
-            ('New line is in \n the middle', 'New line is in  the middle'),
28
-            ('\rStart with the new line', 'Start with the new line'),
29
-        ]
30
-
31
-        for original, modified in subjects:
32
-            et = CommunicationEventType(email_subject_template=original)
33
-            messages = et.get_messages()
34
-            self.assertEqual(modified, messages['subject'])

+ 0
- 99
tests/integration/customer/test_dispatcher.py Bestand weergeven

@@ -1,99 +0,0 @@
1
-from django.core import mail
2
-from django.test import TestCase
3
-
4
-from oscar.apps.customer.models import CommunicationEventType, Email
5
-from oscar.apps.customer.utils import Dispatcher, get_password_reset_url
6
-from oscar.apps.order.models import CommunicationEvent
7
-from oscar.core.compat import get_user_model
8
-from oscar.test.factories import SiteFactory, create_order
9
-
10
-User = get_user_model()
11
-
12
-
13
-class TestDispatcher(TestCase):
14
-
15
-    def _dispatch_order_messages(self, order_number, order, email=None):
16
-        et = CommunicationEventType.objects.create(code="ORDER_PLACED",
17
-                                                   name="Order Placed",
18
-                                                   category=CommunicationEventType.ORDER_RELATED)
19
-
20
-        messages = et.get_messages({
21
-            'order': order,
22
-            'lines': order.lines.all()
23
-        })
24
-
25
-        self.assertIn(order_number, messages['body'])
26
-        self.assertIn(order_number, messages['html'])
27
-
28
-        dispatcher = Dispatcher()
29
-        dispatcher.dispatch_order_messages(order, messages, et)
30
-
31
-        self.assertEqual(CommunicationEvent.objects.filter(order=order, event_type=et).count(), 1)
32
-
33
-        self.assertEqual(len(mail.outbox), 1)
34
-
35
-        message = mail.outbox[0]
36
-        self.assertIn(order_number, message.body)
37
-
38
-        # test sending messages to emails without account and text body
39
-        messages['body'] = ''
40
-        dispatcher.dispatch_direct_messages(email, messages)
41
-        self.assertEqual(len(mail.outbox), 2)
42
-
43
-    def _dispatch_user_messages(self, user, event_code, ctx, subject):
44
-        msgs = CommunicationEventType.objects.get_and_render(
45
-            code=event_code, context=ctx)
46
-        Dispatcher().dispatch_user_messages(user, msgs)
47
-        self.assertEqual(len(mail.outbox), 1)
48
-        self.assertEqual(mail.outbox[0].subject, subject)
49
-        self.assertEqual(Email.objects.count(), 1)
50
-        email = Email.objects.last()
51
-        self.assertEqual(email.user.id, user.id)
52
-        self.assertEqual(email.email, 'testuser@example.com')
53
-
54
-    def test_dispatch_order_messages(self):
55
-        email = 'testuser@example.com'
56
-        user = User.objects.create_user('testuser', email,
57
-                                        'somesimplepassword')
58
-        order = create_order(number='12345', user=user)
59
-        self.assertFalse(order.is_anonymous)
60
-        self._dispatch_order_messages(order_number='12345', order=order, email=email)
61
-
62
-    def test_dispatch_anonymous_order_messages(self):
63
-        order = create_order(number='12346', guest_email='testguest@example.com')
64
-        self.assertTrue(order.is_anonymous)
65
-        self._dispatch_order_messages(order_number='12346', order=order, email='testguest@example.com', )
66
-
67
-    def test_dispatch_email_changed_user_message(self):
68
-        user = User.objects.create_user('testuser', 'testuser@example.com', 'somesimplepassword')
69
-        CommunicationEventType.objects.create(code='EMAIL_CHANGED',
70
-                                              name='Email Changed',
71
-                                              category=CommunicationEventType.USER_RELATED)
72
-        ctx = {
73
-            'user': user,
74
-            'site': SiteFactory(name='Test Site'),
75
-            'reset_url': get_password_reset_url(user),
76
-            'new_email': 'newtestuser@example.com',
77
-        }
78
-        self._dispatch_user_messages(
79
-            user, 'EMAIL_CHANGED', ctx, 'Your email address has changed at Test Site.'
80
-        )
81
-
82
-    def test_dispatch_registration_email_message(self):
83
-        user = User.objects.create_user('testuser', 'testuser@example.com', 'somesimplepassword')
84
-        CommunicationEventType.objects.create(code='REGISTRATION',
85
-                                              name='Registration',
86
-                                              category=CommunicationEventType.USER_RELATED)
87
-        ctx = {'user': user,
88
-               'site': SiteFactory()}
89
-        self._dispatch_user_messages(user, 'REGISTRATION', ctx, 'Thank you for registering.')
90
-
91
-    def test_dispatcher_uses_email_connection(self):
92
-        connection = mail.get_connection()
93
-        disp = Dispatcher(mail_connection=connection)
94
-        disp.dispatch_direct_messages('test@example.com', {
95
-            'subject': 'Test',
96
-            'body': 'This is a test.',
97
-            'html': '',
98
-        })
99
-        self.assertEqual(len(mail.outbox), 1)

+ 0
- 10
tests/integration/customer/test_models.py Bestand weergeven

@@ -1,10 +0,0 @@
1
-import pytest
2
-from django.core import exceptions
3
-
4
-from oscar.apps.customer import models
5
-
6
-
7
-def test_communication_event_type_code_forbids_hyphens():
8
-    ctype = models.CommunicationEventType(code="A-B")
9
-    with pytest.raises(exceptions.ValidationError):
10
-        ctype.full_clean()

+ 61
- 0
tests/integration/order/test_order_dispatcher.py Bestand weergeven

@@ -0,0 +1,61 @@
1
+from django.core import mail
2
+from django.test import TestCase
3
+
4
+from oscar.core.compat import get_user_model
5
+from oscar.core.loading import get_class, get_model
6
+from oscar.test.factories import create_order
7
+
8
+User = get_user_model()
9
+
10
+CommunicationEventType = get_model('communication', 'CommunicationEventType')
11
+CommunicationEvent = get_model('order', 'CommunicationEvent')
12
+Email = get_model('communication', 'Email')
13
+Dispatcher = get_class('communication.utils', 'Dispatcher')
14
+OrderDispatcher = get_class('order.utils', 'OrderDispatcher')
15
+
16
+
17
+class TestDispatcher(TestCase):
18
+
19
+    def _dispatch_order_messages(self, order_number, order, email=None):
20
+        event_code = OrderDispatcher.ORDER_PLACED_EVENT_CODE
21
+        et = CommunicationEventType.objects.create(
22
+            code=event_code,
23
+            name="Order Placed",
24
+            category=CommunicationEventType.ORDER_RELATED,
25
+        )
26
+
27
+        messages = et.get_messages({
28
+            'order': order,
29
+            'lines': order.lines.all()
30
+        })
31
+
32
+        assert order_number in messages['body']
33
+        assert order_number in messages['html']
34
+
35
+        order_dispatcher = OrderDispatcher()
36
+        order_dispatcher.dispatch_order_messages(order, messages, event_code)
37
+
38
+        assert CommunicationEvent.objects.filter(order=order, event_type=et).count() == 1
39
+
40
+        assert len(mail.outbox) == 1
41
+
42
+        message = mail.outbox[0]
43
+        assert order_number in message.body
44
+
45
+        # Test sending messages to emails without account and text body
46
+        messages['body'] = ''
47
+        dispatcher = Dispatcher()
48
+        dispatcher.dispatch_direct_messages(email, messages)
49
+        assert len(mail.outbox) == 2
50
+
51
+    def test_dispatch_order_messages(self):
52
+        email = 'testuser@example.com'
53
+        user = User.objects.create_user('testuser', email, 'somesimplepassword')
54
+        order = create_order(number='12345', user=user)
55
+        assert not order.is_anonymous
56
+        self._dispatch_order_messages(order_number='12345', order=order, email=email)
57
+
58
+    def test_dispatch_anonymous_order_messages(self):
59
+        order = create_order(number='12346', guest_email='testguest@example.com')
60
+        assert order.is_anonymous
61
+        self._dispatch_order_messages(order_number='12346', order=order, email='testguest@example.com', )

+ 35
- 0
tests/integration/templatetags/test_url_tags.py Bestand weergeven

@@ -0,0 +1,35 @@
1
+from django import template
2
+from django.contrib.sites.models import Site
3
+from django.test import TestCase
4
+
5
+
6
+class TestUrlTags(TestCase):
7
+
8
+    def test_absolute_url_tag(self):
9
+        tmpl = template.Template(
10
+            '{% load i18n %}'
11
+            '{% load url_tags %}'
12
+            '{% absolute_url site.domain path %}.'
13
+        )
14
+        out = tmpl.render(template.Context({
15
+            'site': Site.objects.get_current(),
16
+            'path': '/some/test/path',
17
+        }))
18
+        assert out == 'http://example.com/some/test/path.'
19
+
20
+    def test_absolute_url_tag_with_blocktrans(self):
21
+        tmpl = template.Template(
22
+            '{% load i18n %}'
23
+            '{% load url_tags %}'
24
+            '{% absolute_url site.domain path_1 as url_1 %}'
25
+            '{% absolute_url site.domain path_2 as url_2 %}'
26
+            '{% blocktrans with test_url_1=url_1 test_url_2=url_2 %}'
27
+            '1st - {{ test_url_1 }}. 2nd - {{ test_url_2 }}.'
28
+            '{% endblocktrans %}'
29
+        )
30
+        out = tmpl.render(template.Context({
31
+            'site': Site.objects.get_current(),
32
+            'path_1': '/some/test/path',
33
+            'path_2': '/some/another/test/path',
34
+        }))
35
+        assert out == '1st - http://example.com/some/test/path. 2nd - http://example.com/some/another/test/path.'

+ 0
- 0
tests/settings.py Bestand weergeven


Some files were not shown because too many files changed in this diff

Laden…
Annuleren
Opslaan